1. Introduction

Étant responsable des inscriptions au sein d’une société, je me suis rendu compte de l’utilité du site Doodle qui permet de faciliter grandement le choix de la date et de l’heure d’une réunion entre plusieurs personnes. Je pense donc créer quelque chose de semblable qui répondrait à mes besoins personnels concernant ma fonction dans la société, car actuellement soit je dois prendre les inscriptions à la main, soit je dois me débrouiller avec Doodle qui n’est pas toujours idéal quand on ne possède pas de compte « premium ». Nous avons également un Discord où nous communiquons et cela serait plus simple pour tout le monde de faire cela directement sur Discord.

L’idée est donc de créer un bot discord. Je pourrais interagir avec de façon à ajouter des événements et les gens peuvent interagir avec afin de donner leurs disponibilités. En tant qu’utilisateur on doit également pouvoir interagir de façon à supprimer, changer ou voir les détails d’un événement.

2. Matériel et méthodes

2.1 Matériel

Comme il s'agit là d'un projet purement software je n'ai pas eu besoin de matériel particulier, juste mon ordinateur.
Le projet est écrit en python 3.10.

2.2 Méthode

J'ai donc structuré mon code ainsi:

  1. Importer toutes les librairies nécessaires.
  2. Importer les informations importantes depuis le fichier de configuration
  3. Configurer le bot et la base de données
  4. Création des fonctions utiles
    1. Permission
    2. Confirmation
    3. Send
  5. Gestion des événements Discord
    1. Prêt
    2. Nouveau membre
    3. Modification de message
    4. Gestion des rôles
    5. Gestion des messages
  6. Gestion des commandes Discord
    1. Aide
    2. Information
    3. Créer un événement
    4. Lister les événements
    5. Supprimer des événements
    6. Modifier des événements
    7. Voir les informations concernant un événements
    8. Gestion des commandes inexistantes.
  7. Lancer le bot

2.2.1 Importer toutes les librairies nécessaires.

Les librairies nécessaires à ce projet sont:

2.2.1.1 discord

Elle nous permet de gérer les permissions du bot sur le serveur (les Intents) ou encore de mettre en forme proprement les messages (Embed) que notre bot enverra. Documentation

2.2.1.2 discord_components

Cette librairie nous donne accès de manière simple et concise à la création de bouton. Documentation

2.2.1.3 discord.ext.commands

Nous n'importerons que CommandNotFound depuis cette librairie. Documentation

2.2.1.4 dotenv et os

On importe simplement getenv d'os et load_dotenv de dotenv. On les utilisera juste dans la section suivante (2.2.2 Importer les informations importantes depuis le fichier de configuration).

2.2.1.5 tinydb

On importe TinyDB, where et Query depuis tinydb. C'est la librairie qui comme son nom l'indique ("tiny database" signifie "petite base de données" en français) nous permet de gérer facilement en python des petites bases de données contenues dans des fichiers .json. Documentation

2.2.1.6 asyncio

Asyncio est un module qui permet de gérer la programmation asynchrone. On utilisera juste sleep

2.2.2 Importer les informations importantes depuis le fichier de configuration

Un certain nombre d'informations peuvent être considérées comme sensibles, notamment on ne désirerait pas que le token de notre bot (la clé qui nous permet de se connecter au bot) ou que les identifiants des personnes ou du serveur tombent dans de mauvaises mains si nous devions partager notre code en ligne. Nous séparons donc ces informations et nous plaçons dans un fichier que j'ai nommé config. Ainsi si nous devions partager notre code à un autre utilisateur, nous pourrons partager tout le projet sauf le fichier config. Cet autre utilisateur pourra se créer un fichier du même nom et tout fonctionnera de la même manière tout en s'assurant qu'il ne dispose d'aucune information nous concernant.
L'on créer donc notre fichier config à l'intérieur duquel on écrit nos informations sensibles (les informations suivantes sont bien évidemment fausses).

 # general information
 TOKEN=2rjf0ua58vy3jtrl
 GUILD_ID=8666712168187226

