Differences

This shows you the differences between two versions of the page.

Link to this comparison view

ii:labs:04:tasks:02 [2021/12/06 14:29]
radu.mantu
ii:labs:04:tasks:02 [2024/11/29 09:16] (current)
alexandru.bala [02. [40p] Discord bot setup]
Line 1: Line 1:
-==== 02. [??pChoosing a license ​====+==== 02. [40pDiscord bot setup ====
  
-If you ever decide ​to publish code that you'​ve ​written, note that it will automatically fall under the protection of copyright law. This means that distributing copies of your codeor using it as a basis for something that may be construed as derivative work is prohibitedAs result, people will generally stay clear of your project since they don't know what your intentions are.+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 daybut you can create ad-hoc servers ​for you and your friendsThese servers have both text and voice channels and can be joined via referral link.
  
-In the 1980s, ​[[https://www.youtube.com/watch?​v=jUibaPTXSHk|Richard Stallman]] pioneered concepts known as __free software__ and __copyleft__Free software ​is software distributed with guarantee ​that the end user can modify ​and adapt it for whatever purposeprofit included. In order for this to happen, the user must have ultimate control over the software ​in questionwhich implies access to the source ​code. So, for a piece of software ​to become ​//"free software"// it must include a public license such as the [[https://www.gnu.org/licenses/gpl-3.0.html|GNU General Public License]][[https://opensource.org/licenses/MIT|MIT license]], etcThese licenses waive part of the author'​s ​rights ​and and grants them to the recipient ​of the softwareAlmost all free-software licenses contain ​copyleft provision. This provision states ​that when modified versions of the free software are distributed, it must provide ​the same guarantees ​as the originalunder the same license ​(or more permissive one).+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 [[https://docs.pycord.dev/en/​stable/​api/​clients.html|Client class]]), you will be able to automate a __human__ accountThis is fine, if you want to write 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? 
 + 
 +=== [10p] Task A - Test server and bot application === 
 + 
 +Before we dive into the script above, we need to set up a [[https://​discord.com/​|discord]] account (no need to register), [[https://​support.discord.com/​hc/​en-us/​articles/​204849977-How-do-I-create-a-server-|create a server]] ​for testing purposesand [[https://​docs.pycord.dev/​en/​master/​discord.html|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. 
 + 
 +=== [10p] Task B - Code skeleton === 
 + 
 +We've prepared a code skeleton ​for this part: 
 + 
 +<​spoiler>​ 
 +<file python 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'​]) 
 +</​file>​ 
 +</​spoiler>​ 
 + 
 +Let's take a closer look at the code skeleton and see what each thing does: 
 +  * ''​from discord.ext import commands'':​ as you guessedthis works pretty much like the regular ''​import module''​. The difference here is that we don't import it as a whole, but bits and pieces such as submodules, functions, etc. Additionally,​ we don't have to specify the whole module chain (i.e.: ''​discord.ext.commands''​) every time we use it. Instead, we can just say ''​commands''​ and the interpreter will know what we're referring to. 
 +  * ''​log_msg()'':​ this is a helper function that colors your message according to its urgency and also displays the line where it was invoked. Use it if you want, but it's not mandatory. Note however the arguments: ''​msg:​ str, level: str''​. Since //Python 3.5// you can use [[https://​docs.python.org/​3/​library/​typing.html|typing hints]] ​for variables and function return values. When possible, use these hints for clarity if nothing else. 
 +  * ''​@bot.event'':​ this is called ​decorator. Most likely, you've never seen this before and we can't blame you. Things like this belong on [[https://​www.reddit.com/​r/​iamverysmart/​|r/​iamverysmart]] and not in programming language specifications. A decorator is basically a function processing function. As the name implies, by specifying a decorator before the declaration ​of that function, the declared function will be passed ​to the decorator function for "some processing"​. In this case, our **on_ready()** function is passed to the [[https://docs.pycord.dev/​en/​master/​api/​clients.html|event]] decorator belonging to our global bot instance (line 58). The "processing" ​that the **event** decorator does is to assign **on_ready()** as the bot's callback method for the event with the same name. So when the bot finishes its initialization and connects to the server, it raises an //on_ready// event and our method will intercept ​it and resolve it. 
 +  * ''​async''​ & ''​await'':​ in //Python// there'​s these things called ​[[https://docs.python.org/3/library/​asyncio-task.html|coroutines]] (also quickly explained in [[https://docs.pycord.dev/en/stable/​faq.html#​coroutines|PyCord FAQ]]). Coroutines are functions that can be enteredexited and resumed at different points in their executionDiving into this subject may prove too difficult right now, so we'll try to keep it simple. Thus, please excuse ​the hand waving: basically, any event handler can be triggered at any time (even when other handlers are already executing), so we mark them as //​asynchronous//​ with the **async** keyword. When we try to call on asynchronous functions, we need to **await** their execution to finish before continuing on our merry way. 
 +  * ''​@bot.command'':​ yet another decorator, but this one registers the following function as a __command__. Commands are identified by the prefix established during the bot'​s ​instantiation (in this case, ''​!''​). So, by writing //"​!roll 100"// in the **discord** chat, the bot will read the message, recognize **roll** as a command, parse the argument ​and reply with a random number between 1 and 100. 
 +  * ''​@roll.error'':​ this decorator defines an error callback routine for the **roll** command. Without this, if the user forgets ​to specify ​the argument let's say, the **roll()** function will fail with a nasty error printed to //stdout// but the script will not crash! By specifying the error handler, we can output its message to the discord user instead ​of the random numberThis way, he isn't greeted by silence, but instead learns what he did wrong. 
 +  * ''​os.environ['​BOT_TOKEN'​]'':​ generally, it'​s ​bad idea to hardcode IDs and passwords into the source codeAs such, we save our bot's token inside an environment variable in **bash** (or **zsh**). When running a program from your shell, it inherits all your variables. So before you run the skeleton, export the token under the name //​BOT_TOKEN//​. 
 + 
 +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 drivenso debugging ​it would be kind of annoying. For this, you can use a little hack: 
 + 
 +<code python>​ 
 +code.interact(local=dict(globals(),​ **locals()))  
 +</​code>​ 
 + 
 +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 variableswe need to combine them in a single dictionary. **dict** is the dictionary type. When using it to create a new dictionary ​(instead 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! [[https://​docs.python.org/​3/​library/​stdtypes.html#​dict|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()**. 
 + 
 +<code python>​ 
 +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} 
 +</​code>​ 
 + 
 +For now, use **code.interact()** in the **on_message()** function. Inspect the [[https://​docs.pycord.dev/​en/​stable/​api/​clients.html#​discord.Bot|bot]] and [[https://​docs.pycord.dev/​en/​stable/​api/​models.html#​discord.Message|msg]] variables.
  
-In this exercise we will //​manually//​ add a GPL license to your bot. If you want to learn more about different kinds of licenses (or licenses in general), listen to this episode of the Destination Linux podcast. If you want to go straight to the discussion on each individual license, skip ahead 10m relative to the timestamp in the video. 
  
-<​html><​center>​ 
-<iframe width="​700"​ height="​400"​ src="​https://​www.youtube.com/​embed/​dsm1SKqVsTQ?​controls=0&​amp;​start=406"​ title="​YouTube video player"​ frameborder="​0"​ allow="​accelerometer;​ autoplay; clipboard-write;​ encrypted-media;​ gyroscope; picture-in-picture"​ allowfullscreen></​iframe>​ 
-</​center></​html>​ 
ii/labs/04/tasks/02.1638793780.txt.gz · Last modified: 2021/12/06 14:29 by radu.mantu
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0