As with a large portion of people during these post-pandemic times I have found myself spending more and more time online. Most of that online interaction has been playing games with friends online.
Obviously trying to coordinate between multiple 30 year olds is a bit difficult what with all responsabilities that that entails.
For a while we were using Text Messages and GroupMe but that was unwieldly specially with both Android and IOS.
The solution was to use discord, and along the way also help us solve the problem of audio across mutliple different consoles.

One cool thing about Discord is the very robust system for setting up bots to do different things on each server. There are a lot of different ones you can add. However I'm still a little paranoid about giving access to a private communication channel to a random third party. I also have a bunch of raspberrypi's laying around so I decided to code one myself.

Setting up

Setting up a bot is really straight forward. Discordpy is a great wrapper for the functions and has documentation on the steps necessary.

You will need to:

  1. Create a new application on the Discord site
  2. Create a Bot User (not Public)
  3. Get the bot token
  4. Invite your bot to your server

Getting the Python Client up

As mentioned before most of the heavy lifting is done by the discord.py module.
The first step is storing our credentials in a place that can be accessed by our script. In my case I use the .env method.

# .env
TOKEN='YOURBOTTOKEN'

Then in our main file all we need to do is initialize the client and run it at the end

# main.py
import discord
import os
from dotenv import load_dotenv, find_dotenv
from typing import Iterator, Optional
import logging

client = discord.Client()

def get_vars() -> Optional[bool]:
    try:
        dotenv_path = find_dotenv(".env")
        logging.info("Dotenv located, loading vars from local instance")
        return load_dotenv(dotenv_path)

    except:
        logging.info("Loading directly from system")

get_vars()

