I volunteer to help run Southwest CCDC every year, and had a need to deploy all of the communication infrastructure in a hurry. With Covid Times™ upon us, we needed to move a competition that usually has at least one round in person to all-virtual. Discord was the obvious choice for how to do that successfully - it is targeted to communities of people, and has moderation tools. My discord server needed to have a few things:
- A few general channels for normal Discord business - announcements, general chatter, help requests etc.
- Private channels for each competing team
- Private channels for staff to organize during the event
Given that the smallest round will usually have 8 teams and the largest round varies - the only answer to make it correctly, efficiently, and correctly seemed to be automation. Clicking around in a GUI isn’t fast nor is it easy to track what’s been done and what is left. Discord’s settings screens are also always full-screen instead of being something smaller you can drag around.
Creating a Discord bot
While my preferred language is PowerShell, sometimes there’s no point re-inventing the wheel and it’s worth it to get your hands dirty in a second language. In this case the best/most well supported option seemed to be python, in the form of Discord.py.
The first step is to head to Discord’s developer portal and create an application. Here’s where you set the icon your bot will use, along with its name also a few extra bits.
Creating an application is not enough to give your bot a presence on a server, so you also need to move down to the Bot tab, and also add an icon and username there. While you’re there you’ll have to turn on the ‘Server Members Intent’ toggle. This is for Reasons. The short story is that Discord feels that bots shouldn’t be able to read the members list merely by expressing that intent in code. Other intents don’t need this, and can be requested in your bot code.
I did my development on an empty test server, so I switched to the OAuth2 tab, and selected the ‘bot’ scope, set the ‘Administrator’ permission, and copied/pasted the URL provided to a new tab to authorize my bot to join the test server. Yes, administrator is a dangerous permission to grant for a bot, but given that it’s my code, I am fine with it in this case. Also some role-managing stuff didn’t work particularly well without it.
Writing the code
Discord.py has a few examples you can start from.
Because I have been exposed to Python before, I was already aware that python projects should make use of virtual environments. I created a folder for my code, initialized git in the folder to help me keep track of code revisions and began to write.
I created my virtual environment as a sub-folder. I couldn’t possibly say whether it’s good practice, but it worked. The virtual environment was in a subfolder called ‘venv’ so I added it to my
.gitignore file. You may find pyenv useful.
Having activated my virtual environment, I followed the instructions to use
pip to add the required modules for Discord.py, and added a few of my own. I wanted to avoid storing credentials in a git repository, and the best way to do that is to hand them off to a vault of some kind. A quick google search found me a module called
keyring. I also wanted to make sure that as I typed the token to save it, it wouldn’t echo to the screen, so that led me to another module called
getpass. Seems simple enough so far - they all seem to do what they say they will. The bot’s name is MR_FLOOFY.
Having a few extra python
pip modules in place, I followed the instructions to use
pip to freeze the list of installed modules in my environment to
requirements.txt. This will allow me to quickly re-create this virtual environment if it is destroyed, or I need to change where it’s hosted.
With a Secure Enough™ way of saving and retrieving the token devised, I headed back to Discord’s developer portal, my app, and the Bot tab to get the token I’d need to allow the bot to connect.
I’m not going to describe the entire, frustrating process of learning just enough about python and Discord.py to make this work, I’m just going to describe how the code works now that it works well enough for my purposes. In the future I’ll probably keep refining it.
The first thing one typically does in a python file is import the modules you’ll need in the script.
Next, we set some discord-specific options, retrieve our token and build objects to use later. The
intents object is used to tell Discord what things the bot will need access to, so we create the object with the defaults, then also ask for the member list.
client object is what we’ll use later to make things go. The
logging object seems to come for free because we used
import logging earlier , I didn’t have to specifically create it. We define the service id that matches the service id from when we ran
save_key.py earlier. We then retrieve the token to use later.
The logging settings are also very important - if you screw up and do too many things to some API endpoints, you’ll get locked out of it for a period of time. Some of the endpoint limits are ridiculously low and the logging will print to the screen to let you know when you’ve hit one of those limits so you don’t enrage yourself wondering why code that worked a few minutes ago doesn’t work now.
This bot is meant to listen for messages that meet criteria, so we use this code to define what the bot will look for. I mostly looked through the Discord.py examples linked earlier to figure out how to get started.
We define the event we care about, then say what should happen when that event fires, in our case
on_message. I believe this is a ‘callback’ but don’t quote me. The
message object contains the contents of the message, who sent it, the server it came from, and a number of other useful properties.
If the message meets some criteria, we do some stuff, then
return to end processing. In this case the only one that needs to issue commands is me, so our first two conditions just check to see if the bot is talking to itself, or whether the caller is not me. Obviously the second condition would make the first condition completely irrelevant - but I’m leaving it in for this example.
CCDC games are run by several teams. Given the audience of this Discord server and it’s purpose, I have to code for 5 separate roles.
- Gold team - organizers of the competition
- Black team - infrastructure for the game, including the game machines themselves
- White team - folks who help run the people side of the competition.
- I also have to set up special rights for team coaches, who are allowed to observe but not help.
- A role for each team
I originally had all of this bundled up in one command. It was really fun to issue one command and watch an entire discord server populate, but like all good things - it got too complicated and debugging turned into a pain.
This next stanza sets up the non-competition roles, along with what color they should appear as, whether the role members should be listed apart from all other server members (’hoisted’) , server-wide permissions, etc.
Now, we need to create a category of channels for staff using these roles, along with customized permissions for channels inside that category.
We begin by getting the role objects we created earlier. We then create permission override objects we’ll use when we create the channels. Following that, we create the category, get an object representing it, and create channels using the category object and the permission overrides.
Here we come to the meat of the script - how do you quickly create a number of teams along with some work channels, locked down to keep each blue team out of each other’s business? Some loops mostly, using the code we’ve already seen above.
Generally speaking, here’s how the permissions for teams are supposed to work:
Black/Gold/White/[1 Blue team] should have r/w for text and voice, while coaches can see everything and say nothing. This is to replicate the in-person experience where team coaches serve as room monitors for all teams except their own. It’s not just about rule enforcement however, coaches have access to this level of information because it will help them coach their teams and learn from other team experiences. Since the ultimate goal of the competition is education, this aligns with that goal without making the competition unfair.
The coach role permissions below are not good enough, they allow the guild-wide permissions for the coach role to filter down to the teams. I’ll change how those work in future versions of Mr Floofy.
As you can see, we create a number of channels. Between the number of categories and channels involved, doing this by hand would be prohibitive and prone to failure.
Lastly, as a final command to help set things up, I wanted a way to automate assigning a user to a role in a way where I could easily paste some commands and get a team in their roles from a spreadsheet, given how the list of team members would be sent to me.
It should be noted that the roles are case-sensitive here.
Lastly, we need an event to fire when the script has initialized and begun to run. We’ll use the objects from the top of the script and tell the bot to go.
It has been an interesting experience building this bot, and as simple as it is, I’m very proud of it. I look forward to extending or replacing it with a proper PowerShell bot, perhaps PoshBot. I deeply hate the experience of trying to debug python, especially in the context of a bot like this.
I hope it’s been instructive to you.
I want to thank my friend George for both his expertise and patience as I struggled through some of the nastier parts of getting used to Python.
I also want to thank the fine folks at Pixabay for this beautiful header image that expresses my feelings about python.
Aaron Glenn Admin/Mentor
Aaron is a Sr. Systems Administrator from Tulsa, specializing in Microsoft technologies, with an emphasis on powershell.