Introduction

Aujourd’hui, nous allons apprendre les fondamentaux de la programmation de bot Discord en Python avec la bibliothèque Pycord.

Prérequis

Attention : Des bases solides en Python sont nécessaires pour la programmation d’un bot Discord. Comprendre des concepts tels que la programmation asynchrone et les requêtes API est important.

Nous utiliserons les outils suivants :

Pour installer Python, il faut se rendre sur le site officiel : python.org . Une fois Python installé et ajouté au PATH (variables d’environnement), on peut installer Pycord avec la commande suivante :

pip install py-cord

Dans le cas ou la commande précédente ne fonctionne pas :

python -m pip install py-cord

Fonctionnement d’un bot Discord

Un bot Discord fonctionne sur le principe d’API. Notre bot reçoit les informations de Discord sur les actions effectuées par un utilisateur, exécute les fonctions correspondantes puis renvoie à Discord une instruction (envoyer un message, octroyer un rôle…). L’action d’un utilisateur déclenche alors les instructions programmées dans l’application (ici, un bot).

Schéma du fonctionnement d’un bot Discord

Création de l’application

Un bot Discord est une application Discord. Pour le créer, il faut se rendre sur la page “Développeur” de Discord : discord.com/developers .

Il est également important de configurer les “Intents”, les permissions de notre application : lire des messages, obtenir des informations sur les serveurs…

Pour inviter le bot dans un serveur, il faut générer un lien dans la section “OAuth2” en prenant soin de cocher les cases “bot”, et “applications.commands”. Cela permettra au bot d’ajouter des commandes au serveur qu’il rejoint.

Cet article ne se penche pas en détails sur la création de l’application. Pour plus d’informations, vous pouvez vous référer à ces ressources :

Premier bot

Instanciation d’un objet Client

Dans un premier temps, nous allons créer un bot avec une commande simple. Pour ce faire, il faut d’abord instancier un object Client de la bibliothèque discord :

import discord


intents = discord.Intents.default()
client = discord.Client(intents=intents, status="online")

Ici, on déclare d’abord les “Intents”, celles définies dans le portail développeur de Discord. Cette variable peut prendre plusieurs valeurs selon les options choisies :

  • discord.Intents.all() : Toutes les permissions.
  • discord.Intents.none() : Aucune permission.

Pour choisir d’autoriser certains Intents, il faut définir l’attribut associé à True. Voici un exemple pour autoriser la lecture du contenu des messages :

import discord

intents = discord.Intents.default()
intents.message_content = True

Tous les attributs configurables peuvent être trouvés dans la documentation officielle de Pycord .

Une fois les Intents définis, on instancie un objet Client en passant en paramètres les Intents ainsi que le statut du bot (En ligne, Occupé, Absent…). Cet objet correspond au bot, ce sont ses attributs et méthodes qui seront modifiés pour répondre aux actions des utilisateurs.

Première commande

Configurons à présent une commande “slash” (dont le préfixe est “/”) basique pour saluer un utilisateur :

@client.slash_command(name="hello", description="Saluer un utilisateur.")
async def hello(msg, utilisateur: str):
    await msg.respond(f"Salutation à toi, {utilisateur}.")

Ici, le décorateur @client.slash_command() sert à déclarer une commande ainsi que ses spécifications : nom, description, restriction de serveurs… (voir plus ici ). La fonction décorée correspond à la fonction appelée lorsqu’un utilisateur entre la commande. Cette dernière comporte plusieurs paramètres :

  • msg : Le premier paramètre correspond au contexte dans lequel est exécutée la commande. On peut notamment accéder à l’auteur du message, à son contenu et d’autres attributs .
  • utilisateur: str : Un paramètre portant le même nom qui est précisé par l’utilisateur de la commande. Il est important de mentionner le type de cette variable pour que Discord puisse vérifier que l’utilisateur entre les paramètres comme il se doit. Malgré tout, on utilise ici un moyen assez primitif de déclarer des paramètres (nous en étudierons un plus poussé).

Démarrage du bot

À présent, nous pouvons allumer le bot pour le tester en ajoutant cette ligne à la fin du code :

client.run(token)

La variable token est l’identifiant unique du bot, à ne surtout pas partager et disponible sur le portail développeur .

Gestion d’évènements

Il est maintenant temps d’explorer la gestion des évènements basiques tels que :

  • Changer le message de statut du bot.
  • Message personnalisé lorsqu’un utilisateur rejoint/ quitte un serveur.

Ces évènements sont accessibles via des méthodes existantes trouvables dans la documentation :

Changer le statut au démarrage