Il est également défini dans ce fichier les identifiants des salons textuelles, les identifiants des rôles, les identifiants des personnes de confiance et l'identifiant d'un message particulier. On peut à présent récupérer les informations dans notre fichiers main. Il faut d'abord charger le fichier puis l'on peut récupérer les valeurs.

 # load config
 load_dotenv(dotenv_path="./config")
 # get the token
 TOKEN_BOT = getenv("TOKEN")
 # get guild ID
 GUILD_ID = int(getenv("GUILD_ID"))

L'on récupère également les autres informations. Nous pouvons à présent disposer de toutes nos informations sensibles dans notre fichier principal.

2.2.3 Configurer le bot et la base de données

Il faut tout d'abord autoriser notre bot à détecter les événements concernant les membres et les réactions sur notre serveur, passer ces deux intents sur vrai lui donne la permission. Tous les intents sont par défaut sur faux.

 # create an instance for discord.Intents.default()
 intents = discord.Intents.default()
 # enable permissions for the intents we need
 intents.members = True
 intents.reactions = True

On peut à présent créer une instance pour notre bot. On déclare les intents qu'on vient de définir. ainsi que le préfixe (dans mon cas "!" qui annoncera nos commandes).

 bot = ComponentsBot(command_prefix='!', intents=intents)

Pour la base de données, il faut juste déclarer le chemin vers notre fichier json.

 db_file = './db_event.json'
 db = TinyDB(db_file, indent=4)

Tout est à présent configuré.

2.2.4 Création des fonctions utiles

Dans cette partie on déclare les fonctions utiles qui nous permettront de réduire la taille de notre code par après car sinon on devrais réécrire plusieurs fois les mêmes lignes de code.

2.2.4.1 Permission

Cette fonction permet de vérifier si un événement existe et si l’utilisateur à le droit de le modifier. On prévoit un message d'erreur si l'une des deux conditions n'est pas remplie. On prend en argument le contexte et le nom de l'événement.

2.2.4.2 Confirmation

Cette fonction envoie deux boutons à l'utilisateur, qui lui permette de confirmer s'il veut oui ou non faire une certaine action. L'on doit déclarer cette fonction avec async def car l'on ne veut pas que cette fonction soit bloquante. Le reste de notre code doit continuer à s’exécuter en attendant la confirmation de l'utilisateur.

2.2.4.3 Send

Cette fonction envoie une demande sur le salon event_registration. L'utilisateur peut répondre s'il vient à un certain événement ou s'il sera absent. L'on envoie ensuite un message privé à l'utilisateur pour confirmer sa réponse. La fonction attend constamment afin de vérifier la réponse de chaque utilisateur. On doit également pouvoir changer d'avis. Comme pour confirmation cette fonction ne doit pas être bloquante.

2.2.5 Gestion des événements Discord

Dans cette partie, on gère tous les événements Discord qui nous intéresse. On déclare les événements à l'aide du décorateur @bot.event.

2.2.5.1 Prêt

Cette événement permet d'annoncer à l'utilisateur qui lance le programme que le bot est prêt.

 # make sure the bot is ready
 @bot.event
 async def on_ready():
     print("Bot is ready. (bot)")
2.2.5.2 Nouveau membre

On gère simplement l'arrivée des nouveaux membres et on envoie un message de bienvenue sur le salon welcome.

2.2.5.3 Modification de message

Cette événement survient quand une personne modifie un message. Afin de garder une trace du message original. On envoie un message avertissant des modifications.

2.2.5.4 Gestion des rôles

Quand l'utilisateur arrive sur le serveur il n'a accès qu'aux salons suivants: welcome et rules. S'il accepte le message sur le salon rules, on lui donnera le rôle requis qui l'autorisera à avoir accès aux salons command_channel et event_registration. On utilisera donc les événements liés à l’ajout et au retrait de réactions sous un message.

2.2.5.5 Gestion des messages

