Creating a Simple Bot using Cogs

Creating a Simple Bot using Cogs

·

8 min read

In this series, I'll explain the best ways to code your Discord bot in Python. We will be creating Slash Commands. I've learned these tips from making my own bots over the years.

Before we start, make sure you've set up your bot from the Developer Console and added it to your test server. If you need help with this, you can find lots of guides online.

Don't forget to check on Slash Commands while inviting your bot to the server!

I will be explaining giving code snippets in between while explaining them, and the final code will be given at the end.

Why Slash Commands?

Discord has designated message content as a Privileged Intent for privacy and performance reasons. To access message content for your bot, you must obtain permission if it is on 100+ servers.

Prefix commands require access to message content to function. Therefore, approval for the Privileged Intent will only be granted if your bot is specifically designed to read, moderate, and manage Discord user messages. Using slash commands is a better option if this is not the case.

For more details, check out:
1. MESSAGE CONTENT IS BECOMING A NEW PRIVILEGED INTENT

2. WHY WE MOVED TO SLASH COMMANDS

Creating the application directory - MyBot

Create your application directory, /MyBot. This directory will contain your main python file - main.py. Create 3 folders - cogs, tokens, and utils.

/cogs will contain the python files with all your commands.

/tokens will contain .env files that will hold your secret tokens of discord and any other API that you use.

/utils will contain any other helper files like python or text files.

Setting up the virtual environment

If you are using VSCode, go to the Terminal

  • cd MyBot

  • python -m venv env -> create a new virtual env called ‘env’

  • .\env\Scripts\activate -> activate the virtual environment

This will create a new directory - env in MyBot.

Why are we setting up a virtual environment?

This will make sure that any module you install for your bot will not clash with the modules of other applications. And when we create a requirements.txt file, the file will contain only the dependencies that are required for your bot.

Discord python library - discord.py

discord.py is a Python library for the Discord API, allowing easy access to the API functions and handling of events. It is used to create Discord bots and interact with the Discord API to perform tasks such as sending messages, managing servers, and more.

Terminal -> pip install discord.py

Click here to check it out

Cogs & Extensions

Cogs are classes that derive from commands.

Cogs are usually placed in separate files and loaded as extensions.

The combination of cogs and extensions allows you to separate commands, events, tasks, and other things into separate files and classes for better organization. They help you to write clean, maintainable code and make it easier to add new features and functionality to your bot over time. They are highly recommended.

We will be creating 2 cogs - OwnerCommands and UserCommands, in 2 separate files - OwnerCommands.py and UserCommands.py under the directory, /cogs, and load them in main.py.

main.py

def __init__(self):

    self.initial_extensions = [
        "cogs.OwnerCommands",
        "cogs.UserCommands"
    ]   

async def setup_hook(self):

    for ext in self.initial_extensions:
        await self.load_extension(ext)

NOTE: To avoid restarting the bot every time we make any changes to the bot**,** we will write owner commands that will sync and reload the commands directly from the Discord server.

Sync

When someone uses a slash command, Discord tells your bot about it. And for this, you need to add your commands to Discord's list of commands. In order to do this, you need to register your commands on the command tree and then tell discord they exist by syncing with tree.sync.

Why should we not auto-sync?

Syncing your command tree automatically causes extra unnecessary requests to be made, which is pointless and a waste of an API request to a limit with an already tight rate limit.

When to Sync?

  • Added/removed a command

  • Changed a command's

    • name

    • description

  • Added/removed an argument

  • Changed an argument's

    • name (rename decorator)

    • description (describe decorator)

    • type (arg: str str is the type here)

  • Added/modified permissions:

    • guild_only decorator or kwarg

    • default_permissions decorator or kwarg

    • NSFW kwarg

  • Added/modified/removed locale strings

  • Converted the global/guild command to a guild/global command

When not to Sync?

  • Changed anything in the function's body (after the async def (): part)

  • Added or modified a library side check:

    • @app_commands.checks

    • @commands...(.check)

    • @app_commands.checks.(dynamic_)cooldown(...)

How to sync without restarting the bot?

OwnerCommands.py

