Building and Launching Your Discord Bot: A Step-by-Step Guide
Unlock the full potential of your Discord community by learning how to create and deploy a custom bot using Python and the Discord.py library
Unlock the power of summarization with Discord TL;DR. Follow this guide to try it for yourself and see how it can condense vast amounts of information into succinct, easy-to-digest summaries. ⭐
Welcome to the exciting world of Discord bot development! In this blog post, we will be covering how I built a powerful chat summarization bot that can handle multiple servers and users, all while scaling to meet the needs of your growing user base.
All the code from this guide is open-sourced on GitHub. 🚀
We will start by setting up the skeleton of our bot and exploring the various ways to handle user data with SQL.
From there, we will delve into the asynchronous nature of Discord bot development and learn how to take full advantage of asyncio to ensure our bot remains responsive at all times.
Next, we will dive into the bot commands, allowing us to customize the functionality of our bot and give users greater control over their chat experience.
Finally, we will containerize our code with Docker and deploy everything to AWS using GitHub Actions, ensuring that our bot is always up and running and able to handle the demands of a large user base.
So grab your favorite Python IDE, and let’s start building a powerful Discord bot that can help users quickly summarize chats and stay up to date with the latest conversations!
Bot skeleton
Before we can start building out the functionality of our Discord bot, we need first to set up the skeleton of our project.
This includes creating a new Discord bot, obtaining the necessary API credentials, setting up our development environment, and installing any required dependencies.
Create the bot
We will use the Python package discord.py
, a fantastic tool that wraps all the Discord API functions together. Please follow this quick and simple tutorial to create your bot.
Don’t forget to create your development Discord server, where we can invite the newly created bot and test all the features. If you are not sure how to make a new Discord server, please follow this link.
Discord bot basics
To start building our Discord bot, we need first to understand its basic structure. At its core, a Discord bot is a class inherited from discord.Client
that defines various event handlers to process different events occurring within Discord.
For example, we can define an on_ready
event handler to run code when the bot first connects to Discord or an on_guild_join
event handler to run code when the bot is added to a new server.
Additionally, we will want to define a setup_hook
method to handle any necessary setup tasks and an on_guild_remove
event handler to clean up any resources when the bot is removed from a server.
Here is the minimum skeleton of a Discord bot to run:
We will use this bot class in a main()
method to run the bot:
We are using an if __name__ == “__main__”
block to wrap the entry point of the launch of the bot.
Using RotatingFileHandler
to store logs is a helpful way to keep track of your application’s activity and debug any issues that may arise. One of the key benefits of RotatingFileHandler
is that it allows you to specify a maximum number of log files to keep, ensuring that your logs do not consume too much disk space. Here, we are using five files which can be 32Mib max.
Also, we are using ClientSession, a class provided by the aiohttp library that allows you to make HTTP requests asynchronously in Python. When working with asyncio, it is often beneficial to use ClientSession instead of the built-in requests library, as ClientSession is designed to work seamlessly with asyncio and can significantly improve the performance of your code.
One of the key benefits of using ClientSession is that it allows you to make multiple HTTP requests concurrently rather than waiting for each request to complete before making the next one. This can be especially useful when working with Discord bots, as it allows you to create multiple requests to the Discord API in parallel, improving the overall performance of your bot.
In addition to making HTTP requests, ClientSession provides other valuable features, such as automatic connection pooling and the ability to set custom headers and timeouts, even if we won’t use them here.
Run the bot
Now that we have set up the basic skeleton of our Discord bot, it’s time to bring everything together and run our bot for the first time. To do this, we must provide our bot’s API token and run the script.
Use a .env
file to store all the environment variables for the project, like your Discord token.
# Update the .env file with your Discord Token
DISCORD_TOKEN="PUT YOUR DISCORD TOKEN HERE"
Launch your bot:
$ python __main__.py
Assuming everything is set up correctly, this should launch our Discord bot and connect it to Discord. From there, we can start building out the functionality of our bot and adding new features as needed.
Store data
Before developing any features for the bot, we have to decide how we will store users’ data. Storing user data can be a crucial aspect of building a Discord bot, as it allows you to provide a personalized and customized experience for each user.
However, it’s essential to carefully consider your database choice when storing user data. Different databases have different strengths and weaknesses, so choosing a database that suits your needs is essential.
SQLite
For our Discord TL;DR project, I used an SQLite database to store our users’ data. We chose SQLite for this project for several reasons:
- SQLite is a lightweight database that is easy to set up and use. It doesn’t require any additional server processes or configuration, making it a simple and convenient choice for small projects or the start of a project.
- SQLite is fast and efficient, making it well-suited for use in a Discord bot where performance is critical. It can handle many read and write operations quickly, ensuring that our bot remains responsive.
- If our project grows in the future and we need a more robust database, we can easily switch to a different database such as MySQL or PostgreSQL.
Another reason I was confident about using SQLite for this project is its compatibility with the aiosqlite
package. If you allow users to make asynchronous commands on multiple servers, but the database can’t handle async, it becomes the bottleneck of the whole project.
However, aiosqlite
allows us to use SQLite in an async context, making it easy to integrate SQLite into our asyncio-based Discord bot. This is precisely the scalability that we aim to provide with this project.
Here is a basic setup of our Python class that will handle the connection between the Discord Python code and the SQLite database.
This code defines a BotDB
class that represents a database for a Discord bot. The BotDB
class has a single method, init_db_and_tables
, which is used to initialize the database and create any necessary tables.
The __init__
method of the BotDB
class is responsible for setting up the database connection. It does this by loading environment variables from a .env
file using the load_dotenv
function and using the DATABASE_VOLUME
environment variable to determine the location of the database file.
By default, I use
/data
for this variable. Don’t forget to update your.env
file with the local folder you want to use to store the database. Note this folder will be binded as a docker volume later.
It then creates an asyncio-based SQLAlchemy engine using the create_async_engine
function and stores it as an instance variable.
The init_db_and_tables
method is an async method that creates an async context using the async with …
syntax. Within this context, it uses the begin method of the engine to create a transaction and then calls the create_all
method of the SQLModel.metadata
object to create any necessary tables. This method is typically called when the bot is starting up to ensure that the database and tables are set up correctly.
SQLModel
I chose to use SQLModel, which is a wrapper around SQLAlchemy. This powerful tool allows you to code SQL directly in your Python code, making it easier to work with databases in your Python applications. It also perfectly handles asynchronous operations thanks to SQLAlchemy async engine.
With SQLModel, you can use Python classes and objects to represent your database tables and rows, allowing you to write SQL queries more naturally and intuitively. For example, consider the following SQLModel code that defines a Guilds
table:
Discord servers are named guilds internally when you use the Discord API. In this code, we define a Guilds
class that represents our guilds table. Each table column is represented as a class attribute, with additional metadata such as default and primary_key
provided using Field
objects.
You can see all the other tables in the database/classes.py
file.
CRUD
CRUD stands for Create, Read, Update, and Delete. It refers to the four basic functions typically used to manipulate data in a database.
In larger projects, these functions may be separated and explicitly identified as CRUD functions, but for this project, I have chosen to add them directly to the BotDB
class. This is a common approach for smaller projects where the focus is on simplicity and ease of maintenance.
For example, here is the add_a_guild()
function that simply adds a Discord guild to the database:
The function is used when the bot is added to a guild to reference it in the database. It takes two arguments: discord_guild_id
, an integer representing the Discord guild’s ID, and guild_owner_id
, an integer representing the owner’s ID of the guild.
The function begins by creating an asynchronous session using AsyncSession(self.engine)
. The function then uses this session to execute a SELECT statement using select(Guilds)
, which selects all columns from the Guilds
table. The .where()
clause of the SELECT statement specifies that the returned rows should only include those with a discord_guild_id
that matches the discord_guild_id
passed to the function as an argument. The result of this SELECT statement is assigned to the variable guild.
The function then enters a try block, which attempts to retrieve a single result from the guild using the one()
method. If this is successful, it means that a row with a matching discord_guild_id
was found in the Guilds table, and a message is printed indicating that the guild already exists.
If no matching row is found, a NoResultFound
exception is raised, and the code in the except block is executed. In the except block, a new Guilds object is created using the Guilds constructor, and the discord_guild_id
and guild_owner_id
arguments are passed to the function.
This new object is then added to the session using session.add(guild)
, and the session is committed using await session.commit()
. Finally, the session is refreshed with the updated guild object using await session.refresh(guild)
.
You can retrieve the other CRUD functions in the database/__init__.py
file.
Bot commands and async
Why asynchronous programming?
Asynchronous programming is a programming paradigm that allows a program to perform multiple tasks concurrently by utilizing resources efficiently. This is particularly useful for applications that need to handle many requests simultaneously, such as a Discord bot that must respond to multiple users and servers simultaneously.
In Python, asynchronous programming is typically implemented using asyncio
, a library that provides a framework for using async/await
syntax and managing asynchronous tasks. The async/await
syntax allows you to define asynchronous functions using the async def
keyword and use the await
keyword to indicate that a task is asynchronous.
One of the key benefits of asynchronous programming is that it allows a program to make use of idle time rather than being stuck waiting for a task to complete. This can lead to significant performance improvements, particularly in applications requiring many I/O bound or high-latency tasks.
In the context of a Discord bot, asynchronous programming is fundamental because it allows the bot to respond to multiple users and servers concurrently rather than being limited to handling one request at a time. This improves the user experience and makes the bot more scalable, as it can take a more significant number of requests without overloading.
The /summarize command
Let’s dive into the primary purpose of this bot: the /summarize
function that allows users to summarize any channels.
I use the wordcab-python package to interact directly with the Wordcab Summarization API in any Python script.
This async function is a command for a Discord bot that allows users to launch a Wordcab summarization job for a specific channel in a server. The function takes several arguments, including an interaction
object representing the Discord interaction, a size
for the summary, a timeframe
for the messages to summarize, and optional arguments for whether to list the summarized messages (list_summarized_chat
) in the response and the language
of the source text.
The list_summarized_chat
is a simple Python bool
that determines if the user wants to retrieve a list of all the messages used to generate the summary.
The function performs some basic validation on the size
and language
arguments and then checks whether the guild associated with the interaction is authenticated. If the guild is not authenticated, the function sends a message to the user indicating that they need to run the /wordcab-login
command first.
The function retrieves the associated guild’s token and calculates a date using the provided timeframe if the guild is authenticated. It then iterates through the messages in the channel’s history, filtering and appending those that meet specific criteria to a list of messages.
It launches a summary job using the start_summary
function, passing in the source_object
, the target size
of the summary, the language
of the source text, and several other arguments related to the Wordcab API. If the number of characters in the list of messages is less than 1000, the function indicates that there needs to be more to summarize. Otherwise, it creates an in-memory source_object
.
If the total number of characters in the list of messages is greater than 4000, the function sends a message indicating that the chats used for the summary have been truncated to 4000 characters.
Finally, the function logs some information about the summary job and sends a message to the user indicating that the summarization job has been launched.
As evidenced by the past function, there are a lot of operations that need to be done asynchronously to avoid overloading the server with the bot. This becomes even clearer with the introduction of the function that sends summaries to users’ direct messages:
The function takes several arguments, including a Discord guild
object representing the guild the user is in, a Discord user
object representing the user to send the summary to, a summary_size
, a timeframe
, a language
, a job_name
, and a guild token
. There is also an optional argument for sending the summarized_messages
if the user requests it.
The function enters a loop that repeatedly retrieves the status of the summary job using the retrieve_job
function and the provided job_name
. If the job’s status is “SummaryComplete”, the loop breaks. If the job’s status is “Deleted” or “Error”, the function sends a message to the user indicating that the job has failed and asks them to try again. If the job’s status is neither of these, the loop waits for three seconds before rechecking the status.
Once the job’s status is “SummaryComplete”, the function retrieves the summary using the retrieve_summary
function and the provided summary_id
.
It then sends the summary to the user via DM, breaking it up into chunks if necessary. If the user requests the summarized chat, it is also sent to the user in chunks.
Finally, the function logs some metrics about the summary and the user’s request, and then deletes the job and the user’s data using the delete_job_after_summary
function.
A final thought on asynchronous programming
Asynchronous programming is highly beneficial to the past two functions because, without it, all users would have to wait indefinitely for a trace of the summary to appear in their DMs.
This is because the functions rely on long-running tasks, such as retrieving the status of a summary job and retrieving the summary itself, which can take an unpredictable amount of time to complete.
By using async/await
and asyncio
to manage these tasks asynchronously, the functions avoid blocking the program’s execution and can respond to requests from other users while the tasks are being completed.
Overall, asynchronous programming is a key tool for developing high-performance, scalable applications and is an essential consideration for any Discord bot that needs to handle a large number of requests simultaneously.
Containerize with Docker
What is Docker?
Docker is a containerization platform that allows developers to package applications and their dependencies into lightweight, standalone containers that can be easily deployed and run on any host. Containerization helps isolate applications from their surroundings and ensure they are portable and consistent across different environments.
I selected Docker as the deployment option for the Discord TL;DR bot because of its well-established reputation for stability and its ability to allow deployment across multiple platforms.
If you’ve never used Docker before, please check their documentation for a quick overview.
The Dockerfile
The Dockerfile outlines the steps to build a container image for the Discord bot using Python. We are using a multi-stage approach to reduce the rebuild time and to reduce the size of the runtime image and container.
The Dockerfile first defines a base image with a specific version of Python installed and sets some environment variables.
It then defines another image based on the first one, installing some build tools and the poetry package manager. It then copies the poetry lock and project files into the image and runs poetry install to install the project dependencies.
Finally, it creates a runtime image from the base image and copies the virtual environment, application code, and environment variables. It also copies an ENTRYPOINT
shell script and sets it as the ENTRYPOINT
for the container, with the bot’s __main__
module specified as the default command to run when the container is started.
The entrypoint shell script
Here is the shell script we use as the ENTRYPOINT
:
The script first activates the virtual environment located at /opt/pysetup/.venv/bin/activate
. This ensures that the Python dependencies installed in the virtual environment are available for the main command to use.
The script then executes the command passed as an argument to the script using exec “$@”
. This allows the main command to be given to the container at runtime rather than being hardcoded into the entrypoint script.
Using this script as the ENTRYPOINT
for the container allows the container to be customized and configured at runtime. The main command can be changed or updated without rebuilding the entire container image.
With larger applications, time spent building images with Docker could be much longer, so any gained seconds are precious!
Run the bot with Docker
These are the two commands we use to build and run the Discord TL;DR bot:
# Build the image
$ docker build --tag discord-bot --file docker/Dockerfile .
# Run the container
$ docker run -d --name discord-bot -v /data:/app/data discord-bot:latest
The first command builds a Docker image using the Dockerfile
located in the /docker
directory. The --tag
flag specifies the name and optionally the image’s tag in the name:tag
format. The --file
flag specifies the path to the Dockerfile
to use for the build. The .
at the end of the command indicates the current directory, which is used as the build context for the image. The build context is the directory that is used to resolve relative paths in the Dockerfile
, so it’s important to launch this command in the leading project directory.
The second command runs a Docker container based on the image built in the first command. The -d
flag runs the container in detached mode, allowing it to run in the background. The --name
flag assigns a name to the container, which can be used to identify and manage it later. The -v
flag mounts a volume at the specified path in the container. In this case, the /data
directory on the host is mounted at /app/data
in the container. This allows data to be shared between the host and the container or to be persistent even if the container is stopped or removed. The discord-bot:latest
at the end of the command specifies the image to use for the container, and the latest
tag refers to the latest version of the image.
Deploy the bot to AWS EC2
Deploying the Discord bot on an Amazon Web Services (AWS) Elastic Compute Cloud (EC2) instance has several advantages. One major advantage is that an EC2 instance can be set up to run the bot continuously, allowing it to run indefinitely without requiring manual intervention. This is especially useful for a Discord bot, as it needs to be available to receive and respond to messages at all times.
Another advantage of deploying the bot on an EC2 instance is that it can take advantage of the scalability and reliability of AWS. If the bot experiences a surge in usage, the EC2 instance can be scaled up to handle the increased load. Additionally, EC2 instances are designed to be highly available, with various features and services available to ensure uptime and availability.
Using a free tier EC2 instance can also be a cost-effective way to run the Discord bot, especially at the beginning when usage is likely to be low. The free tier includes a certain amount of resources that can be used at no cost for the first year, and these resources are often sufficient for a small Discord bot. After the first year, or if the bot’s usage exceeds the free tier resources, it can be run on a paid EC2 instance with resources tailored to its needs.
We will assume that you know how to create an EC2 instance on AWS because there are many tutorials and guides online.
Here is one!
Automate the code push to the EC2 instance
We used GitHub Actions to automatically deploy the last version of the code to our EC2 instance. You can find the file in .github/workflows/deploy-to-ec2.yml
file.
The automation is triggered by a push
event to the main
branch of the repository.
The action consists of a single job with two steps: The first step checks out the code from the repository, while the second step uses the ssh-deploy
action provided by easingthemes to deploy the code to the EC2 instance.
The ssh-deploy
action requires several environment variables to be set, including an SSH private key, the hostname and username of the EC2 instance, and the target location on the instance where the code should be deployed.
These environment variables are stored as secrets in the repository and are accessed using the {{ secrets.<VARIABLE_NAME> }}
syntax.
Overall, this YAML file provides a convenient way to automate the deployment of code changes to an EC2 instance, saving time and effort for the developer and ensuring that the bot is always running the latest version of the code.
Automation improvements
In addition to automating the deployment of code changes to an AWS EC2 instance, this process could be further automated by adding steps to build a Docker image and run a Docker container using the latest code changes.
To do this, add steps to the job in the YAML file to build the Docker image using the previous docker build
command, and then run the container using the last docker run
command.
These steps could be executed remotely on the EC2 instance using the ssh-deploy
action or a different action specifically designed to run Docker commands.
Final thoughts
In conclusion, creating a scalable Discord bot can be a challenging but rewarding task. Using technologies such as SQLAlchemy, SQLModel, and Docker can significantly improve the performance and maintainability of the bot.
Additionally, utilizing asyncio and deploying the bot on a cloud platform such as AWS EC2 can ensure that the bot can handle many users and servers without experiencing any downtime.
Bringing artificial intelligence closer to users by integrating it into Discord bots can be a game changer for companies.
By leveraging the power of AI and the widespread use of Discord, companies can provide valuable and innovative services to a large audience in a convenient and accessible way. A great example of this is MidJourney, a company that has successfully integrated a diffusion model into their bot and now has over 7 million users.
I hope you found this post helpful and that you are now impatient to start coding your first Discord bot! 🎉
Summarize any business communications at scale with Wordcab’s API. You can learn more about Wordcab services and pricing on our website.
If you want to try out the API, you can signup for a free account and start using the API right away.
Check our Python package to bring summarization to any application!