Cet événement permet tout d'abord de s'assurer que le bot n'interagit pas avec lui-même.

 @bot.event
 async def on_message(message):
     # make sure the bot is not interacting with itself
     if message.author == bot.user:
         return

Ensuite on vérifie que le salon textuel dédié au commande ne contienne que des commandes, sinon on supprime le message et on avertit l'utilisateur.

2.2.6 Gestion des commandes Discord

Dans cette partie, on défini toutes les commandes. Elle sont reconnues grâce au préfixe défini au début.

2.2.6.1 Aide

Cette commande indique à l'utilisateur la liste des commandes disponibles. Elle fait office de documentation. Il faut d'abord retirer la commande help de base fournie par le module discord.

 # remove the basic !help commands
 bot.remove_command('help')

Puis l'on créer notre message et on l'envoie finalement.

 await ctx.send(embed=help_message)
2.2.6.2 Information

Cette commande donne des informations sur le serveur, le salon, l'auteur et l'identifiant du message.

 @bot.command()
 async def info(ctx):
     info_data = discord.Embed(
         title='Information',
         description=f"The command was send on the server: {ctx.guild}.\n"
                     f"On the {ctx.channel} text channel.\n"
                     f"The author was {ctx.author}.\n"
                     f"The id of the message is {ctx.message.id}"
                     f"\n\n\nIf you are looking for a command. Type !help",
         colour=discord.Colour.green()
     )
     await ctx.send(embed=info_data)

L'on peut voir ici, un exemple assez simple d'Embed, il s'agit d'une manière plus propre de mettre en page un message discord.

2.2.6.3 Créer un événement

Cette commande permet de créer des événements. Tout d'abord l'on prend le message et l'on récupère les arguments dans une liste:

 @bot.command()
 async def mkev(ctx):
     creator = str(ctx.author.id)
     content = ctx.message.content.lstrip('!mkev')
     arg_list = content.split(',')

Puis il faut enlever nos mots clés et les assignés à nos variables.

 for i in arg_list:
         arg = i.lstrip()
         if arg.startswith("name"):
             name = arg.lstrip("name").lstrip().lstrip('=').lstrip().rstrip()
         if arg.startswith("date"):
             date = arg.lstrip("date").lstrip().lstrip('=').lstrip().rstrip()
         if arg.startswith("time"):
             time = arg.lstrip("time").lstrip().lstrip('=').lstrip().rstrip()
         if arg.startswith("place"):
             place = arg.lstrip("place").lstrip().lstrip('=').lstrip().rstrip()

Si l'événement n'a pas de nom ou pas de date, on prépare un message d'erreur.

 # make sure the event has at least a name and a date
     if name == '' or date == '':
         message = discord.Embed(
             title='Error: event has not been created',
             description='Your event must at least have a name and a place to be created!',
             colour=discord.Colour.red())

Si l'événement à le même nom qu'un événement qui existe déjà, on prépare un message d'erreur.

 # check if an event with the same name already exist, if so don't let the user create a new one
     elif db.search(where('name') == name):
         message = discord.Embed(
             title='Error: event has not been created',
             description='Each event has its own unique name. \\
                   You cannot create an event with an already existing name.',
             colour=discord.Colour.red())

Si l'événement peut être créer l'on ajoute dans la base de données et l'on prépare un message de confirmation.

 # confirmation message
     else:
         db.insert({"name": name,
                    "date": date,
                    "time": time,
                    "place": place,
                    "attendees": attendees,
                    "absents": absents,
                    "creator": creator})
         message = discord.Embed(
             title=f'{ctx.author} created an event',
             description=f"{name} will take place on the {date}",
             colour=discord.Colour.blue())

L'on peut donc envoyer le message et notre événement est à présent créer !

 # send message
 await ctx.send(embed=message)
2.2.6.4 Lister les événements

Cette commande permet de lister les événements. L'on commence par déclarer notre commande

 # list events
 @bot.command()
 async def lsev(ctx):

Puis l'on initialise une variable n car il y a n événement(s) dans notre base de données.

     n = 0

