Skip to content

Commit

Permalink
Add soup, minecraft status, roll, random reactions
Browse files Browse the repository at this point in the history
This should have been multiple commits, but I wasn't planning to push
this ever
  • Loading branch information
nfaltermeier committed Apr 7, 2022
1 parent 410e1d6 commit 8e4713a
Show file tree
Hide file tree
Showing 11 changed files with 331 additions and 29 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ checkpoint
disabled_checkpoint
secret-scholars-bot-config.json
src/config.py
secret-scholars-bot-arm32v6.tar.gz

# Created by https://www.toptal.com/developers/gitignore/api/windows,python
# Edit at https://www.toptal.com/developers/gitignore?templates=windows,python
Expand Down Expand Up @@ -175,3 +176,5 @@ $RECYCLE.BIN/
*.lnk

# End of https://www.toptal.com/developers/gitignore/api/windows,python

!/src/lib/
12 changes: 12 additions & 0 deletions Dockerfile-arm32v6
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Based off of https://github.com/minimaxir/gpt-2-cloud-run/blob/master/Dockerfile
FROM arm32v6/python:3.8-alpine

RUN apk update && apk add git gcc musl-dev

WORKDIR /code

RUN pip3 --no-cache-dir install -U git+https://github.com/Rapptz/discord.py python-dotenv

COPY .env src/ secret-scholars-bot-config.json ./

CMD [ "python", "./bot.py" ]
18 changes: 16 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# About
A discord bot that doesn't do all that much at the moment.
A discord bot that randomly reacts to messages, can monitor a minecraft server, and post the top post from r/soup, and roll a random number.

Designed to be ran in a Docker container. I would recomend removing the `.env` file from the image before pushing the image anywhere.
Has x86 and ARM32v6 Docker configurations.

## Config
`.env`
Expand All @@ -12,14 +13,27 @@ DISCORD_TOKEN=<discord bot token>
Copy `src/config.py.example` to `src/config.py` and customize as desired.

## Build

### x86
Run
```sh
docker build -t secret-scholars-bot .
```

### ARM32v6
Run
```sh
docker build -f Dockerfile-arm32v6 -t secret-scholars-bot:arm32v6 --build-arg ARCH=arm32v6/ .
```

## Run
### x86
Run
```sh
docker run -d secret-scholars-bot
```

### ARM32v6
Run
```sh
docker run -d secret-scholars-bot secret-scholars-bot:arm32v6
```
40 changes: 29 additions & 11 deletions src/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@
import logging
from datetime import datetime, timezone
import donut
import soup
import roll
import config
import asyncio
import minecraft

logging.basicConfig(level=logging.INFO)

Expand All @@ -17,6 +20,8 @@
@client.event
async def on_ready():
logging.info(f'{datetime.now(timezone.utc)} We have logged in as {client.user}')
donut.add_emoji(client, config)
minecraft.start(client, config)

# API says this is called for thread creation and joining a thread...
@client.event
Expand All @@ -25,19 +30,32 @@ async def on_thread_join(thread):
logging.info(f'{datetime.now(timezone.utc)} Joined thread {thread.name}')
await thread.join()

# https://discordpy.readthedocs.io/en/stable/api.html?highlight=react#discord.RawReactionActionEvent

@client.event
async def on_message(message):
if message.author == client.user:
return

await donut.on_message(message, config)

if message.content.startswith('$hello'):
bot_response = await message.channel.send('Hello!')
await asyncio.sleep(300)
logging.info(f'{datetime.now(timezone.utc)} Attempting to delete hello messages')
await message.delete()
await bot_response.delete()
try:
# Ignore the bot's own messages
if message.author == client.user:
return

# keep most bot functions restricted to the home server
if message.guild is None or message.guild.id != config.homeserver_id:
return

await donut.on_message(message, config, client)
await soup.on_message(message, client, config)
if await roll.on_message(message):
return

if message.content.startswith('$hello'):
await message.channel.send('Hello!', delete_after=300)
await asyncio.sleep(300)
await message.delete()
except BaseException as error:
logging.exception(f'{datetime.now(timezone.utc)} main on_message failed')
await message.channel.send("Something went wrong :(")


load_dotenv()
client.run(os.getenv('DISCORD_TOKEN'))
14 changes: 14 additions & 0 deletions src/config.py.example
Original file line number Diff line number Diff line change
@@ -1,2 +1,16 @@
homeserver_id = 12345 # Discord server ID for the bot's main server that most features are locked to

donut_ids = [12345] # discord user IDs to donut
strict_donuts = False

random_reaction_emoji = ["😼"] # emoji characters to randomly react to messages with
random_reaction_custom_emote_ids = [12345] # discord ids of custom emotes to randomly react to messages with
random_reaction_base_chance = 2.5 # 2.5 is 2.5% chance to react to a message
random_reaction_chance_increment = 0.25

minecraft_status_report_enabled = False
minecraft_status_report_ip = "1.1.1.1"
minecraft_status_report_port = 25565
minecraft_status_report_channels = [12345] # Discord channel IDs to report the status of the minecraft server to

soup_channels = ["soup"] # The names of channels that the soup command can be used in
67 changes: 51 additions & 16 deletions src/donut.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,54 @@
import discord
import asyncio
import random
import logging

async def on_message(message, conf):
if message.author.id in conf.donut_ids:
try:
await message.add_reaction("🍩")
await message.add_reaction("🇩")
await message.add_reaction("🇴")
await message.add_reaction("🇳")
await message.add_reaction("🇺")
await message.add_reaction("🇹")
await message.add_reaction("🇸")
except discord.HTTPException as result:
if result.status == 403 and conf.strict_donuts:
await message.delete()
bot_response = await message.channel.send("Donut be naughty...")
await asyncio.sleep(10)
await bot_response.delete()
emoji = []
chance = 0

