Skip to content

Instantly share code, notes, and snippets.

@4Kaylum
Last active November 10, 2024 02:20
Show Gist options
  • Save 4Kaylum/a1e9f31c31b17386c36f017d3c59cdcc to your computer and use it in GitHub Desktop.
Save 4Kaylum/a1e9f31c31b17386c36f017d3c59cdcc to your computer and use it in GitHub Desktop.
A simple bot tutorial for Discord.py

Writing a Discord bot with Discord.py

Hey one, hey all, and welcome to a basic Discord bot writing tutorial. This is a basic tutorial that doesn't cover coding in general, but rather how to work with the Discord.py (v1.0.0a) library and how to write a simple bot with it. General help can be found on the Discord API guild and in the documentation.

This tutorial assumes some prior knowledge of programming, and assumes you already have Python3.5 (and Pip) or later installed and added to your PATH.

With that, let's get to it.

Installation

As with most Python libraries, Discord.py can be installed with Pip. However, since we're using the rewrite (alpha) version, you also need to have Git SCM installed, so be sure to do that. After you have that installed (and, optionally, you're in whatever venv you want to use) just run the command

pip install discord.py

This will install Discord.py.

Bot Token

After we have that installed, we need to grab a bot token from Discord. A "token" is a string that you send to a server to authenticate with.

Go to this url and create a new application. The name you supply will be the name of your bot, but you can change that later. After you've made an application, you should be redirected to a page with all of the app's details on it. Scroll to the bottom of this page and press "create a bot user". You will then have a "click to reveal" link that shows you your bot's token. Make sure you keep track of this - we'll need that in a minute.

Before you close that page, be sure to review whether you want your bot to be public or not. If the box is ticked, then anyone with the invite link will be able to add your bot to their guild, but if left unticked, only you can add the bot to any given guild.

Adding your Bot

It's fairly important that you add your bot to the guild you want to test it on, and that's simple enough to do. On your application page, you should be able to see "client ID" near the top. Copy that value, and paste it into this website (officially recommended by Discord). This allows you to tick the permissions you want your invite link to have, and then spits it out to you nicely at the bottom for you to share with others, or use so you can add it to your own servers. Having done that, let's get the actual programming underway~

Hello World

Finally, we can get to some coding.

Start off by making a new file. It doesn't matter what the name of it is, provided it's not "discord", since we'll be importing that in a second. From there, you can start to write your bot.

First off, we need to import the things that interface with Discord. There are two (technically four, but we're ignoring the autosharded versions) included clients with Discord.py. There's Client, and there's Bot. There's not too much of a difference between the two, but Bot has the ability to interface with the commands extention of Discord.py, which is super useful, since we'll be writing... well... commands. So let's import that.

from discord.ext import commands
prefix = "?"
bot = commands.Bot(command_prefix=prefix)
bot.run(TOKEN)  # Where 'TOKEN' is your bot token

As you can see from the above snippet, we've created a new bot with the command prefix of ?. The command_prefix argument can take one of three things: a list, a string, or a function (which takes bot and then message, and needs to return a list or string). The simplest way to use this is with a string, so that's what we'll cover now.

This snippet, though short, is a basic bot made in Python, and when run will bring your bot online. It won't do anything (because you've not told it to yet), but it is a valid bot

Events

The basis of Discord.py is to have functions that are triggered by events. These events are all documented here, but I'll cover a couple of them now.

on_ready

The first one of note is on_ready. This is event is triggered when the bot is online and ready to communicate with Discord's servers. It's usually used as a simple "everything's ready, boss" kind of statement, so you know what's happening. This can be included like so:

@bot.event
async def on_ready():
    print("Everything's all ready to go~")

There's a few things of note here. First of all, the @bot.event line. This is how all events are notated in Discord.py. Commands have a different decorator, but we'll get to that. Secondly, you'll see that you have async def instead of the normal def that you may have used before. That's because Discord.py is asynchronous, which means that multiple functions can be running at the same time, without interrupting one another. You may have come across the term "threaded" before, and this is essentially like that - each function would be running in a different thread when fired.

on_message

The next event that you'll probably use is on_message. Unlike on_ready, this takes a parameter of a Message object, and will include the details of whatever message fired the event.

@bot.event
async def on_message(message):
    print("The message's content was", message.content)

It's here that some people will start to see how to make commands. What you would do is compare the message's content with another string (the command name) and then perform actions based on that. Many other libraries are laid out like that, but Discord.py includes a syntax for writing commands in a much nicer way.

commands

It's now that we'll start writing commands. There will be two included in this tutorial: a "ping" command, and an "echo" command.

Importantly, if you're going to keep your on_message event, you must include await bot.process_commands(message) at the bottom - this tells D.py that after you've done your message event stuff that the message should be processed as a command.

ping

The "ping" command on some bots will just spit the word "pong" back out at you, but we're going to write it so it gives you the latency of the bot instead. Pay attention to the difference between how commands are written and how events are written.

@bot.command()
async def ping(ctx):
    '''
    This text will be shown in the help command
    '''

    # Get the latency of the bot
    latency = bot.latency  # Included in the Discord.py library
    # Send it to the user
    await ctx.send(latency)

That may look like a lot, but don't worry - I'll explain it all now.

First of all, we have @bot.command(). That's how all of your commands should start. If it's not got a decorator like that, then it won't be read as a command by the library.

Secondly, you have async def ping. In this, you have ping being automatically assigned as the name of the command, so you don't need to specify it otherwise.