L'on déclare une chaîne de caractère vide.

     list_of_events = ''

Et l'on récupère toutes les données depuis notre base de données.

     db_dic = db.all()

Tant que nous n'avons pas parcouru toutes les événements, on continue à les parcourir. L'on récupère le nom, la date, l'heure et le lieu de l'événement.

     while n < len(db):
         name = db_dic[n]['name']
         date = db_dic[n]['date']
         time = db_dic[n]['time']
         place = db_dic[n]['place']

L'on ajoute notre événements à la liste des événements déclarées plus haut.

         list_of_events += f"{name} will take place on the {date}"

Si l'événement à une heure.

         if time != '':
             list_of_events += f" {time}"

Si l'événement à un lieu.

         if place != '':
             list_of_events += f" at {place}"

L'on met un point à notre phrase puis l'on passe à la ligne. Enfin nous pouvons passer indenter notre n afin de répéter cela pour le prochain événement.

         list_of_events += ".\n"
         n += 1

Une fois cela fait il ne reste plus qu'à définir un message et l'envoyer.

     message = discord.Embed(
         title='List of all events',
         description=list_of_events,
         colour=discord.Colour.blue())
     await ctx.send(embed=message)
2.2.6.5 Supprimer des événements

Cette commande permet de supprimer des événements. Pour cela on commence par vérifier si l'événement existe et si l'utilisateur à le droit de le modifier.

 # delete an event
 @bot.command()
 async def rmev(ctx):
     # get argument
     data_to_remove = ctx.message.content.lstrip('!rmev').strip()
     message = permission(ctx, data_to_remove)
     # if the user has the permission to delete
     if message.title == 'Authorized':