def add_emoji(client, conf):
global emoji, chance
emoji = conf.random_reaction_emoji
for e in conf.random_reaction_custom_emote_ids:
emoji.append(client.get_emoji(e))

chance = conf.random_reaction_base_chance

async def on_message(message, conf, client):
global chance

if random.randint(0, 100) < chance:
await message.add_reaction(emoji[random.randrange(0, len(emoji))])
chance = random.uniform(0, conf.random_reaction_base_chance * 2)
else:
chance += conf.random_reaction_chance_increment
if message.author.id in conf.donut_ids:
try:
if random.randint(1, 5) == 1:
await message.add_reaction("🇱")
await message.add_reaction("🇮")
await message.add_reaction("🇬")
await message.add_reaction("🇲")
await message.add_reaction("🇦")
await message.add_reaction("🇩")
await message.add_reaction("🇴")
await message.add_reaction("🇳")
await message.add_reaction("🇺")
await message.add_reaction("🇹")
await message.add_reaction("🇸")
await message.add_reaction("🍩")
await message.add_reaction("👅")
else:
await message.add_reaction("🍩")
await message.add_reaction("🇩")
await message.add_reaction("🇴")
await message.add_reaction("🇳")
await message.add_reaction("🇺")
await message.add_reaction("🇹")
await message.add_reaction("🇸")
except discord.HTTPException as result:
if result.status == 403 and conf.strict_donuts:
await message.delete()
bot_response = await message.channel.send("Donut be naughty...")
await asyncio.sleep(10)
await bot_response.delete()
Empty file added src/lib/__init__.py
Empty file.
110 changes: 110 additions & 0 deletions src/lib/mcping.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# Code taken from https://gist.github.com/MarshalX/40861e1d02cbbc6f23acd3eced9db1a0
import socket
import struct
import json
import time

class StatusPing:
""" Get the ping status for the Minecraft server """

def __init__(self, host='localhost', port=25565, timeout=5):
""" Init the hostname and the port """
self._host = host
self._port = port
self._timeout = timeout

def _unpack_varint(self, sock):
""" Unpack the varint """
data = 0
for i in range(5):
ordinal = sock.recv(1)

if len(ordinal) == 0:
break

byte = ord(ordinal)
data |= (byte & 0x7F) << 7 * i

if not byte & 0x80:
break

return data

def _pack_varint(self, data):
""" Pack the var int """
ordinal = b''

while True:
byte = data & 0x7F
data >>= 7
ordinal += struct.pack('B', byte | (0x80 if data > 0 else 0))

if data == 0:
break

return ordinal

def _pack_data(self, data):
""" Page the data """
if type(data) is str:
data = data.encode('utf8')
return self._pack_varint(len(data)) + data
elif type(data) is int:
return struct.pack('H', data)
elif type(data) is float:
return struct.pack('Q', int(data))
else:
return data

def _send_data(self, connection, *args):
""" Send the data on the connection """
data = b''

for arg in args:
data += self._pack_data(arg)

connection.send(self._pack_varint(len(data)) + data)

def _read_fully(self, connection, extra_varint=False):
""" Read the connection and return the bytes """
packet_length = self._unpack_varint(connection)
packet_id = self._unpack_varint(connection)
byte = b''

if extra_varint:
# Packet contained netty header offset for this
if packet_id > packet_length:
self._unpack_varint(connection)

extra_length = self._unpack_varint(connection)

while len(byte) < extra_length:
byte += connection.recv(extra_length)

else:
byte = connection.recv(packet_length)

return byte

def get_status(self):
""" Get the status response """
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as connection:
connection.settimeout(self._timeout)
connection.connect((self._host, self._port))

# Send handshake + status request
self._send_data(connection, b'\x00\x00', self._host, self._port, b'\x01')
self._send_data(connection, b'\x00')

# Read response, offset for string length
data = self._read_fully(connection, extra_varint=True)

# Send and read unix time
self._send_data(connection, b'\x01', time.time() * 1000)
unix = self._read_fully(connection)

# Load json and return
response = json.loads(data.decode('utf8'))
response['ping'] = int(time.time() * 1000) - struct.unpack('Q', unix)[0]

return response
36 changes: 36 additions & 0 deletions src/minecraft.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import logging
from datetime import datetime, timezone
from discord.ext import tasks
import socket
from lib import mcping

server = None
client = None
channels = []

def start(bot, conf):
global client, server, channels
if not conf.minecraft_status_report_enabled:
return
server = mcping.StatusPing(conf.minecraft_status_report_ip, conf.minecraft_status_report_port)
client = bot
for channel in conf.minecraft_status_report_channels:
channels.append(client.get_channel(channel))
ping_minecraft.start()

@tasks.loop(minutes=5)
async def ping_minecraft():
try:
status = server.get_status()
message = f"{status['description']['text']}: {status['players']['online']}/{status['players']['max']} players online"
for channel in channels:
await channel.send(message, delete_after=600)
except socket.timeout as timeout:
message = "Connection timed out (server offline)"
for channel in channels:
await channel.send(message, delete_after=600)
except BaseException as error:
message = "An error occurred while pinging the server"
logging.exception(f'{datetime.now(timezone.utc)} minecraft ping failed')
for channel in channels:
await channel.send(message, delete_after=600)
11 changes: 11 additions & 0 deletions src/roll.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import discord
import asyncio
import random
import logging

async def on_message(message):
if message.content.startswith('$roll'):
await message.channel.send(f'{message.author.mention} rolled {random.randint(0, 100)}')
return True
else:
return False
Loading

0 comments on commit 8e4713a

Please sign in to comment.