De manière générale, on utilise la fonction on_ready() car elle est presque toujours appelée au démarrage du bot. Ce qui est pratique pour définir le statut du bot. À noter que la définition du statut est, elle, réalisée par client.change_presence() .

@client.event
async def on_ready():
    await client.change_presence(activity=discord.Game("Un jeu"))

Ce code change donc le statut du bot pour “Joue à Un jeu” à son démarrage.

Note : Il est possible de choisir d’autres activités telles que “Écoute …” ou “Streame …”. Voir la documentation des activités .

Message d’accueil sur un serveur

Pour envoyer un message à l’arrivée d’un membre , il faut utiliser on_member_join(member) pour réagir à l’évènement. Mais il faut également un canal dans lequel le message sera envoyé. Ici, il sera récupéré grâce à son ID (identifiant unique).

@client.event
async def on_member_join(member):
    channel = client.get_channel(CHANNEL_ID)
    await channel.send(f"Bienvenue à {member.name} sur le serveur.")

Note : Cette fonction requiert que l’Intent “Intents.members” soit activé pour être accessible.

Exercice : Faire en sorte que le bot envoie un message lorsqu’un utilisateur quitte un serveur. Vous pouvez vous aider des ressources suivantes :

Commandes à paramètres poussés

On a vu plus haut une manière de créer une commande à paramètre. Ici, nous allons étudier le décorateur @option ainsi que la classe Option(). Il sera possible de configurer les paramètres que prend la commande : type, valeur minimum/ maximum, optionnel ou non, description…

Les deux possibilités se ressemblent, dans chaque cas, il sera possible de préciser des détails sur les paramètres. Il est important de toujours mentionner le type de variable attendu (utilisateur, entier, texte…).

Avec le décorateur @option, il faut passer en premier argument le nom du paramètre associé. Alors que la classe Option() sera utilisée pour annoter un paramètre, il n’est donc pas utile de préciser le nom de la variable.

Avec @option

import discord
from discord import option


@client.slash_command(name="gift", description="Offrir une quantité de pièces à un utilisateur.")
@option("utilisateur", type=discord.Member, required=True)
@option("quantite", type=int, min_value=10, max_value=100, default=10, description="Nombre de pièces")
async def gift(msg, utilisateur, quantite):
    await msg.respond(f"Vous offrez {quantite} pièces à {utilisateur.mention}")

Avec Option()

import discord
from discord import Option


@client.slash_command(name="gift", description="Offrir une quantité de pièces à un utilisateur.")
async def gift(msg,
               utilisateur: Option(discord.Member, required=True),
               quantite: Option(int, min_value=10, max_value=100, default=10, description="Nombre de pièces")):
    await msg.respond(f"Vous offrez {quantite} pièces à {utilisateur.mention}")

Dans ces deux exemples, la commande prend en paramètre un membre du serveur (discord.Member) qui est un paramètre obligatoire (required=True). Un paramètre pour indiquer le nombre de pièces est configuré pour avoir une valeur par défaut de 10, ainsi qu’un intervalle de valeurs valides (de 10 à 100). Il est possible de donner une description à un paramètre, elle est alors affichées dans Discord lorsque l’on rentre la commande.

Il existe de nombreuses options configurations pour spécifier un paramètre, trouvables dans la documentation . Une liste des types acceptables pour un paramètre s’y trouve également.

Les deux méthodes sont équivalentes, utilisez votre favorite !

Note : Il est possible de créer des paramètres prédéterminés parmi lesquels l’utilisateur peut choisir. Cela se fait avec le paramètre choices qui correspond à la liste des choix pour l’utilisateur.

On peut également proposer des choix avec OptionChoice pour associer une valeur à un choix (à l’instar d’un dictionnaire) :

@client.slash_command(name="buy", description="Acheter un kilo de fruit")
async def buy(msg,
              fruit: Option(str, required=True, choices=[
                  OptionChoice("Pomme", "1.8"),
                  OptionChoice("Banane", "2.25")
              ])):
    await msg.respond(f"Le prix est de {fruit} €")

C’est l’équivalent de :

fruits: dict = {
    "Pomme": "1.8",
    "Banane": "2.25"
}


@client.slash_command(name="buy", description="Acheter un kilo de fruit")
async def buy(msg,
               fruit: Option(str, required=True, choices=["Pomme", "Banane"])):
    await msg.respond(f"Le prix est de {fruits[fruit]} €")

Gestion des permissions

À l’instar de l’ajout d’une option, on peut restreindre l’usage de commande aux utilisateurs possédant une permission en particulier. Cela se fait à l’aide d’un décorateur : @has_permissions .