L'utilisateur à donc le droit de supprimer cet événement. Il faut à présent lui demander de confirmer ses actions.

         # ask for user confirmation
         message = await confirmation(ctx, event_name=data_to_remove,
                                      button1_label='YES', button1_custom_id='button_yes_rm',
                                      button2_label='NO', button2_custom_id='button_no_rm',
                                      confirmation_question='Do you really want \\
                                            to delete this event ?')

Si l'utilisateur accepte, nous pouvons retirer l'événement de la base de données.

         # if the user accepts
         if message.title == 'Modification success':
             # remove the event, db.remove return the id of the object deleted in a list
             db.remove(where('name') == data_to_remove)

L'on peut envoyer le message de confirmation selon les choix de l’utilisateur.

     # send message
     await ctx.send(embed=message)
2.2.6.6 Modifier des événements

Cette commande permet de modifier des événements. L'on commence par récupérer les arguments puis l'on vérifie les permissions de façon relativement similaires à !mkev et !rmev que nous avons traité plus haut. Puis l'on demande la confirmation. Si tout est validé, l'on peut alors changer nos données.
L'on commence par ouvrir une requête avec notre base de données. L'on indique dans quelle colonne nous allons chercher, dans notre cas celle du nom.

 # open a query
 query = Query().name

Si une nouvelle date est définie, l'on place alors la nouvelle valeur dans notre base de données.

 # if a new date is define, change the date
 if not new_date == '':
 db.upsert({'date': new_date}, query == name)

Idem pour le nom, l'heure et le lieu.

2.2.6.7 Voir les informations concernant un événement

Cette commande permet de voire les informations concernant un événement en particulier. L'on commence par déclarer notre commande puis récupérer le nom de l'événement qui nous intéresse.

 @bot.command()
 async def seeev(ctx):
     # get argument
     data_to_show = ctx.message.content.lstrip('!seeev').strip()
     data = db.search(where('name') == data_to_show)  # return a list with a dict inside

L'on vérifie si l'événement concerné existe. Si oui on récupère toutes les informations le concernant.

    # check if event exists, if data is an empty list then it would be False
     if data:
         data_dic = data[0]  # get just the dict
         # get values
         name = data_dic['name']
         date = data_dic['date']
         time = data_dic['time']
         place = data_dic['place']
         attendees_list = data_dic['attendees']  # list of user id in str
         absents_list = data_dic['absents']      # list of user id in str
         creator = data_dic['creator']

L'on recherche le créateur. fetch_user va chercher l'utilisateur qui correspond à l'identifiant qu'on lui passe.

 # if event is not created by manual insertion, get user
         if not creator == 'manual insertion':
             creator = await bot.fetch_user(creator)

Si le lieu n'est pas indiqué, on le défini alors comme non indiqué.

         # if the place is not indicate
         if place == '':
             place = 'no place indicated'

Idem pour l'heure
L'on déclare une chaîne de caractère vide pour la liste des personnes présentes, puis l'on ajoute toutes les personnes présentes à l'intérieur de cette dernière.

         attendees = ''
         # if attendees list is not empty
         if attendees_list:
             for user_id in attendees_list:
                 # fetch user name
                 user = await bot.fetch_user(int(user_id))
                 attendees += str(user) + "\n"
         # if attendees list is not empty
         else:
             attendees = 'No one is announced present'

Idem pour les absents.
Puis l'on définit un message disposant toutes les informations obtenues à propos de l'événement et on l'envoie.

2.2.6.8 Gestion des commandes inexistantes.

Ici nous cherchons à gérer toutes les commandes que l'utilisateurs pourrait taper mais qui n'existe pas. On commence par vérifier si l'objet error est une instance ou une sous-classe de CommandNotFound.

 if isinstance(error, CommandNotFound):

Si c'est le cas, on est bien face à la bonne erreur et on peut donc envoyer un message informant l'utilisateur que la commande tapée n'existe pas.

     error_message = await ctx.send(embed=non_existing_command_message)

Afin de garder le salon textuel propre, nous supprimons la commande erronée ainsi que le message que nous venons d'envoyer après 15 secondes.

     await sleep(15)
     await ctx.message.delete()
     await error_message.delete()

2.2.7 Lancer le bot

Et finalement il ne reste plus qu'à exécuter le code écrit jusqu'à présent en lançant le bot.

 # run the bot
 bot.run(TOKEN_BOT)

3. Résultats

Pour tester le résultat j'ai donc créé un compte de test. J'ai mené les tests suivants.

  1. Gestion des événements Discord
    1. Nouveau membre
    2. Gestion des rôles
    3. Modification de message
    4. Gestion des messages
  2. Gestion des commandes Discord
    1. Aide
    2. Information
    3. Créer un événement
    4. Lister les événements
    5. Supprimer des événements
    6. Modifier des événements
    7. Voir les informations concernant un événements
    8. Gestion des commandes inexistantes.

3.1 Gestion des événements Discord

Dans cette première phase de test, j'ai testé toutes les fonctionnalités concernant les événements discord qui doivent être gérés par notre code.

3.1.1 Nouveau membre

Lorsqu'un nouveau membre rejoint notre serveur, notre bot doit réagir. Tout fonctionne bien concernant cet événement.

3.1.2 Gestion des rôles

Lorsqu'un utilisateur accepte le message un rôle doit lui être attribué. Ci-dessus, une image où l'on voit que l'utilisateur n'a pas encore réagit au message. Aucun rôle ne lui est donc attribué comme on le voit sur l'onglet disponible à droite. Il n'a accès qu'à deux salons textuels sur la gauche de l'image. A présent l'utilisateur a réagit au message et il s'est vu attribuer le même rôle que les autres utilisateurs comme on le constate à droite. Il a maintenant accès à tous les salons du serveur.
Tout fonctionne donc bien concernant cet événement.

3.1.3 Modification de message

Quand l'utilisateur modifie un événement le bot réagit. Tout fonctionne bien concernant cet événement.

3.1.4 Gestion des messages

Le bot vérifie supprime le message si ce n'est pas une commande sur le salon déstiné aux commandes. Si il s'agit d'une commande mais qu'elle n'existe pas. Il doit également réagir puis la supprimer. Je n'ai pas mis d'image illustrant la suppression des messages, mais cela fonctionne! Tout fonctionne donc bien concernant cet événement.

3.2 Gestion des commandes Discord

Dans cette deuxième phase de test, j'ai testé toutes les fonctionnalités concernant les commandes qui doivent être gérées par notre code.

3.2.1 Aide

J'ai commencé par \!help. Le résultat est là, tout fonctionne.

3.2.2 Information

!info fonctionne également très bien.

3.2.3 Créer un événement

La commande !mkev (make event) créer un événement. Elle prend comme argument la date, le lieu, l'heure et le nom de l'événement. Imaginons que nous voulons créer un événement le 10 mai, il s'agirait de la deadline pour le projet P à midi au GyRe. Nous tapons alors la commande suivante. Nous constatons que le bot a bien confirmé la création de l'événement.
Une invitation est à présent ouverte dans le salon "event_registration". Si un utilisateur appuie sur l'un des deux boutons, il recevra un message privé lui confirmant son choix. Nous ne pourrions pas recréer un événement du même nom comme le montre l'image suivante.

3.2.4 Lister les événements

Cette commande (!lsev) liste les événements présents dans notre base de données.

3.2.5 Supprimer des événements

!rmev retire des événements de la base de données. L'utilisateur doit confirmer ses actions. Si l'utilisateur n'a pas les droits, il ne doit pas pouvoir supprimer les données. Si l'événement n'existe pas, il y a une erreur. Et si quelqu'un d'autre que l'utilisateur qui a tapé la commande appuie sur un bouton, une erreur apparaît également.

3.2.6 Modifier des événements

Tout comme pour la suppression d'un événement, !chev vérifie si l'utilisateur à le droit de modifier l'événement puis lui demande une confirmation.

3.2.7 Voir les informations concernant un événements

!seeev (see event) permet de voir les informations détaillées concernant un événement.

3.2.8 Gestion des commandes inexistantes.

Si l'utilisateur entre une commande inexistante, nous l'informons que la commande n'existe pas. Cela arrive régulièrement en voulant taper trop vite, il arrive fréquemment qu'on tape par inadvertance des commandes eronnées.

4. Discussion

Bien que tout fonctionne, il faut avouer que tout n'est pas parfait. Quelques bugs subsistent.

Par exemple si l'on créer un événement puis que l'on change son nom, l'invitation dans le salon event_registration cesse de fonctionner. Cela est embêtant, mais je me suis malheureusement rendu compte trop tard de ce problème.

Un autre problème survient au niveau des boutons, en effet quand l'on clique dessus discord nous indique Echec de l'interaction en rouge alors que le programme a réagi correctement à l'interaction de l'utilisateur. Je n'ai pas trouvé de solution à ce problème. Et Il semblerait que je ne soit pas le seul à rencontrer ce soucis d'après mes recherches.

Le projet à encore une grande marge de progression. Utiliser un module tel que datetime pour mieux gérer les dates serait intéressant. L'on pourrait ainsi trier les événements par date, définir un délai de réponse ou envoyer un rappel un certain temps avant l'événement.

Il reste encore certainement de nombreux bugs que je n'ai pas encore découvert, mais je me réjouis de les découvrir et de comprendre le pourquoi du comment, car le fait d'affronter personnellement une situation reste la meilleure source d'expérience. Et à présent que j'ai saisi le fonctionnement de la librairie discord.py, le nombre de possibilités s'offrant à moi pour l'amélioration de ce projet ou pour la création d'autres bots est énorme.

5. Conclusion

En conlusion, ce projet était passionnant à faire car j'ai beaucoup appris sur la programmation en général. Cette expérience en création de bot discord va m'être utile étant donné que j'utilise cette application quotidiennement pour communiquer avec différents groupes d'amis. Je réutiliserai donc certainement ces compétences dans un futur proche. Il est également satisfaisant de constater que le projet fonctionne plutôt bien.

Références

Documentation:

discord.py
discord_components
discord.ext.commands
tinydb

Inspiration pour mon code:

Si la commande n'existe pas