22P — Gestion d'événements avec un bot Discord
Par admin le mardi, mai 10 2022, 12:00 - 2021–2022 - Lien permanent
Ce projet a été réalisé par Dutruy, Eloi.
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:
- Importer toutes les librairies nécessaires.
- Importer les informations importantes depuis le fichier de configuration
- Configurer le bot et la base de données
- Création des fonctions utiles
- Permission
- Confirmation
- Send
- Gestion des événements Discord
- Prêt
- Nouveau membre
- Modification de message
- Gestion des rôles
- Gestion des messages
- Gestion des commandes Discord
- Aide
- Information
- Créer un événement
- Lister les événements
- Supprimer des événements
- Modifier des événements
- Voir les informations concernant un événements
- Gestion des commandes inexistantes.
- 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.
- Gestion des événements Discord
- Nouveau membre
- Gestion des rôles
- Modification de message
- Gestion des messages
- Gestion des commandes Discord
- Aide
- Information
- Créer un événement
- Lister les événements
- Supprimer des événements
- Modifier des événements
- Voir les informations concernant un événements
- 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