@commands.command()
@commands.guild_only()
@commands.is_owner()
async def sync(self, ctx: commands.Context, guilds: commands.Greedy[discord.Object], spec: Optional[Literal["~", "*", "^"]] = None) -> None:
    if not guilds:
        if spec == "~":
            synced = await ctx.bot.tree.sync(guild=ctx.guild)
        elif spec == "*":
            ctx.bot.tree.copy_global_to(guild=ctx.guild)
            synced = await ctx.bot.tree.sync(guild=ctx.guild)
        elif spec == "^":
            ctx.bot.tree.clear_commands(guild=ctx.guild)
            await ctx.bot.tree.sync(guild=ctx.guild)
            synced = []
        else:
            synced = await ctx.bot.tree.sync()

        await ctx.send(
            f"Synced {len(synced)} commands {'globally' if spec is None else 'to the current guild.'}"
        )
        return

    ret = 0
    for guild in guilds:
        try:
            await ctx.bot.tree.sync(guild=guild)
        except discord.HTTPException:
            pass
        else:
            ret += 1

    await ctx.send(f"Synced the tree to {ret}/{len(guilds)}.")

With the above code, you can tag the bot in your server and sync the commands as given below

@MyBot sync -> global sync

@MyBot sync ~ -> sync current guild

@MyBot sync * -> copies all global app commands to the current guild and syncs

@MyBot sync ^ -> clears all commands from the current guild target and syncs (removes guild commands)

@MyBot sync id_1 id_2 -> syncs guilds with id 1 and 2

Reloading Extensions

You need to reload extensions whenever you change anything in the code and want to see the new changes in your bot, i.e., whenever you make any changes to the body of your command.

How to load/unload/reload the extensions without restarting the bot?

OwnerCommands.py

    #Command which Loads a Module. Remember to use dot path. e.g: cogs.owner
    @commands.command(name='load')
    @commands.is_owner()
    async def load_cog(self, ctx: commands.Context, *, cog: str):
        try:
            await self.bot.load_extension(cog)
        except Exception as e:
            await ctx.send(f'**`ERROR:`** {type(e).__name__} - {e}')
        else:
            await ctx.send('**`SUCCESS`**')

    #Command which Unloads a Module. Remember to use dot path. e.g: cogs.owner
    @commands.command(name='unload')
    @commands.is_owner()
    async def unload_cog(self, ctx: commands.Context, *, cog: str):
        try:
            await self.bot.unload_extension(cog)
        except Exception as e:
            await ctx.send(f'**`ERROR:`** {type(e).__name__} - {e}')
        else:
            await ctx.send('**`SUCCESS`**')

    #Command which Reloads a Module. Remember to use dot path. e.g: cogs.owner
    @commands.command(name='reload')
    @commands.is_owner()
    async def reload_cog(self, ctx: commands.Context, *, cog: str):
        try:
            await self.bot.unload_extension(cog)
            await self.bot.load_extension(cog)
        except Exception as e:
            await ctx.send(f'**`ERROR:`** {type(e).__name__} - {e}')
        else:
            await ctx.send('**`SUCCESS`**')

With the above code, you can tag the bot in your server and reload the UserCommands extension as @MyBot reload cogs.UserCommands

Creating Simple Slash Commands

say_hi command

UserCommands.py

# say_hi command
@app_commands.command(name='say_hi', description="Says Hi to you")
async def say_hi(self, interaction: discord.Interaction, name: str):
    reply = f"Hi {name}!"
    await interaction.response.send_message(reply)

In say_hi command, name is the input we get from the user.

add_numbers command

UserCommands.py

# add_numbers command
@app_commands.command(name='add_numbers', description="Adds 2 numbers")
async def add_numbers(self, interaction: discord.Interaction, num1: int, num2: int):
    reply = num1+num2
    await interaction.response.send_message(reply)

In add_numbers command, num1 and num2 are the inputs we get from the user.

Final Code

Terminal -> pip install python-dotenv

main.py

import os
import discord
from discord.ext import commands
#import logging

# Get discord token from the file
from dotenv import load_dotenv
load_dotenv('tokens/discord_token.env')
DISCORD_TOKEN = os.getenv("DISCORD_TOKEN")

#handler = logging.FileHandler(filename='discord.log', encoding='utf-8', mode='w')

class MyBot(commands.Bot):

    def __init__(self):

        intents = discord.Intents().default()

        super().__init__(
            command_prefix=commands.when_mentioned,
            intents=intents,
            status=discord.Status.online, 
            activity=discord.Game("/help"),
        )

        self.initial_extensions = [
            "cogs.OwnerCommands",
            "cogs.UserCommands"
        ]   

    async def setup_hook(self):

        for ext in self.initial_extensions:
            await self.load_extension(ext)

    async def close(self):
        await super().close()
        await self.session.close()


    async def on_ready(self):
        print(f'{self.user} has connected to Discord')

