Making Discord bots in Python

Unlike webhooks, discord bots are invited into servers, and managed through a special developer portal. A link to the discord Python API reference can be found here.

Using the developer portal to create a bot

In order to develop a bot, we need to register the correct authentication tokens on the developer portal, through OAuth2. To do this we, on the main dashboard of the portal

  • select New Application

  • give the application a name

  • navigate to the Bot tab on the hamburger menu, and attach a bot to the application

OAuth2 protocol generates limited-access tokens based on the permissions granted to the application/bot.

Overview of the portal

The following explains some of the details which may be configured in the developer portal.

Application features

TODO; what is the client ID, what is the client secret?

Bot features

Bots can be granted special OAuth2 tokens, and as many permissions as they need. It also allows you to access the secret bot tokens required to activate the bot remotely from a server.

TODO; what are all the setting available?

Adding a bot to the server

To add our bot to the server, we require a key from the OAuth2 URI generator.

Generating OAuth2 URIs

From the hamburger menu, select OAuth2. This dashboard is a wrapper for the OAuth2 API, and manages and generates credential tokens.

For our bot we want to add user access to the discord API; to do this, scroll down and select

  • bot from SCOPES

  • Administrator from BOT PERMISSIONS

NB: for deployment, best not to select Administrator but tailor your needs specifically for the bot. Discord now generates the authorization URI, with the correct scope and permissions for our development bot.

Select Copy and paste it into the browser in order to invite the application/bot to your server.

Using python to handle application functionality

The environment requires the discord.py API packages

pip install discord.py

Connecting to a Discord server

Require a Client instance to connect to the server. The client represents a connection to a given discord server; the servers the bot is currently connected to is handles by client.guild. The client is able to interact with the full discord API.

import discord, os
TOKEN = os.getenv("DISCORD_TOKEN")	# read in the secret token

client = discord.Client()

@client.event
async def on_ready():			# once connection established
	print("{} is here lads!".format(client.user))

client.run(TOKEN)

We set the environment variable with the secret bot token

export DISCORD_TOKEN=bot-token

The client instance comes with a range of information on the connected server. For example, to print out the user list of a given server, we could use

guild = [g for g in client.guilds if g.name == "YOUR SEVER NAME"]
for member in guild:
	print(member.name)

Utility functions

The discord python API includes a utils module to fasciculate quality-of-life functions, such as the ability to find

guild = discord.utils.find(lambda i: i.name == "YOUR SERVER NAME", client.guilds)

Responding to events

Event triggers can be defined by either using the client.event decorator, or by creating a custom client class, inheriting from discord.Client, and overriding the appropriate methods. Some common endpoints are


class NewClient(discord.Client)
	async def on_ready(self):
		pass

	async def on_member_join(self, member):
		pass

	async def on_message(self, message):
		pass

Messages

The message argument given to the on_message() function contains the message contents and metadata, for example

if message.author == client.user: 	# encase you messaged yourself
	return

if message.content == "Hey fuck you!":
	resp = "Yes! Yes! Fuck you too!"
	await message.channel.send(resp)

Direct messaging channels

To direct message a member, we can create a new dm channel, with

await member.create_dm()
await member.dm_channel.send("MESSAGE")

Connecting a bot

Subclasses of Client include bot, which has tailored functionality for bot interactions. This includes the whole of the commands API, which the superclass Client does not have access to.

Attaching a bot

Instead of using an instance of Client, we instead want to use an instance of Bot, with many of the same endpoints as client. The primary difference is that we are going to prefix the bot with a command token, e.g.

from discord.ext import commands

bot = commands.Bot(command_prefix='?')

Bot commands

Commands, different to the event endpoints, are arbitrary. We can define a command endpoint with

@bot.command(name='beer', help='Ask the bot for a beer.')
async def give_beer(context):
	resp = "nah mate it's my last one"
	await context.send(resp)

Given the command prefix of ?, the bot will now respond to the input ?beer. Similarly, using ?help will trigger the help text to be shown.

Converting commands

Say we wanted to create a command that takes parameters as arguments, we could do so for our bot with

@bot.command(name='beer', help='Ask the bot for <n> beers')
async def give_beer(context, n: int):
	await content.send(f"You want {n}? Nah mate...")

Python3’s builtin annotations will automatically get the API to convert the argument into the type you want, and handle exceptions if the input does not match.

Command predicates

We can make sure that our commands have the correct permissions or environment in order to execute the desired task. For example, we can ensure we have admin permissions by using

@bot.command(name='create-channel')
@commands.has_role('admin')
async def create_channel(context, channel_name: str):
	if not discord.utils.get(context.guild.channels, name=channel_name):	# make sure channel doesn't already exist
		await guild.create_text_channel(channel_name)