Building and Launching Your Discord Bot: A Step-by-Step Guide

Thomas Chaigneau
17 min readJan 12, 2023

--

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 .envfile 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.

Add a new repository secret on GitHub

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!

--

--