bot = MyBot()
bot.run(DISCORD_TOKEN)
#bot.run(DISCORD_TOKEN, log_handler=handler, log_level=logging.DEBUG)

Here, command_prefix=commands.when_mentioned is given so that we can tag the bot for sync and reloading extensions.

UserCommands.py

import discord
from discord import app_commands
from discord.ext import commands

class UserCommands(commands.Cog):

    def __init__(self, bot):
        self.bot = bot

    # say_hi command
    @app_commands.command(name='say_hi', description="Says Hi to you")
    async def say_hi(self, interaction: discord.Interaction, name: str):
        reply = f"Hi {name}!"
        await interaction.response.send_message(reply)

    # add_numbers command
    @app_commands.command(name='add_numbers', description="Adds 2 numbers")
    async def add_numbers(self, interaction: discord.Interaction, num1: int, num2: int):
        reply = num1+num2
        await interaction.response.send_message(reply)

async def setup(bot: commands.Bot) -> None:
    await bot.add_cog(UserCommands(bot))

We are making all the User Commands global

OwnerCommands.py

import discord
from discord import app_commands
from discord.ext import commands
from typing import Optional, Literal


class OwnerCommands(commands.Cog):

    def __init__(self, bot):
        self.bot = bot

    @commands.command()
    @commands.guild_only()
    @commands.is_owner()
    async def sync(self, ctx: commands.Context, guilds: commands.Greedy[discord.Object], spec: Optional[Literal["~", "*", "^"]] = None) -> None:
        if not guilds:
            if spec == "~":
                synced = await ctx.bot.tree.sync(guild=ctx.guild)
            elif spec == "*":
                ctx.bot.tree.copy_global_to(guild=ctx.guild)
                synced = await ctx.bot.tree.sync(guild=ctx.guild)
            elif spec == "^":
                ctx.bot.tree.clear_commands(guild=ctx.guild)
                await ctx.bot.tree.sync(guild=ctx.guild)
                synced = []
            else:
                synced = await ctx.bot.tree.sync()

            await ctx.send(
                f"Synced {len(synced)} commands {'globally' if spec is None else 'to the current guild.'}"
            )
            return

        ret = 0
        for guild in guilds:
            try:
                await ctx.bot.tree.sync(guild=guild)
            except discord.HTTPException:
                pass
            else:
                ret += 1

        await ctx.send(f"Synced the tree to {ret}/{len(guilds)}.")

    #Command which Loads a Module. Remember to use dot path. e.g: cogs.owner
    @commands.command(name='load')
    @commands.is_owner()
    async def load_cog(self, ctx: commands.Context, *, cog: str):
        try:
            await self.bot.load_extension(cog)
        except Exception as e:
            await ctx.send(f'**`ERROR:`** {type(e).__name__} - {e}')
        else:
            await ctx.send('**`SUCCESS`**')

    #Command which Unloads a Module. Remember to use dot path. e.g: cogs.owner
    @commands.command(name='unload')
    @commands.is_owner()
    async def unload_cog(self, ctx: commands.Context, *, cog: str):
        try:
            await self.bot.unload_extension(cog)
        except Exception as e:
            await ctx.send(f'**`ERROR:`** {type(e).__name__} - {e}')
        else:
            await ctx.send('**`SUCCESS`**')

    #Command which Reloads a Module. Remember to use dot path. e.g: cogs.owner
    @commands.command(name='reload')
    @commands.is_owner()
    async def reload_cog(self, ctx: commands.Context, *, cog: str):
        try:
            await self.bot.unload_extension(cog)
            await self.bot.load_extension(cog)
        except Exception as e:
            await ctx.send(f'**`ERROR:`** {type(e).__name__} - {e}')
        else:
            await ctx.send('**`SUCCESS`**')


# sync the OwnerCommands only in the BotTest Server
async def setup(bot: commands.Bot) -> None:
    await bot.add_cog(
        OwnerCommands(bot),
        guild = discord.Object(id=111111111111111111)) #give your Server ID here

We are making all the Owner Commands guild-only, i.e., these commands will be available only in a particular guild (give your own Server ID)

discord_token.env

DISCORD_TOKEN=ABCD1234

Give your Discord bot's Secret Token here

I hope you found this blog helpful!

I will be writing more blogs on how to add more features to your Discord Bot.

Happy Coding!! :D