Table of Contents

02. [60p] Discord bot

Let's move on to the good part. In this exercise, you will write your very own discord bot. In case you've been living under a rock, discord is pretty much like teamspeak from back in the day, but you can create ad-hoc servers for you and your friends. These servers have both text and voice channels and can be joined via a referral link.

Each account (provisional or fully registered) is allocated a token. This token is a string of characters that uniquely identifies you to the discord servers. If you use this token with pycord (see the Client class), you will be able to automate a human account. This is fine, if you want to write a terminal discord client for yourself. However, as a developer, you are also able to create bot applications under your account. These bots belong to you, but are issued separate tokens.

While human accounts are added to a server by receiving an invite link, bot owners give out join links to server admins. Once the admin approves the join request for your bot (reviewing a list of permissions that the bot needs to function), the bot will appear as a normal user in that respective server. When you run your Python script and authenticate yourselves with the bot's token, the bot will come online. When you stop your script and the connection is lost, the bot goes offline in discord. Simple, right?

To get you started, we prepared a code skeleton for you:

Click to display ⇲

Click to hide ⇱

bot-skel.py
#!./.venv/bin/python
 
import discord      # base discord module
import code         # code.interact
import os           # environment variables
import inspect      # call stack inspection
import random       # dumb random number generator
 
from discord.ext import commands    # Bot class and utils
 
################################################################################
############################### HELPER FUNCTIONS ###############################
################################################################################
 
# log_msg - fancy print
#   @msg   : string to print
#   @level : log level from {'debug', 'info', 'warning', 'error'}
def log_msg(msg: str, level: str):
    # user selectable display config (prompt symbol, color)
    dsp_sel = {
        'debug'   : ('\033[34m', '-'),
        'info'    : ('\033[32m', '*'),
        'warning' : ('\033[33m', '?'),
        'error'   : ('\033[31m', '!'),
    }
 
    # internal ansi codes
    _extra_ansi = {
        'critical' : '\033[35m',
        'bold'     : '\033[1m',
        'unbold'   : '\033[2m',
        'clear'    : '\033[0m',
    }
 
    # get information about call site
    caller = inspect.stack()[1]
 
    # input sanity check
    if level not in dsp_sel:
        print('%s%s[@] %s:%d %sBad log level: "%s"%s' % \
            (_extra_ansi['critical'], _extra_ansi['bold'],
             caller.function, caller.lineno,
             _extra_ansi['unbold'], level, _extra_ansi['clear']))
        return
 
    # print the damn message already
    print('%s%s[%s] %s:%d %s%s%s' % \
        (_extra_ansi['bold'], *dsp_sel[level],
         caller.function, caller.lineno,
         _extra_ansi['unbold'], msg, _extra_ansi['clear']))
 
################################################################################
############################## BOT IMPLEMENTATION ##############################
################################################################################
 
# bot instantiation
intents = discord.Intents.all()
bot = commands.Bot(command_prefix='!', intents=intents)
 
# on_ready - called after connection to server is established
@bot.event
async def on_ready():
    log_msg('logged on as <%s>' % bot.user, 'info')
 
# on_message - called when a new message is posted to the server
#   @msg : discord.message.Message
@bot.event
async def on_message(msg):
    # filter out our own messages
    if msg.author == bot.user:
        return
 
    log_msg('message from <%s>: "%s"' % (msg.author, msg.content), 'debug')
 
    # overriding the default on_message handler blocks commands from executing
    # manually call the bot's command processor on given message
    await bot.process_commands(msg)
 
# roll - rng chat command
#   @ctx     : command invocation context
#   @max_val : upper bound for number generation (must be at least 1)
@bot.command(brief='Generate random number between 1 and <arg>')
async def roll(ctx, max_val: int):
    # argument sanity check
    if max_val < 1:
        raise Exception('argument <max_val> must be at least 1')
 
    await ctx.send(random.randint(1, max_val))
 
# roll_error - error handler for the <roll> command
#   @ctx     : command that crashed invocation context
#   @error   : ...
@roll.error
async def roll_error(ctx, error):
    await ctx.send(str(error))
 
################################################################################
############################# PROGRAM ENTRY POINT ##############################
################################################################################
 
if __name__ == '__main__':
    # check that token exists in environment
    if 'BOT_TOKEN' not in os.environ:
        log_msg('save your token in the BOT_TOKEN env variable!', 'error')
        exit(-1)
 
    # launch bot (blocking operation)
    bot.run(os.environ['BOT_TOKEN'])

[5p] Task A - Test server and bot application

Before we dive into the script above, we need to set up a discord account (no need to register), create a server for testing purposes, and create a bot account.

Normally, for bot permissions, you would want to be as restrictive as possible. People will be reticent to give admin privileges to a random bot. In this case, we want our bot able to view channels and logged users, view their text messages and respond in kind, and also to connect to the voice channel and maybe play some music.

Finish the setup as per the links above and save the bot's token. You'll need it in the following task. If you encounter any problem, ask a friend. If you don't have any, the lab assistant will have to suffice.

[5p] Task B - Code skeleton

Let's take a closer look at the code skeleton and see what each thing does:

This must have been a lot to take in. So take some time and ask any question that you may have. If you've done everything correctly so far, once you run the script you should see your bot coming online in your test discord server. Try interacting with it!

[10p] Task C - Debugging

Notice that our bot is event driven, so debugging it would be kind of annoying. For this, you can use a little hack:

code.interact(local=dict(globals(), **locals())) 

When executed, this line of code will pause the script and open a python shell for you to investigate the state of both the local and global variables. The syntax might seem a bit weird. The interact() function takes a dictionary as the local argument. The local argument is used to initialize the variables available in the newly opened shell. Because we want to see both local and global variables, we need to combine them in a single dictionary. dict is the dictionary type. When using it to create a new dictionary (in stead of the basic { } syntax), we can specify another dictionary for initialization: dict(globals()). But we want to combine two dictionaries. However, we can't just add them with the + operator since it doesn't work that way. Also, we don't want to change any of the dictionaries returned by globals() and locals() since it might get us into trouble with the interpreter later. But wait! dict can also be initialized from optional keyword arguments, represented by key-value pairs. So **locals() can be used to break down the second dictionary in optional keyword arguments for the resulting dictionary's constructor!

Here's an easier example where x is basically globals() and c=3, d=4 is **locals().

In [1]: x = { 'a' : 1, 'b' : 2 }
 
In [2]: y = dict(x, c=3, d=4)
 
In [3]: y
Out[3]: {'a': 1, 'b': 2, 'c': 3, 'd': 4}

For now, use code.interact() in the on_message() function. Inspect the bot and msg variables.

[40p] Task D - Adding features

Up until now, everything was pretty tutorial-ish. From here on out, it's time for you to get your hands dirty and add some new features to the bot! This means looking stuff on the Internet. For yourselves. Scary stuff… I know!
Hint: a good starting point would be the API Reference page.

Here is what you have to add:

  1. A play command that takes a song name (e.g.: danger_zone.mp3) as argument. Invoking this command while present in a voice channel should cause the bot to connect to that channel and play the song. The song should be loaded from a local file (hint: use any music file that you have on hand, or a YouTube downloader).
  2. A list command that lists all available songs in the discord chat.
  3. A scram command that tells the bot to disconnect from the current voice channel immediately.
  4. An event handler for on_voice_state_update that checks if the bot was left alone in the channel after a user left. If the bot is indeed alone, it should also disconnect.

Hint: check this out for an example: https://brucecodes.gitbook.io/pycord/guide/voice-commands