`
`
`
`
`
`
`
client.run(os.getenv('TOKEN'))

Reacting to Events

Now that we have our bot logged into discord and ready running we can work on defining the events that will trigger a response.

To do so all we need to do is use the @client.event decorator. A simple one to check if the bot is working is as follows:

# main.py
@client.event
async def on_ready():
    print('We have logged in as {0.user}'.format(client))

Now if we wanted to do something more complex we have to monitor the messages on a given channel. Discord has a a guide on obtaining a channel's internal id. So for example if we wanted to get a response from

# main.py
import discord
import os
from dotenv import load_dotenv, find_dotenv
from typing import Iterator, Optional
import logging

client = discord.Client()

# CHANNEL DEFININITIONS
GENERAL= YOURCHANNELID

@client.event
async def on_message(message):
    if message.channel.id== GENERAL:
        msg=message.content

        if msg.lower().startswith("BotCheck"):
            await.message.channel.send("https://supercoolgif.gif")

So we defined an event everytime a message is recieved. We evaluate the channel in which that message was posted, and if the message starts with "BotCheck" we have the bot post a gif.

Setting up polls

Now one thing that I wanted to have was an easy way to start polls without having to link to a different website. In order to do this will take advantage of discord reactions. This is going to be divided into two separate event watchdogs. One to generate the poll and one to allow for voting on it.

# Poll Creation Code

#define the number emojis to be used as reactions, could also just use the id's for the emojis
numberEmoji=['1️⃣','2️⃣','3️⃣','4️⃣']

#If a message is recieved in General
if message.channel.id == GENERAL:
        msg = message.content
        #If the msg starts with $Poll
        if msg.startswith("$Poll"):
            # Split by the | decorator
            itms=msg.split('|')
            # Need at least 3 items and at most 6
            if len(itms)>3 and len(itms)<6:
                # Create an discord embed message
                msgEmbd=discord.Embed(title="Bot wants to know", description="__{}__".format(itms[1]))
                used_Em=[]
                # For each item add an emoji with what we want to be the option
                for itm, emj in zip(itms[2:],numberEmoji):
                    msgEmbd.add_field(name=emj, value=itm, inline=False)
                    used_Em.append(emj)

                # In order to track the responses we setup a footer with the BP prefix and all the used emjos.
                msgEmbd.set_footer(text="BP|{}".format('.'.join(used_Em)))

                # Post the poll
                await message.channel.send(embed=msgEmbd)
                await message.delete()
            else:
                message.reply("I can't let you do that StarFox!")

This takes care of creating the poll in the system. The users enter the command BP|Option1|Option2| and that generates the correct poll.

Sample poll

Now that we can create the polls we need a way to only allow certain reactions (the numbered ones) on it to keep it clean.

# Poll reaction

# Watch for reactions
@client.event
async def on_reaction_add(reaction, user):
    msg=reaction.message
    # Check to see if the msg that is being reacted to is an embed
    if len(msg.embeds)>0:
                emb=msg.embeds[0]
                #Check the footer of the embed to see if it contains the BP code
                ft=emb.footer
                if ft.text.startswith('BP'):
                    print('This is a poll')
                    # Get the allowed reactions
                    allowed_reacts=ft.text.split('|')[1].split('.')
                    # If the reaciton is not allowed then earase it.
                    if reaction.emoji not in allowed_reacts:
                        print('Cant let you do that')
                        await reaction.clear()

All this code does it take a look at the messages in that are being reacted and evaluating if its a poll embed. Finally it will check that the emojis being used are in the list previously defined.

Democratic Deleting of Images

Finally the one thing we needed was a way to have the server members vote on removing certain content from the server. I wanted this to be democratic so that it was clear that a majority of the members did not want it. This is mostly due to having one user that likes to post edgy content.

Once again we write a function to check if the image was posted and another to handle the voting.

USERSTOSUPERVISE='SET TO ID'
extensionsToCheck=['.jpg','.jpeg','.png','.gif']

# functions to check content and attachments

def attachIsImage(msgAttach):
    url = msgAttach.url
    if any(ext in url for ext in extensionsToCheck):
        return True
    else:
        return False

def contentIsImage(msg):
    if any(ext in msg for ext in extensionsToCheck):
        return True
    else:
        return False


@client.event
async def on_message(message):
    # Limit ourselves to a given channel
    if message.channel.id == GENERAL:
        #If the user matches the one to check (could also use a list)
        if message.author.id == USERSTOSUPERVISE:
            # Check if the post has images in the attachments
            if len(message.attachments) > 0:
                isIm=False
                print("detected image")
                for att in message.attachments:
                    if attachIsImage(att):
                        isIm=True
            # Check to see if the content of the message contains an image
            else:
                isIm=False
                print("detected image")
                if contentIsImage(message.content):
                    isIm=True
            #Post a reply to the image 
            if isIm:
                await message.reply("#ConGuard : User submitted and image,
                 if 3 react with 💩 bot will drop the hammer")

This code checks to see if an image is posted by a given user and posts a direct reply to it. This lets us track reactions to it.

con_guard=discord.Embed(title="Bot Keeping you safe")

@client.event
async def on_reaction_add(reaction, user):
    msg=reaction.message
    if msg.channel.id == GENERAL:
        #Reactions to Bot
        # Check to see if the author is the bot user
        if msg.author.id == BOTID:
            #Check if the content starts with the hashtag we define before
            if msg.content.startswith('#ConGuard'):
                # Check if our preferred emoji is the selected one
                if reaction.emoji == '💩':
                    #Get the count of the emojis on it, if its larger than 3 then delete the message resolved
                    # and post a new embed on the server.
                    if reaction.count>=3:
                        rmsg=msg.reference.resolved
                        newemb=con_guard
                        newemb.set_image(url='https://giphy.gif')
                        newemb.add_field(name="Bot does not approve", value="ALL PRAISE BOT", inline=False)
                        newemb.set_footer(text='CONGUARD')
                        await msg.channel.send(embed=newemb)
                        await rmsg.delete()

The basic flow is to check all reactions, identify if its one of the bot messages and delete if the count matches.

Final Steps

Once we are done with all the code I just went ahead and moved the script to raspberrypi 3 and set it up to fire the script on boot. If it all goes well the bot should show up as a logged in user on the side bar.