Voici un exemple pour une commande qui supprime un nombre donné de messages :

import discord
from discord.ext.commands import has_permissions


@client.slash_command(name="clear", description="Supprime un nombre donné de message.")
@has_permissions(manage_messages=True)
async def clear(msg,
                nombre: Option(int, min_value=1, max_value=100, required=True)):
    await msg.channel.purge(limit=nombre)
    await msg.respond(f"{nombre} messages supprimés.")

L’usage de cette commande est restreint aux utilisateurs qui possèdent la permission de gérer les messages. La liste complète des permissions se trouve ici .

Exercice : Créer une commande pour rendre un utilisateur muet pendant une durée donnée. Restreindre l’usage de cette commande aux membres qui ont la permission. Vous pouvez vous aider des ressources suivantes :

Gestion des délais (Cooldown)

Une fonctionnalité intéressante est de pouvoir limiter l’usage d’une commande avec un délai. On parle alors de cooldown. Un cooldown se définit principalement par deux variables :

  • Un intervalle de temps
  • Un nombre de commandes autorisées pendant cet intervalle

C’est le décorateur @commands.cooldown(rate, per, type) qui permet cela :

  • rate : Nombre de fois que la commande peut être utilisée avant de déclencher le délai.
  • per : La durée pendant laquelle la commande n’est plus utilisable.
  • type : Le type de cooldown.

Voici un exemple d’une commande limitée à 2 usages par intervalle de 10 secondes :

import discord
from discord import Option
from discord.ext import commands


@client.slash_command(name="clear", description="Supprime un nombre donné de message.")
@commands.cooldown(2, 10, commands.BucketType.user)
async def clear(msg, nombre: Option(int, min_value=1, max_value=100, required=True)):
    await msg.channel.purge(limit=nombre)
    await msg.respond(f"{nombre} messages supprimés.")

La commande précédente est donc limitée par un cooldown, mais il est aussi intéressant de prévoir un message pour avertir l’utilisateur de la durée restante. Pour cela, nous allons utiliser le fait que l’erreur CommandOnCooldown est levée lorsqu’un utilisateur tente d’exécuter une commande pendant le délai.

Il existe une fonction déclenchée à chaque erreur de commande : on_application_command_error. Nous allons donc prévoir le cas où nous rencontrons l’erreur CommandOnCooldown pour répondre à l’utilisateur :

@client.event
async def on_application_command_error(msg, erreur):
    if isinstance(erreur, commands.CommandOnCooldown):  # Vérifier le type de l'erreur
        await msg.respond(f"Veuillez patienter {erreur.retry_after:.1f}s")
    else:
        raise erreur

Envoi d’un Embed

Une autre fonctionnalité intéressante de pouvoir envoyer des intégrations, dites Embed .

Dans un premier temps, il faut instancier un objet Embed(), puis spécifier ses attributs. En voici quelques-uns, la liste complète se trouve dans la documentation :

  • title : Le titre de l’intégration.
  • color/ colour : La couleur de la barre à gauche de l’intégration.
  • footer : Le texte au pied de l’intégration.

Certains de ces attributs peuvent être indiqués à l’instanciation de l’objet (comme title ou color), d’autres sont configurables avec une méthode dédiée (par exemple, footer est défini avec la méthode set_footer() ).

Un autre aspect important des intégrations sont les “champs” (fields). Un champ est une ligne de l’intégration définie par un nom, ainsi qu’une valeur. Pour ajouter un champ, il faut faire appel à la méthode add_field() .

Voici un exemple d’intégration qui contient une image, ainsi qu’un champ :

@client.slash_command(name="info", description="Statistique de l'utilisateur")
async def my_embed(msg):
    embed = discord.Embed(title="Informations", description=f"Les informations sur {msg.author.name}", color=0x46e32b)
    embed.set_thumbnail(url="https://placehold.co/[email protected]")
    embed.add_field(name="Pièces", value="Vous possédez 10 pièces", inline=False)

    await msg.respond(embed=embed)

La création d’un Embed peut parfois s’avérer fastidieuse, il existe de nombreux sites pour facilement configurer et prévisualiser des Embed. Par exemple :

Conclusion

Avec tous les éléments précédents, vous possédez dorénavant de bonnes connaissances pour démarrer la création d’un bot Discord. Dans un article prochain, le sujet des composants interactifs sera abordé. Il concernera la mise en place de boutons, menus déroulants, formulaires, réactions…

Crédits et ressources

Documentation officielle de Pycord : docs.pycord.dev .