After that, you have (ctx):, which none of the events had. The ctx argument is automatically passed to any command you write as the first argument, and includes all the information about the message/command. It includes the original message (ctx.message), and by extention the guild, channel, and author of it (ctx.message.guild, ctx.message.channel, ctx.message.author). These can be useful if you want your response to include the guild's name, or mention the author, or anything like that.

After that, we have a docstring. The docstring will be read by Discord.py, and automatically included in the help command (one is automatically generated for you). If you include multiple lines, then only the first will be included unless you explicitly run help name instead of just help.

The next line is pretty straightforward - latency = bot.latency is something that's included in the client; a float of how many seconds of latency the bot has.

The next line is not so straightforward. await ctx.send(latency). First of all, you have the await statement. This means that the async flow of the function needs to stop and spend some time doing whatever it is you said. And what you said is ctx.send(latency). The latency part is simple enough, it's the text that's being sent to the user. ctx.send is a method that allows you to directly reply to whatever message was sent. Many things have a .send attribute, including channels and users, but just using ctx.send allows you to just reply in the same channel as the command was invoked in.

Looking at it, it's pretty simple, right?

echo

The echo command is slightly more complicated, since it needs to read what the user said. Regardless, there's only a couple of differences in the command than before.

@bot.command()
async def echo(ctx, *, content:str):
    await ctx.send(content)

If you compare this to the other statement, then you'll immediately be able to see the difference is in the arguments for the command - (ctx, *, content:str) as opposed to (ctx). The difference is simply because we're getting more information from the user than before. In the first example, the user simply needed to invoke the command, and that was it. In this example, the user not only needs to invoke the command, but give the bot something to echo back. The * argument means that all remaining values are captured into the next variable, and the content:str is simply the value and type that it should be captures into/as.

For example: if I ran ?echo a b c, then the value of content would be "a b c". If I ran ?echo test, the value of content would be "test".

Conversely, for async def example(ctx, first:str, second:str, *, third:str), running ?example a b c d would fill first with "a", second with "b", and third with "c d". Hopefully that should help you to see what significance the * argument has.

Full Bot and its Layout

Having written these commands, you might be wondering how you include them in your main program. The answer is simple: it needs to be after you define the bot variable, and before you turn the bot on (with bot.run). As such, a full program with the examples before could be seen as such:

from discord.ext import commands
prefix = "?"
bot = commands.Bot(command_prefix=prefix)


@bot.event
async def on_ready():
    print("Everything's all ready to go~")


@bot.event
async def on_message(message):
    print("The message's content was", message.content)
    await bot.process_commands(message)


@bot.command()
async def ping(ctx):
    '''
    This text will be shown in the help command
    '''

    # Get the latency of the bot
    latency = bot.latency  # Included in the Discord.py library
    # Send it to the user
    await ctx.send(latency)


@bot.command()
async def echo(ctx, *, content:str):
    await ctx.send(content)


bot.run(TOKEN)  # Where 'TOKEN' is your bot token

When saved and run, this will act as a functional bot for you!

Conclusion

Well, there you have it. A simple introduction to writing bots using Python and Discord.py. This is expandable in whole, so you can keep adding commands to that file for as long as you want. However, what you have here is definitely functional as a Discord bot. I'm happy to have helped you.

@laughingclouds
Copy link

It cleared a lot of things, thanks for the in-depth explanation!

@pizzapizzaa
Copy link

OMG this really helps to kick off my chicken python3 skills! Love your tutorial!

@jonzmuda
Copy link

I get a syntax error at my token, I'm fairly new to coding and I can't find anything online. Can someone help me?

You have to put your token in-between " ' ". I know that's hard to understand so let me show an example.

bot.run('12345678')

Its important that you are placing your token in-between apostrophes not quotation marks.

@jonzmuda
Copy link

How do I add a category to my ?help command.

No Category:
  echo 
  help Shows this message
  ping This will execute a ping command

Type ?help command for more info on a command.
You can also type ?help category for more info on a category.

@beackers
Copy link

beackers commented Apr 5, 2022

@jonzmuda
You can add a category to your help command by creating something discord.py calls a "Cog".
Here's what you can do:

class MyCog(commands.Cog):
    def __init__(self, bot):
        self.bot = bot
    
    @commands.command()  #Note that you cannot use "bot.command()" as this will return an error.
    async def myCommand(self, ctx):     # All commands must have "self" as the first argument, then "ctx" afterward.
        await ctx.send("This is a cool command!")

Make sure at the end, you add this:

bot.add_cog(MyCog(bot))

then run your bot.

@av1d
Copy link

av1d commented Aug 4, 2023

Here's what I did to get this running. Changes in comments.

import discord  # added this
from discord.ext import commands

prefix = "?"
intents = discord.Intents.all()  # added this
bot = commands.Bot(command_prefix=prefix, intents=intents)  # modified this


@bot.event
async def on_ready():
    print("Everything's all ready to go~")


@bot.event
async def on_message(message):
    print("The message's content was", message.content)
    await bot.process_commands(message)


@bot.command()
async def ping(ctx):
    '''
    This text will be shown in the help command
    '''

    # Get the latency of the bot
    latency = bot.latency  # Included in the Discord.py library
    # Send it to the user
    await ctx.send(latency)


@bot.command()
async def echo(ctx, *, content:str):
    await ctx.send(content)

bot.run('your-token-here')

@lmaddox
Copy link

lmaddox commented Aug 2, 2024

Manually calling process_commands() from my Cog's on_message handler resulted in duplicated command-handling. I guess it's only necessary for Bots ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment