Créer un bot Discord avec discord.js

Créer un bot Discord avec discord.js

Dans ce guide sur Discord, nous allons allons vous guider pas à pas dans la création de votre bot Discord. Pour notre bot, nous allons utiliser le langage de développement JavaScript et la librairie officielle discord.js.

Nous utiliserons également le framework Discord Commando qui permet un développement rapide et facile, permettant d'accélérer le développement du bot en évitant de ré-inventer la roue (inutile par exemple de redévelopper un système de préfixe pour les commandes, un fonctionnalité basique déjà développé par d'autres pour nous!).

Dans ce tutoriel complet nous répondrons à tous les cas communs que vous pourriez rencontrer lors du développement de votre bot et ses commandes, afin de vous permettre de créer par la suite vos propres fonctionnalités.

Préambule

Ce guide est toujours en cours de rédaction, venez nous encourager sur notre Discord!

Les chapitres en cours de rédaction:

  • utilisation d'une base de données (firebase): système de ranking
  • hébergement du bot Discord, sur un VPS (+ auto-update)
  • lire un flux RSS + auto-posting
  • créer un commande de suggestion
  • créer une commande de statut du bot
  • créer une commande kick/ban
  • crawler un site internet et afficher le résultat
  • afficher le statut de son serveur (minecraft, ark, rust, fivem, gmod, etc) avec une commande
  • créer un channel de publicité (serveurs de jeu, vidéos, images, liens, etc)
  • recevoir les erreurs js du bot dans un channel

Prérequis: JavaScript, Node.js, Git et IDE

Avant de commencer, vous devez avoir à minima des notions de JavaScript. Vous pouvez consulter le tutoriel de OpenClassRooms: apprenez à programmer avec JavaScript.

Vous devrez également installer :

  • Un IDE - un éditeur de code avancé tel que Visual Studio Code sur Windows, pour un développement dans les meilleures conditions: coloration syntaxiques, debug, console d’exécution du bot, etc.
  • Node.js - un moteur d'exécution de code JavaScript, il interprète votre code source et le fait fonctionner. Pour l'installer sur Windows: cliquez ici. Sur macOS, brew install node et sur Linux: apt-get install node. La version 12 au minimum est requise.
  • Discord Developer - pour chaque bot que vous allez créer, vous devrez associer une application sur Discord Developer, qui vous attribuera un token spécifique à votre bot (ce token est confidentiel, ne le partagez jamais!). Nous abordons en détail ce point plus bas.
  • Git - un outil de versioning de code source. Pour l'installer sur Windows: git for windows. Sur Linux: apt-get install git.

Qu'est ce que npm ?

Dans ce tutoriel, nous allons régulièrement utiliser npm pour l'installation de librairies tierces comme discord.js, got et bien d'autres!

npm est un gestionnaire de paquet, il vous permet de gérer les dépendances externes (=non développées par vous) d'un projet très facilement. Son grand avantage est de mettre à disposition de votre projet tous les paquets de la communauté, allant des requêtes HTTP/HTTPS, à la gestion de base de données en passant par le requêtage d'un serveur de jeu (statut), ... les limites sont presque infinies! La librairie discord.js est par exemple un paquet npm.

Il a été installé automatiquement avec Node.js, il devrait donc déjà être présent sur votre système. Sachez néanmoins que contrairement à un programme traditionnel, il n'y a aucune interface graphique. Il vous faudra jouer avec la ligne de commande ;)

Pour parcourir la liste de tous les paquets, rendez-vous sur le site officiel. Quelques exemples de paquets que nous utilisons sur le bot officiel de mTxServ :

  • discord.js
  • discord.js-commando
  • discord.js-pagination
  • got
  • puppeteer
  • firebase-admin
  • rss-parser
  • steam-workshop

Création du répertoire du projet

Vous avez rempli tous les prérequis? Votre IDE et Node.js sont bien installés? C'est parti !

Il nous faut un répertoire pour accueillir le code source du bot, sur notre ordinateur. Dans un endroit facilement accessible, créez un nouveau dossier comme vous le feriez normalement (sur Linux, vous pouvez utiliser mkdir nom-projet dans votre terminal). Pour ma part, j'ai nommé le dossier bot.

Une fois que vous avez fini de créer le dossier, ouvrez-le (pour Linux, vous pouvez utiliser cd nom-projet).

Ligne de commande

J'ai commencé à vous en parler plus haut, pour utiliser npm nous devons utiliser la ligne de commande (= console = terminal). Pour les utilisateurs de Linux et MacOS rien de bien nouveau, mais si vous êtes sur Windows, il va falloir vous y mettre ;)

Toutes les commandes npm <commande> (et node <FICHIER.js>) du tutoriel doivent être entrées en ligne de commande, dans le répertoire du projet (= là où est votre code source).

Si vous êtes sous Linux, vous pouvez rapidement ouvrir le terminal avec Ctrl + Alt + T.

Si vous êtes sous Windows et que vous n'êtes pas familier avec l'ouverture de l'invité de commandes, procédez comme suit :

  • Ouvrez le dossier du projet du bot, que vous venez de créer.
  • Maintenez la touche Maj enfoncée et cliquez avec le bouton droit à l'intérieur du dossier.
  • Vous devriez avoir une option Ouvrir la fenêtre PowerShell ici (ou Ouvrir une fenêtre de commandes ici sur les versions anciennes de Windows), cliquez dessus pour ouvrir l'interpréteur de commande.

Devrait alors s'ouvrir une fenêtre, avec un fond noir ou bleu. Pour commencer, entrez la commande node -v. Elle doit vous afficher la version de Node.js installée sur votre ordinateur.

Si vous voyez quelque chose comme v12.0, super ! Sinon revenez en arrière et réessayez d'installer Node.js.

Initialisation du projet

Maintenant que vous savez manier la ligne de commande, nous allons pouvoir initialiser le projet avec npm puis installer les dépendances dont nous avons besoin pour l'instant :

  • discord.js
  • discord.js-commando

Initialisons le projet avec npm :

npm init -y

Cette commande permet d'initialiser un fichier package.json à la racine du projet. Ce fichier est très important, il va contenir la liste des dépendances nécessaires au fonctionnement de notre code.

Voici le fichier qui a été généré suite à ma commande:

{
  "name": "bot",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

Nous pouvons désormais utiliser le gestionnaire de dépendance pour installer les librairies, avec la commande npm install <NOMDUPAQUET>.

On commence par l'installation de discord.js:

npm install --save discord.js

Si vous voyez des WARN, ne vous affolez pas pour autant : installation discord.js

Et maintenant, on installe le framework Commando :

npm install --save https://github.com/discordjs/Commando.git

Les librairies sont installées, il ne nous reste plus qu'une étape avant de pouvoir nous lancer dans la création du bot Discord, la création de l'application sur le site de Discord, qui sera associée à notre bot discord.js.

Vous pouvez également constater la présence d'un nouveau dossier node_modules à la racine du projet. Ce dossier est géré par npm, vous ne devez jamais modifier son contenu.

Création de l'application développeur

Nous sommes presque prêt à passer au développement du bot. Pour le démarrer, vous aurez besoin d'un token. Pour obtenir ce token, vous devez créer une application sur le site Discord développer.

  1. Rendez-vous sur le site de Discord et connectez-vous.
  2. Ouvrez le portail des développeurs Discord.
  3. Cliquez sur New Application.
  4. Entrez le nom du bot (exemple mTxBot) et confirmez la création en cliquant sur le bouton Créer.

Vous pourrez également fournir une description et une image de profil (non obligatoire) à votre bot, après création de l'application : nouvelle application

Déplacez vous maintenant dans l'onglet Bot, nous allons déclarer l'application comme étant un bot :

  1. Cliquez sur Add Bot.
  2. Confirmez en cliquant sur Yes, do it!.

Une fois cela fait, vous pouvez modifier si vous le souhaitez le nom d'utilisateur du bot.

Tant que votre bot est en cours de développement, ou tout simplement si vous ne souhaitez pas le partager avec d'autres, décochez la case Public bot, active par défaut.

Public bot

Il ne nous reste plus qu'à récupérer le token pour notre bot et nous pourrons commencer à coder le bot : token bot discord

Gardez le token de côté pour plus tard. Gardez tout de même à l'esprit que ce token permet un accès total à votre bot, il ne doit jamais être diffusé.

Inviter le bot sur son serveur

C'est bien beau de développer votre propre bot, mais s'il n'est pas sur votre serveur Discord, cela ne vous avancera à rien ^^

Pour inviter le bot, vous devez générer un lien d'invitation dans le portail des développeurs :

Inviter un bot discord

  1. Cliquez sur l'onglet OAuth2.
  2. Dans la section scopes de OAuth2 URL Generator, sélectionnez bot.
  3. Une nouvelle section Bot Permissions doit apparaître juste en dessous, cliquez sur Administrator dedans.
  4. Cliquez sur Copy dans scopes pour récupérer le lien d'invitation du bot.
  5. Ouvrez le lien que vous venez de copier dans votre navigateur.
  6. Sélectionnez le serveur discord sur lequel ajouter votre bot.
  7. Validez les permissions en cliquant sur Autoriser.

Plus tard, si vous souhaitez diffuser votre bot, vous n'aurez à partager le lien d'invitation (du type https://discord.com/api/oauth2/authorize?client_id=773938386171723796&permissions=8&scope=bot).

Et voila, le bot est présent sur votre serveur, pas si difficile ;) Vous pouvez faire chauffer l'IDE !

Mon premier bot avec discord.js

Commencez par lancer votre IDE, Visual Studio Code par exemple sur Windows.

Créez un premier fichier bot.js dans le répertoire racine du projet, avec le contenu suivant:

const Discord = require('discord.js'); // Chargement de la librairie discord.js

const client = new Discord.Client();  // Création du bot

client.on('ready', () => {
    console.log(`Je suis prêt !`); // On affiche un message de log dans la console (ligne de commande), lorsque le bot est démarré
});

client.on('error', console.error); // Afficher les erreurs

// Lancement du bot, avec le token spécifié (que vous avez généré précédemment)
client.login('VOTRE_TOKEN');

Pour lancer le bot, entrez dans la ligne de commande :

node bot.js

Si vous avez fait une erreur sur le token, vous aurez un message d'erreur contenant Error [TOKEN_INVALID]: An invalid token was provided..

Si tout va bien, votre bot va démarrer et afficher:

Je suis prêt !

Pour arrêter l’exécution du bot, utilisez les touches CTRL + C.

Votre tout premier bot est fonctionnel, mais pas encore très utile ^^

Discord Commando

Nous venons de créer notre premier bot, mais il n'est pas très utile en l'état. Avant de nous lancer dans la création de commandes, nous allons ajouter le framework Commando.

Commando est le framework officiel de développement pour discord.js. Il est flexible, utilise une programmation objet pour un meilleur découplage et maintenabilité de votre bot, et est facile à utiliser. En outre, il utilise pleinement la fonctionnalité async d'ES2017 pour obtenir un code clair et concis, simple à écrire et facile à comprendre.

La documentation est accessible ici.

Création du client Commando

Pour initialiser le framework, voici ce que dit la documentation pour la configuration minimale :

const { CommandoClient } = require('discord.js-commando');

const client = new CommandoClient({
	commandPrefix: '?', // Préfixe des commandes (ex: ?help)
	owner: 'VOTRE_ID_UTILISATEUR', // ID de l'owner du bot, peut également etre un tableau d'id pour plusieurs owners, ex: ['ID1', 'ID2']
        disableMentions: 'everyone' // Désactive, par sécurité, l'utilisation du everyone par le bot
});

client.registry
    .registerDefaultTypes()
    .registerGroups([])
;

Premiers éléments qui peuvent retenir votre attention :

  • le préfixe des commandes est passé dans les options. Avec un préfixe ?, les commandes devront être précédées de ce préfixe, par exemple m!help avec un préfixe m!.
  • l'id de l'owner, traduisible par l'id du propriétaire du bot (le votre), par exemple 515178853967331321.
  • disableMentions qui désactive, par sécurité, le everyone (voir plus bas).

Pour trouver votre ID OWNER (vous devez avoir le mode développeur activé) :

trouver son identifiant discord

  1. Allez dans les paramètres de votre compte Discord.
  2. Cliquez sur ....
  3. Cliquez sur Copier l'identifiant.

Maintenant que nous avons notre identifiant, adaptons notre code. Le client provenant de Commando, qui est plus évolué, va prendre le relais sur celui de discord.js :

const { CommandoClient } = require('discord.js-commando'); // Chargement du client de Discord Commando

const client = new CommandoClient({
	commandPrefix: '?', // Préfixe des commandes (ex: ?help)
	owner: 'VOTRE_ID_UTILISATEUR', // ID de l'owner du bot, peut également être un tableau d'id pour plusieurs owners, ex: ['ID1', 'ID2']
        disableMentions: 'everyone' // Désactive, par sécurité, l'utilisation du everyone par le bot
});

client.registry
    .registerDefaultTypes()
    .registerGroups([])
;

client.once('ready', () => {
    console.log(`Je suis prêt !`);
});

client.on('error', console.error); // Afficher les erreurs

client.login('VOTRE_TOKEN');

Vérifions que tout va bien en lançant le bot, entrez dans la ligne de commande :

node bot.js

Vous devriez voir, comme tout à l'heure, le message suivant. La différence étant que cette fois-ci, c'est Commando que nous utilisons. Vous aller voir tout son intérêt lors de la création de commandes !

Je suis prêt !

Avant de passer à la suite, sachez que la section registerGroups permettra par la suite de créer des groupes de commandes. Nous verrons sont utilisation avec la création de notre première commande, et plus tard son utilité avec la commande d'aide du bot que nous développerons.

Notez également qu'il existe de nombreuses options que vous pouvez passer au client Commando, dont :

  • disableMentions - Désactive l'utilisation de mentions dans les messages (valeurs possibles: none, all et everyone).
  • fetchAllMembers - Forcer la récupération de tous les membres de discord, par défaut false. A activer que si vous en avez réellement besoin.
  • presence - Permet de définir un message de présence pour le bot. Nous verrons ce point en détail plus tard.
  • invite - Lien d'invitation vers le serveur Discord principal du bot (ex: https://discord.gg/mtxserv).

Il peut souvent être utile de pouvoir modifier le client dans nos développement. Pour ajouter de la flexibilité à notre code, nous allons utiliser les avantages de la programme objet, en créant une nouvelle classe qui représentera notre client (et qui sera facilement modifiable), tout en héritant de celle de Commando. C'est une bonne pratique, à suivre absolument.

Créez un fichier client.js, et copiez le contenu suivant :

const { CommandoClient } = require('discord.js-commando')

module.exports = class BotClient extends CommandoClient {
    constructor(options) {
        super(options);
    }
};

Nous n'aurons par la suite qu'à modifier cette classe et ajouter notre code, pour personnaliser et factoriser facilement du code.

Pour utiliser notre nouveau client, éditez maintenant le fichier bot.js. Cherchez :

const { CommandoClient } = require('discord.js-commando');

et remplacez cette ligne par celle-ci pour utiliser notre nouveau client :

const CommandoClient = require('./client');

Vous devriez avoir :

const CommandoClient = require('./client');

const client = new CommandoClient({
	commandPrefix: '?', // Préfixe des commandes (ex: ?help)
	owner: 'VOTRE_ID_UTILISATEUR', // ID de l'owner du bot, peut également être un tableau d'id pour plusieurs owners, ex: ['ID1', 'ID2']
        disableMentions: 'everyone' // Désactive, par sécurité, l'utilisation du everyone par le bot
});

client.registry
    .registerDefaultTypes()
    .registerGroups([])
;

client.once('ready', () => {
    console.log(`Je suis prêt !`);
});

client.on('error', console.error); // Afficher les erreurs

client.login('VOTRE_TOKEN');

Et bien sur, on vérifie le bon fonctionnement de notre code :

node bot.js

A ce stade, on consolide surtout la base de notre code, pour créer un bot robuste, qui pourra faire tout ce qu'on veut.

Avant de passer à la suite, vous pouvez vérifier votre code, nous avons ajouté le code ici: mTxServ/discord.js-1-client-commando

Les commandes

Nous allons passer à la création de notre première commande avec Commando.

Chaque commande qui sera disponible sur le bot va posséder son propre fichier. Là encore, cela va faciliter le découplage et la maintenabilité de votre bot Discord.

Pour commencer, créez un répertoire commands à la racine du projet. Vous devez donc avoir la structure :

commands/
bot.js
client.js

Nous devons maintenant indiquer au framework de charger automatiquement les commandes qui seront dans notre répertoire commands. Pour cela, éditez le fichier bot.js, nous allons faire appel à registerCommandsIn pour indiquer l'emplacement des commandes, à la chaîne des autres appels sur client.registry :

client.registry
    .registerDefaultTypes()
    .registerGroups([])
    .registerCommandsIn(path.join(__dirname, 'commands'))
;

Pour que cela fonctionne, ajoutez const path = require('path') en haut du fichier, sinon vous aurez une erreur. Voici le code final du bot.js après modification :

const CommandoClient = require('./client');
const path = require('path') // on ajoute la librairie path

const client = new CommandoClient({
	commandPrefix: '?',
	owner: 'VOTRE_ID_UTILISATEUR',
        disableMentions: 'everyone'
});

client.registry
    .registerDefaultTypes()
    .registerGroups([])
    .registerCommandsIn(path.join(__dirname, 'commands')) // on indique où seront les fichiers des commandes du bot
;

client.once('ready', () => {
    console.log(`Je suis prêt !`);
});

client.on('error', console.error);

client.login('VOTRE_TOKEN');

Nous pouvons maintenant ajouter notre première commande, qui va simplement répondre avec un message de salutation.

Sachez qu'une commande doit obligatoirement être dans un sous-dossier du répertoire commands/, sinon elle ne sera pas détectée, par exemple commands/divers/.

Dans le répertoire commands/divers/, créez un fichier hello.js avec le contenu suivant :

const { Command } = require('discord.js-commando');

module.exports = class HelloCommand extends Command {
	constructor(client) {
		super(client, {
			name: 'hello',
			memberName: 'hello',
			group: 'divers',
			aliases: ['bonjour', 'hi'],
			description: 'Replies with a hello message.',
	                guildOnly: false,
	                throttling: {
	                        usages: 2,
	                        duration: 10,
	                },
		});
	}

	async run(msg) {
            msg.say(`Bonjour, je suis ${this.client.user.tag} (\`${this.client.user.id}\`)`);
	}
};

Vous remarquerez tout de suite la méthode run qui contient le code qui sera exécuté lors de l'appel à notre commande sur Discord. Vous pouvez accéder à l'API discord.js, qui permet d’interagir avec Discord, dans cette méthode. Pour accéder à l'API, utilisez this.client.

La méthode runpossède comme premier argument le message de l'utilisateur qui a déclenché la commande. Vous pouvez voir la structure de cet objet ici.

Regardons maintenant de plus prêt le constructor, qui permet de déclarer une nouvelle commande dans le bot et ses options :

  • name qui représente le nom de la commande, qu'il faudra taper sur Discord.
  • memberName est le nom de la commande qui sera utilisé dans le groupe. Indiquez la même chose que dans name.
  • group est le nom du groupe associé à la commande.
  • aliases (facultatif) permet de déclarer des alias à une commande.
  • description contient une description de la commande, utilisé par la commande d'aide du bot.
  • guildOnly (facultatif) permet de limiter l'utilisation de la commande (le bot accepte par défaut les commandes en message privé ainsi que sur les channels). Si true, la commande ne fonctionnera pas en message privé (par défaut à false).
  • throttling (facultatif) permet d'activer la protection anti-flood du bot, pour éviter un spam des utilisateurs (sachez que même si elle est active, elle ne s'appliquera pas au propriétaire du bot), en limitant le nombre d'appel à la commande sur une intervalle de temps (ex: l'utilisateur ne peut utiliser la commande que 2 fois toutes les 10 secondes).

Nous avons déjà vu l'existence d'une notion de groupe, avec l'appel à la méthode registerGroups dans le fichier bot.js. Vous pouvez voir les groupes comme des catégories, regroupant une ou plusieurs commandes. Ils seront notamment utilisés pour la future commande d'aide de notre bot, qui regroupera les commandes par groupe dans son affichage.

Une commande doit obligatoirement être associée à un groupe. Chaque groupe doit préalablement avoir été déclaré. Vous l'aurez compris, à chaque fois que utilisez un nouveau groupe, il ne faudra pas oublier de le déclarer au préalable.

Déclarons notre groupe divers en éditant le fichier bot.js. Modifiez la ligne .registerGroups([]) et ajoutez votre nouveau groupe, dans notre exemple divers :

    .registerGroups([
        ['divers', 'Divers'], // la première valeur correspond à la section 'group' de votre commande, la deuxième valeur sera utilisée pour l'affichage du nom du groupe, dans l'aide par exemple.
    ])

Vous devez donc avoir :

const CommandoClient = require('./client');
const path = require('path') // on ajoute la librairie path

const client = new CommandoClient({
	commandPrefix: '?',
	owner: 'VOTRE_ID_UTILISATEUR',
        disableMentions: 'everyone'
});

client.registry
    .registerDefaultTypes()
    .registerGroups([
        ['divers', 'Divers'],
    ])
    .registerCommandsIn(path.join(__dirname, 'commands')) // on indique où seront les fichiers des commandes du bot
;

client.once('ready', () => {
    console.log(`Je suis prêt !`);
});

client.on('error', console.error);

client.login('VOTRE_TOKEN');

Notre commande est prête, vous pouvez lancer le bot :

node bot.js

Envoyez maintenant le message ?hello (en privé au bot ou sur un des channels du serveur Discord). Le bot doit vous répondre Bonjour !.

Si vous avez mal configuré le groupe de la commande, vous aurez un message :

Error: Group "NOM DU GROUPE" is not registered.

Dans ce cas, revenez en arrière et corrigez votre code ;)

Nous avons publié le code source sur GitHub, vous pouvez vérifier votre code ici.

Les permissions

Discord possède un système de permissions très puissant, qu'il faut savoir manipuler pour réaliser des commandes complexes. Pour résumer en quelques mots, elles permettent de définir qui est autorisé à faire quoi et où (= dans quel channel).

Dans chacune de nos commandes, nous pouvons indiquer facilement :

  • de quelle permission à besoin le bot pour exécuter la commande.
  • de quelle permission à besoin l'utilisateur pour exécuter la commande.

Pour indiquer les permissions requises, nous allons modifier le constructeur de notre commande. Il existe deux options qui vont nous être utiles, clientPermissions et userPermissions, chacune facultative.

  • clientPermissions (facultatif) représente les permissions dont le bot à besoin pour exécuter la commande.
  • userPermissions (facultatif) représente les permissions dont l’utilisateur qui a tapé la commande à besoin pour qu'il soit autorisé à l'utiliser.

Les permissions disponibles sont :

  • ADMINISTRATOR
  • CREATE_INSTANT_INVITE
  • KICK_MEMBERS
  • BAN_MEMBERS
  • MANAGE_CHANNELS
  • MANAGE_GUILD
  • ADD_REACTIONS
  • VIEW_AUDIT_LOG
  • PRIORITY_SPEAKER
  • STREAM
  • VIEW_CHANNEL,
  • SEND_MESSAGES
  • SEND_TTS_MESSAGES
  • MANAGE_MESSAGES
  • EMBED_LINKS
  • ATTACH_FILES
  • READ_MESSAGE_HISTORY
  • MENTION_EVERYONE
  • USE_EXTERNAL_EMOJIS
  • VIEW_GUILD_INSIGHTS
  • CONNECT
  • SPEAK
  • MUTE_MEMBERS
  • DEAFEN_MEMBERS
  • MOVE_MEMBERS
  • USE_VAD
  • CHANGE_NICKNAME
  • MANAGE_NICKNAMES
  • MANAGE_ROLES
  • MANAGE_WEBHOOKS
  • MANAGE_EMOJIS

Pour notre commande, nous allons demander à ce que l'utilisateur soit administrateur du serveur et vérifier que le bot possède bien les droits pour répondre dans le channel :

clientPermissions: ['SEND_MESSAGES'], // le bot doit avoir la permission d'envoyer des messages
userPermissions: ['ADMINISTRATOR'], // l'utilisateur doit être administrateur pour exécuter la commande

Modifions notre code :

const { Command } = require('discord.js-commando');

module.exports = class HelloCommand extends Command {
	constructor(client) {
		super(client, {
			name: 'hello',
			memberName: 'hello',
			group: 'divers',
			aliases: ['bonjour', 'hi'],
			description: 'Replies with a hello message.',
			clientPermissions: ['SEND_MESSAGES'], // le bot doit avoir la permission d'envoyer des messages
			userPermissions: ['ADMINISTRATOR'], // l'utilisateur doit être administrateur pour exécuter la commande
	                guildOnly: true,
	                throttling: {
	                        usages: 2,
	                        duration: 10,
	                },
		});
	}

	async run(msg) {
                msg.say(`Bonjour, je suis ${this.client.user.tag} (\`${this.client.user.id}\`)`);
	}
};

Si vous voulez restreindre une commande au propriétaire du bot (vous), il existe une option ownerOnly que vous pouvez passer à true. Seul le propriétaire du bot alors exécuter la commande :

const { Command } = require('discord.js-commando');

module.exports = class HelloCommand extends Command {
	constructor(client) {
		super(client, {
			name: 'hello',
			memberName: 'hello',
			group: 'divers',
			description: 'Replies with a hello message.',
	                ownerOnly: true, // Uniquement le propriétaire du bot pourra lancer cette commande
		});
	}

	async run(msg) {
                msg.say(`Bonjour, je suis ${this.client.user.tag} (\`${this.client.user.id}\`)`);
	}
};

Vous constatez ici un des avantages de l'utilisation de Commando, vous pouvez très facilement créer et configurer vos commandes, avec très peu de code à créer.

Le code source est disponible ici.

Les arguments

Dans certaines de vos commandes vous aurez besoin d’interagir avec l'utilisateur pour obtenir des données et adapter la réponse en conséquence. Commando nous aide dans ce processus, avec une gestion des arguments.

Un argument est associé à un nom (clé), un message et un type de donnée (chaîne de caractère, nombre, channel discord, utilisateur discord, etc). Vous pouvez également associer un validateur, qui vérifiera que ce qu'à répondu l'utilisateur est conforme à vos attentes (par exemple, si vous attendez un lien, vous pouvez vérifier facilement que l'utilisateur n'a pas répondu autre chose qu'un lien).

Pour déclarer des arguments, utilisez l'option args du constructeur :

args: [
	{
		key: 'text',
		prompt: 'Quel texte voulez-vous que le bot répondre ?',
		type: 'string',
	},
],

Les options de chaque argument sont :

  • key - le nom de la variable qui sera utilisé pour récupérer la donnée entrée par l'utilisateur, dans la méthode run.
  • prompt- le message affiché à l'utilisateur pour lui demander de rentrer la valeur.
  • type - le type de donnée, peut-être: string, integer, float, boolean, user, member, role, channel, message.
  • default (facultatif) - valeur par défaut à utiliser si l'utilisateur ne rentre aucune donnée.
  • validate (facultatif) - fonction de validation de la donnée rentrée par l'utilisateur. Si elle renvoie false, alors la donnée entrée par l'utilisateur sera considérée comme invalide.

Pour récupérer un argument dans la méthode run, vous devez utiliser sa key, dans notre exemple :

async run(msg, { text }) {
    msg.say(`Votre text: ${text}`);
}

Si vous aviez plusieurs arguments, il faudrait les lister, par exemple si nous avions eu un deuxième argument link :

async run(msg, { text, link }) {
    // ...
}

Créez une nouvelle commande, cette fois-ci avec argument, dans le fichier commands/divers/reply.js :

const { Command } = require('discord.js-commando');

module.exports = class ReplyCommand extends Command {
	constructor(client) {
		super(client, {
			name: 'reply',
			memberName: 'reply',
			group: 'divers',
			description: 'Reply.',
			clientPermissions: ['SEND_MESSAGES'], // le bot doit avoir la permission d'envoyer des messages
	                throttling: {
	                        usages: 2,
	                        duration: 10,
	                },
	                args: [
			        {
			                key: 'text',
			                prompt: 'Quel texte voulez-vous que le bot répondre ?',
			                type: 'string',
			        },
	                ],
		});
	}

	async run(msg, { text }) {
		msg.say(`Votre texte est: ${text}`);
	}
};

On va va maintenant tester notre nouvelle commande, lancez le bot :

node bot.js

Tapez maintenant la commande ?reply dans un de vos channels ou en message privé au bot. Il devrait vous demander d'entrer un texte et afficher le message que vous avez entré.

Vous pouvez vérifier votre code actuel en le comparant avec le notre, disponible ici.

Avant de finir avec les arguments, voici quelques exemples de validateurs personnalisées (option validate) :

Vérifier la taille de la chaîne de caractère :

{
    key: 'question',
    prompt: 'Quelle est la question ?',
    type: 'string',
    validate: question => {
        if (question.length < 101 && question.length > 11) return true;
        return 'La question doit avoir au minimum 10 caractères, et maximum 100.';
    }
},

Valider une URL :

{
    key: 'url',
    prompt: 'Which image url do you want to share?',
    type: 'string',
    validate: url => {
        return /^(?:(?:(?:https?|http):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))(?::\d{2,5})?(?:[/?#]\S*)?$/i.test(url);
    }
},

Valider une URL du workshop STEAM :

{
    key: 'url',
    prompt: 'Quelle URL du workshop STEAM ?',
    type: 'string',
    validate: url => {
        return /^https:\/\/steamcommunity.com\/sharedfiles\/filedetails\/\?id=[0-9]{2,15}(&.+)?$/i.test(url);
    }
},

Vous trouverez un exemple de validation personnalisée ici.

Utilisation d'un logger

Pour que notre code soit robuste, nous devons ajouter un système permettant de déboguer et afficher des informations facilement dans la console, qui permettra également d'avoir des logs une fois le bot finalisé et en production (par exemple dans un fichier console.log).

Avec npm, il est très facile d'ajouter un tel système de logging dans son bot, nous aurions tord de nous en priver.

Le paquet winston conviendra à vos besoins, quelques fonctionnalités :

  • gestion de fichiers de log (ex: bot.log).
  • niveaux de priorité (error, warn, info, http, debug, ..).
  • affichage amélioré des lignes de logs, dans la ligne de commande.
  • conçu pour la performance!

Pour l'ajouter dans le projet, tapez dans la ligne de commande :

npm install --save winston

Maintenant, utilisons le dans notre code. Pour initialiser Winston :

const winston = require('winston');

client.logger = winston.createLogger({
  transports: [
    new winston.transports.Console(),
    new winston.transports.File({ filename: 'log' })
  ],
  format: winston.format.printf((log) => `[${new Date().toLocaleString()}] - [${log.level.toUpperCase()}] - ${log.message}`)
});

client.on('ready', () => client.logger.log('info', 'The bot is online!'));
client.on('debug', m => client.logger.log('debug', m));
client.on('warn', m => client.logger.log('warn', m));
client.on('error', m => client.logger.log('error', m));

process.on('uncaughtException', error => client.logger.log('error', error));

Puisque nous avons créer notre classe client.js, on va ajoute le code dedans, cela nous permettra aussi d'y accéder ultérieurement, depuis n'importe où dans notre code.

Après édition du fichier client.js, vous devriez donc avoir :

const { CommandoClient } = require('discord.js-commando')
const winston = require('winston')

module.exports = class BotClient extends CommandoClient {
    constructor(options) {
        super(options);

        // initialisation du logger
        this.logger = winston.createLogger({
            transports: [
                new winston.transports.Console(),
                new winston.transports.File({ filename: 'console.log' })
            ],
            format: winston.format.printf((log) => `[${new Date().toLocaleString()}] - [${log.level.toUpperCase()}] - ${log.message}`)
        });

        this.on('ready', () => this.logger.log('info', `Je suis prêt !`));
        this.on('debug', m => this.logger.log('debug', m));
        this.on('warn', m => this.logger.log('warn', m));
        this.on('error', m => this.logger.log('error', m));

        process.on('uncaughtException', error => this.logger.log('error', error));
    }
};

Vous vous aussi supprimer l'ancienne gestion des erreurs, présente dans bot.js. Supprimez les lignes :

client.once('ready', () => {
    console.log(`Je suis prêt !`);
});

client.on('error', console.error);

Il ne nous reste plus qu'a vérifier que le bot fonctionne avec notre logger :

node bot.js

Vous devriez voir :

[2020-11-6 13:28:44] - [INFO] - Je suis prêt !

Pour faire appel au logger depuis une commande :

async run(msg) {
   this.client.logger.log('info', 'Un nouveau membre est là');
   this.client.logger.log('warn', 'Attention !!');
   this.client.logger.log('error', 'Une erreur est survenue :(');
}

Pour tester, créez une commande commands/divers/logger.js avec :

    const { Command } = require('discord.js-commando');
    
    module.exports = class TestLoggerCommand extends Command {
        constructor(client) {
            super(client, {
                name: 'logger',
                memberName: 'logger',
                group: 'divers',
                description: 'Test the logger.',
            });
        }
    
        async run(msg) {
            this.client.logger.log('info', 'Un nouveau membre est là');
            this.client.logger.log('warn', 'Attention !!');
            this.client.logger.log('error', 'Une erreur est survenue :(');
        }
    };

Lancez le bot et testez en lui envoyer la commande ?logger. Vous devriez voir cela dans la ligne de commande :

[2020-11-6 13:35:49] - [INFO] - Je suis prêt !
[2020-11-6 13:35:53] - [INFO] - Un nouveau membre est là
[2020-11-6 13:35:53] - [WARN] - Attention !!
[2020-11-6 13:35:53] - [ERROR] - Une erreur est survenue :(

Les logs sont maintenant automatiquement ajoutés dans le fichier console.log à la racine du projet, en plus d'être affichés dans la ligne de commande.

Vous pouvez vérifier votre code à ce stade, avec le code disponible ici.

Vous voila en bonne route pour la création d'un code robuste, fiable, flexible... et maintenable !!

Configuration & variables d'environnement

Vous en avez marre de devoir remplacer l'user id et le token à chaque fois que vous copiez un code de ce tutoriel ? Ce chapitre va retenir votre attention, nous allons mettre en place un système de configuration pour notre bot, avec les variables d'environnement.

Nous aurons ainsi un fichier .env à la racine du projet, qui contiendra les variables de configuration globales du bot (token, owner id, etc). Une bonne pratique pour un bot robuste et maintenable, surtout si vous collaborez à plusieurs dans le développement de votre bot. Cela va également vous être utile une fois le bot en production, pour paramétrer différemment votre bot en développement, et en production.

Voici un exemple de fichier de configuration d'environnement.

Pour mettre en place notre système de configuration, nous allons utiliser le paquet dotenv. Première étape, l'installer :

npm install --save dotenv

Créons maintenant le fichier de configuration .env à la racine du projet, il va contenir notre token et notre owner id. Ce fichier ne doit jamais être partagé, il contient votre token.

BOT_TOKEN=VOTRE_TOKEN
BOT_OWNER_ID=VOTRE_USER_ID

Vous avez créé le fichier et remplacé les paramètres token et user_id par les vôtres ? On peut passer à la suite, la prise en compte du fichier dans notre code. Ces paramètres sont utilisés dans le fichier bot.js, c'est donc ce fichier que nous allons devoir modifier.

Si on regarde la documentation du paquet dotenv, nous devons ajouter dans notre code :

const fs = require('fs')
const dotenv = require('dotenv')

const envConfig = dotenv.parse(fs.readFileSync('.env'))
for (const k in envConfig) {
    process.env[k] = envConfig[k]
}

On pourra ensuite accéder à nos variables de configuration partout, avec process qui est accessible dans tous vos scripts, Il suffit de lui passer la clé de configuration que vous voulez récupérer :

console.log( process.env.BOT_TOKEN )
console.log( process.env.BOT_OWNER_ID )

Adaptons notre fichier bot.js, cela donne :

const CommandoClient = require('./client');
const path = require('path');
const fs = require('fs')
const dotenv = require('dotenv')

const envConfig = dotenv.parse(fs.readFileSync('.env'))
for (const k in envConfig) {
    process.env[k] = envConfig[k]
}

const client = new CommandoClient({
    commandPrefix: '?',
    owner: process.env.BOT_OWNER_ID,
    disableMentions: 'everyone'
});

client.registry
    .registerDefaultTypes()
    .registerGroups([
        ['divers', 'Divers'],
    ])
    .registerCommandsIn(path.join(__dirname, 'commands'))
;

client.login(process.env.BOT_TOKEN);

Il ne vous reste plus qu'à tester, si vous avez tout bien fait, le bot devrait se lancer :

node bot.js

Si tout va bien, vous verrez toujours le message :

[2020-11-6 14:18:07] - [INFO] - Je suis prêt !

Vous pouvez vérifier votre code avec le notre, disponible ici.

Discord.js

Jusqu'à présent nous avons surtout manipulé Commando. Nous allons désormais prêter attention à la librairie discord.js qui permet de communiquer avec Discord. C'est elle qui va nous permettre d'étoffer nos méthodes run(), dans nos commandes.

Pour vous présenter cette librairie, qui présente une quantité importante de fonctionnalités, nous allons nous concentrer sur les fonctions essentielles.

Message embed

Jusqu'à présent, les messages que nous avons envoyés avec le bot sont fades et contiennent un simple texte. Un grand avantage de l'utilisation d'un bot sur son serveur Discord est de pouvoir créer des messages spéciaux, appelés embed, bien plus agréables à l’œil de vos utilisateurs !

Ils peuvent avoir une bordure colorée, des images incorporées, des champs de texte et d'autres propriétés de mise en page. Voici un exemple d'embed :

discord.js embed

Pour créer un embed, vous devez instancier un nouvel objet de la classe MessageEmbed :

const Discord = require('discord.js');

const embed = new Discord.MessageEmbed(); // création de l'embed

Vous allez ensuite pouvoir le personnaliser avec l'utilisation de setters (ex: setTitle()) :

const Discord = require('discord.js');

const embed = new Discord.MessageEmbed(); // création de l'embed

embed
    .setColor(`BLUE`) // ou .setColor(`#0099ff`)
    .setTitle(`Titre du message, maximum 256 caractères`)

    // .setAuthor(`Nom de l'auteur`, `https://mtxserv.com/build/img/favicon/favicon.ico`, `https://mtxserv.com/fr/`)
    .setAuthor(`${this.client.user.tag}`, `${this.client.user.displayAvatarURL()}`, 'https://mtxserv.com/fr/')

    .setDescription(`Message contenu dans l'embed, maximum 2048 caractères`)
    .setFooter(`Pied de page du message`, `${this.client.user.displayAvatarURL()}`)

    .setImage(`https://mtxserv.com/uploads/cover/creer-un-bot-discord-avec-discord-js-discord-191c77d00c4d79bf822422d6a05496bd.jpg`)
    .setThumbnail(`https://mtxserv.com/uploads/banners/ae49ad104085151cbb44e27fffd9f16862cb6f2c.png`)

    .setTimestamp() // Vous pouvez passer un objet Date() en argument

    // Fields

    // Sur une ligne complète :
    .addField(`Titre, maximum 256 caractères`,`Votre texte, maximum 1024 caractères`)

    // Plusieurs sur une même ligne :
    .addField(`Titre 1`,`Votre texte 1`, true)
    .addField(`Titre 2`,`Text avec un [lien](https://mtxserv.com/fr/)`, true)
;

A noter que la plupart des setters (ex: setTimestamp()) sont facultatifs.

Pour envoyer le message, vous devez remplacer vos appels à la méthode msg.say() :

msg.say(`Bonjour!`);

Le code va devenir pour un message embed :

msg.say(embed);

Nous avons le nécessaire pour créer notre première commande avec embed. Créez un fichier commands/divers/embed.js avec le contenu suivant :

const { Command } = require('discord.js-commando');
const Discord = require('discord.js');

module.exports = class EmbedCommand extends Command {
    constructor(client) {
        super(client, {
            name: 'embed',
            memberName: 'embed',
            group: 'divers',
            description: 'Send an embed message.',
            ownerOnly: true,
            clientPermissions: ['SEND_MESSAGES', 'EMBED_LINKS'], // le bot doit avoir la permission d'envoyer des messages
            throttling: {
                usages: 2,
                duration: 10,
            },
        });
    }

    async run(msg) {
        const embed = new Discord.MessageEmbed(); // création de l'embed

        embed
            .setColor(`BLUE`) // ou .setColor(`#0099ff`)
            .setTitle(`Titre du message, maximum 256 caractères`)

            // .setAuthor(`Nom de l'auteur`, `https://mtxserv.com/build/img/favicon/favicon.ico`, `https://mtxserv.com/fr/`)
            .setAuthor(`${this.client.user.tag}`, `${this.client.user.displayAvatarURL()}`, 'https://mtxserv.com/fr/')

            .setDescription(`Message contenu dans l'embed, maximum 2048 caractères`)
            .setFooter(`Pied de page du message`, `${this.client.user.displayAvatarURL()}`)

            .setImage(`https://mtxserv.com/uploads/cover/creer-un-bot-discord-avec-discord-js-discord-191c77d00c4d79bf822422d6a05496bd.jpg`)
            .setThumbnail(`https://mtxserv.com/uploads/banners/ae49ad104085151cbb44e27fffd9f16862cb6f2c.png`)

            .setTimestamp() // Vous pouvez passer un objet Date() en argument

            // Fields

            // Sur une ligne complète :
            .addField(`Titre, maximum 256 caractères`,`Votre texte, maximum 1024 caractères`)

            // Plusieurs sur une même ligne :
            .addField(`Titre 1`,`Votre texte 1`, true)
            .addField(`Titre 2`,`Text avec un [lien](https://mtxserv.com/fr/)`, true)
        ;

        msg.say(embed)
    }
};

Il ne vous reste plus qu'à redémarrer le bot. Envoyez ensuite la commande ?embed dans un channel, vous devriez voir votre premier message embed ;)

Sachez tout de même qu'il y a quelques limites à connaître en raison des limitations de Discord :

  • Les titres sont limités à 256 caractères.
  • Les descriptions sont limitées à 2048 caractères.
  • Il peut y avoir jusqu'à 25 champs (addField()).
  • Le nom d'un champ est limité à 256 caractères et sa valeur à 1024 caractères.
  • Le texte du pied de page est limité à 2048 caractères.
  • Le nom de l'auteur est limité à 256 caractères.
  • La somme de tous les caractères d'un embed ne doit pas dépasser 6000 caractères.
  • Un bot ne peut envoyer qu'un embed par message.
  • Un webhook peut contenir 10 embeds par message.

Pour vous aider, vous pouvez utiliser ce générateur.

Vous pouvez vérifier votre code avec le notre, disponible ici.

Exemples d'embeds

Vous savez désormais comment créer des messages agréables pour vos utilisateurs. Voici quelques exemples dont vous pouvez vous inspirer :

bot statut

feed

articles

embed

embed

Les collections

Maintenant que vous avez tout pour créer vos propres commandes, intéressons nous aux collections et à l’interaction avec l'API Discord, grâce à la librairie discord.js. Cette partie est plus théorique, mais très importante pour la création de commandes évoluées.

Avant tout, vous devez comprendre ce qu'est une collection et son intérêt pour nos développements. Une collection (= une liste, un tableau) est représentée par une classe, son instance permet de récupérer, stocker et manipuler des données provenant de Discord.

Cette classe expose des méthodes utilitaires comme map(), get(), has(), array(), toJSON(), filter(), mapValues(), et bien d'autres pour aider le développeur.

Elle est optimisée pour la performance grâce à une mise en cache des données transparente pour le développeur. Vous pouvez voir la liste de toutes les méthodes sur la documentation officielle.

Pour accéder aux collections, vous devez utiliser le client ou une instance d'un message. Depuis la méthode run(msg) de vos commandes, le client est accessible avec this.client.

Quelques collections que vous rencontrerez :

  • users - Collection UserManager représentant les utilisateurs Discord.
  • guilds - Collection GuildManager représentant les serveurs Discord sur lequel le bot est présent.
  • members - Collection GuildMemberManager représentant les utilisateurs d'un serveur (= guild) Discord.
  • channels - Collection ChannelManager représentant les channels d'un serveur Discord.
  • roles - Collection RoleManager représentant les rôles d'un serveur (= guild) Discord.
  • émojis - Collection GuildEmojiManager représentant les émojis d'un serveur (= guild) Discord.

Depuis la v12 de discord.js, un nouveau système de cache est en place. Vous devez utiliser l'attribut cachepour vos appels aux méthodes, par exemple this.client.guilds.size sera remplacé par this.client.guilds.cache.sizesur les versions récentes.

Nous allons créer notre première commande utilisant les collections. Elle affichera le nombre de serveurs Discord sur lesquels le bot est présent, dans un message embed.

Créez un fichier commands/divers/stats.js avec le contenu suivant :

const { Command } = require('discord.js-commando');
const Discord = require('discord.js');

module.exports = class StatsCommand extends Command {
    constructor(client) {
        super(client, {
            name: 'stats',
            memberName: 'stats',
            group: 'divers',
            description: 'Affiche le nombre de serveurs Discord où est présent le bot.',
        });
    }

    async run(msg) {
        const embed = new Discord.MessageEmbed()
            .setDescription(`Je suis présent sur **${this.client.guilds.cache.size} serveur(s)** :heart:`)
            .setColor('BLUE')
        ;

        return msg.say(embed);
    }
};

Concentrons nous sur la méthode run(msg). Nous créons un nouveau message embed dans lequel nous voulons indiquer le nombre de serveurs où est présent le bot. Pour cela, nous devons accéder à discord.js, et plus particulièrement à la collection guilds. L'attribut size contient le nombre de valeurs dans la collection (= le nombre de serveurs où est le bot).

Lancez le bot, et envoyez la commande ?stats, vous devriez voir :

stats bot discord

Le code source est disponible ici.

Récupérer des données

Pour récupérer des données depuis une collection, vous devez utiliser un identifier avec la méthode get().

Avant d'utiliser cette méthode, nous allons vérifier avec la méthode has() que le serveur qu'on veut récupérer est bien accessible à notre bot. On va utiliser la collection guilds du client :

this.client.guilds.cache.has('529605510219956233') // retourne true ou false

Si le serveur (= guild) est bien accessible, on fait appel à get() :

const guild = this.client.guilds.cache.get('529605510219956233')

Une bonne pratique est de toujours faire appel à la méthode has() avant un get(), afin d'éviter tout risque d'erreur :

const serverId = '529605510219956233'

if (this.client.guilds.cache.has(serverId)) {
    const guild = this.client.guilds.cache.get(serverId)
}

Filtrer les données

La méthode filter() permet de filtrer les données d'une collection pour ne récupérer que les éléments qui nous intéresse.

Par exemple, pour lister tous les serveurs où est présent le bot et ayant plus de 10 membres :

const guilds = this.client.guilds.cache.filter( g => g.memberCount > 10 ) // retourne une collection

for (const guild of guilds) {
    this.logger.log('info', `Serveur ${guild.name} - ${guild.memberCount} membres`))
}

Appliquer un traitement aux données

La méthode map() permet d'appliquer un traitement à toutes les données d'une collection. Imaginons nous souhaitions récupérer uniquement la liste des noms des serveurs, nous allons utiliser cette fonction pour la transformer la collection et ne retourner qu'un tableau épuré, contenant uniquement des chaines de caractères :

const guildsName = this.client.guilds.cache.map( g => g.name) // retourne un tableau contenant les noms des serveurs, ex: ['Mon serveur', 'Le serveur de mon pote']

for (const guildName of guildsName) {
    this.logger.log('info', `Le bot est présent sur le serveur \`${guildName}\``))
}

Collections d'un message

Vous l'avez surement déjà remarqué, un objet de la classe CommandoMessage, représentant le message ayant déclenché la commande, est présent en premier argument de la méthode run(msg).

Cet objet vous permet, tout comme avec le client, d'accéder à des collections. A la différence qu'elles seront liées au message, à son auteur ainsi qu'au serveur dont il provient.

async run(msg) {
     console.log(msg.content); // contient le message ayant déclenché la commande, par exemple "?hello"

     console.log(msg.channel); // contient le channel ayant déclenché la commande
     if (msg.channel.type === 'dm') { // le message a t'il été envoyé via message privé ? Ou dans un channel ?
            // bloquer la commande ou adapter le code en fonction
     }

     console.log(msg.author); // contient un objet représentant l'auteur du message
     console.log(msg.author.id); // ID de l'auteur
     console.log(msg.author.username); // nom d'utilisateur de l'auteur
     console.log(msg.author.displayAvatarURL); // image de profil de l'utilisateur

     console.log(msg.member); // contient un objet représentant les informations de l'auteur sur le serveur où il a posté le message
     console.log(msg.member.roles.cache.some(role => role.id === '602918672482172978')) // on vérifie si l'auteur du message a le role demandé, sur le serveur où il a écrit

     // et plus ! Consultez la doc officielle ;)
}

Les emojis / smileys

Pour rendre vos messages plus vivant, vous pouvez utiliser les émoticônes dans vos messages. Il en existe deux types :

  • les émoticônes unicode qui sont accessibles partout, et pour tous. Vous pouvez les parcourir ici.
  • les émoticônes externes qui vous permettent de personnaliser les émojis disponibles sur votre serveur.

Pour utiliser un émoticône unicode, rien de plus simple, il suffit de le copier dans vos messages. Par exemple avec le coeur :

embed.setDescription(`Je vous aime ❤️`) // unicode
embed.setDescription(`Je vous aime :heart:`) // fonctionne aussi

Pour les émoticône externes, leur utilisation est plus compliquée. Vous devez récupérer l'ID de l'émoji, à partir du nom de celui-ci dans l'interface Discord :

const customCatEmoji = msg.guild.emojis.cache.find(emoji => emoji.name === 'cat');
if (customCatEmoji) {
    embed.setDescription(`Je vous aime ${customCatEmoji}`) // emoji externe
}

Vous n'avez pas encore ajouté d'émoji personnalisé sur votre serveur ? Nous avons un pack pour vous, à télécharger ici, que vous pouvez ensuite importer sur Discord.

Les réactions

Maintenant que nous savons utiliser les émoticônes, on peut se pencher sur les réactions aux messages. Un exemple de message avec réaction :

message avec reaction

Pour ajouter une réaction à un message embed, utilisez la méthode react() :

async run(msg) {
        const embed = new Discord.MessageEmbed()
            .setDescription(`Exemple de message avec des réactions`)
        ;

        const replyMsg = await msg.say(embed); // on conserve dans 'replyMsg' les informations sur message que nous venons d'envoyer

        // on ajoute les réactions à notre réponse :
        replyMsg.react('❤️');

        // utilisation d'un emoji externe :
        const customCatEmoji = msg.guild.emojis.cache.find(emoji => emoji.name === 'cat');
        if (customCatEmoji) {
             replyMsg.react(customCatEmoji);
        }
}

On a vu plus compliqué :p

Les permissions

Les permissions sont vitales pour sécuriser les commandes de votre bot, elles permettent de vérifier si un membre à accès à une commande et possède bien les droits nécessaires.

Nous avons déjà abordé les permissions dans le chapitre Commando, nous allons maintenant vous montrer quelques exemples de l'utilisation des rôles et permissions dans votre code.

Pour accéder à la collection des rôles d'un serveur, utilisez guild.roles. Cette collection est accessible depuis l'instance du message msg :

async run(msg) {
     if (msg.channel.type === 'dm') {
            return; // ne rient faire si le message est envoyé en DM, aucun serveur (= guild) n'est lié.
     }

    const roles = msg.guild.roles.cache // liste des rôles du serveur où est tapé le message
}

Notre prochaine commande va afficher les rôles du serveur où est posté le message et le nombre d'utilisateur ayant chaque rôle. Nous allons utiliser la collection roles de member et la méthode map() pour compter le nombre de membres ayant chaque rôle.

Créez un fichier commands/divers/roles.js avec le contenu suivant :

const { Command } = require('discord.js-commando');
const Discord = require('discord.js');

module.exports = class StatsCommand extends Command {
    constructor(client) {
        super(client, {
            name: 'roles',
            memberName: 'roles',
            group: 'divers',
            description: 'Liste les roles du serveur et le nombre de membres de chaque role.',
        });
    }

    async run(msg) {
        const embed = new Discord.MessageEmbed()
            .setAuthor(`${this.client.user.tag}`, `${this.client.user.displayAvatarURL()}`)
            .setColor('BLUE')
            .setTimestamp();

        const guildMembers = msg.guild.members.cache

        msg.guild.roles.cache.map(role => {
            const countMembersOfRole = guildMembers.filter( member => member.roles.cache.has(role.id) ).size

            embed.addField(
                role.name.replace("@everyone", "ALL"),
                countMembersOfRole, 
                true
            )
        })

        return msg.say(embed)
    }
};

Il ne vous reste plus qu'à tester votre nouvelle commande ?roles ;)

Pour vérifier qu'un utilisateur possède bien un rôle donné :

const hasRoleByName = msg.member.roles.cache.some(role => role.name === 'Nom du role');

const hasRoleById= msg.member.roles.cache.some(role => role.id === '602918672482172978');

Pour récupérer un rôle, par son nom :

const roleModerator = msg.guild.roles.cache
            .filter(role => role.name === 'Modérateur')
            .first();

Les événements

La librairie discord.js intègre des événements que vous pouvez écouter. Lorsqu'ils se déclenchent, vous pourrez exécuter du code. Il existent de nombreux événements par exemple:

  • un nouveau message envoyé sur le serveur.
  • la création d'un nouveau channel.
  • le bannissement d'un membre.
  • l'ajout du bot sur un nouveau serveur.
  • et bien d'autres.

Voici la liste brute de tous les évènements disponibles :

  • channelCreate
  • channelDelete
  • channelPinsUpdate
  • channelUpdate
  • debug
  • emojiCreate
  • emojiDelete
  • emojiUpdate
  • error
  • guildBanAdd
  • guildBanRemove
  • guildCreate
  • guildDelete
  • guildIntegrationsUpdate
  • guildMemberAdd
  • guildMemberRemove
  • guildMembersChunk
  • guildMemberSpeaking
  • guildMemberUpdate
  • guildUnavailable
  • guildUpdate
  • invalidated
  • inviteCreate
  • inviteDelete
  • message
  • messageDelete
  • messageDeleteBulk
  • messageReactionAdd
  • messageReactionRemove
  • messageReactionRemoveAll
  • messageReactionRemoveEmoji
  • messageUpdate
  • presenceUpdate
  • rateLimit
  • ready
  • roleCreate
  • roleDelete
  • roleUpdate
  • shardDisconnect
  • shardError
  • shardReady
  • shardReconnecting
  • shardResume
  • typingStart
  • userUpdate
  • voiceStateUpdate
  • warn
  • webhookUpdate

Vous ne l'avez peut-être pas remarqué, mais nous avons déjà commencé à utilisé ces événements, avec l'affichage du message Je suis prêt ! lorsque le bot démarre, sur l'événement ready.

Nous allons mettre en place un système plus robuste pour exécuter du code lorsqu'un événement est déclenché. Le code qui sera exécuté sera stocké dans un fichier et son nom sera normalisé. Vous devrez toujours respecter la norme events/NOMDELEVENT.js.

Par exemple :

  • le code de l’événement ready sera dans events/ready.js.
  • le code de l’événement message sera dans events/message.js.
  • le code de l'événement guildMemberAdd sera dans events/guildMemberAdd.js.

Pour mettre en place ce système, nous allons ajouter ce morceau de code dans notre fichier bot.js :

fs.readdir('./events/', (err, files) => {
    if (err) return console.error(err);
    files.forEach((file) => {
        const eventFunction = require(`./events/${file}`);
        if (eventFunction.disabled) return;

        const event = eventFunction.event || file.split('.')[0];
        const emitter = (typeof eventFunction.emitter === 'string' ? client[eventFunction.emitter] : eventFunction.emitter) || client;
        const { once } = eventFunction;

        try {
            emitter[once ? 'once' : 'on'](event, (...args) => eventFunction.run(client, ...args));
        } catch (error) {
            console.error(error.stack);
        }
    });
});

Pour faire simple, ce code permet de charger automatiquement les fichiers qui sont dans votre répertoire events/.

Après édition, vous devriez avoir le code suivant dans bot.js :

const CommandoClient = require('./client');
const path = require('path');
const fs = require('fs')
const dotenv = require('dotenv')

const envConfig = dotenv.parse(fs.readFileSync('.env'))
for (const k in envConfig) {
    process.env[k] = envConfig[k]
}

const client = new CommandoClient({
    commandPrefix: '?',
    owner: process.env.BOT_OWNER_ID,
    disableMentions: 'everyone'
});

fs.readdir('./events/', (err, files) => {
    if (err) return console.error(err);
    files.forEach((file) => {
        const eventFunction = require(`./events/${file}`);
        if (eventFunction.disabled) return;

        const event = eventFunction.event || file.split('.')[0];
        const emitter = (typeof eventFunction.emitter === 'string' ? client[eventFunction.emitter] : eventFunction.emitter) || client;
        const { once } = eventFunction;

        try {
            emitter[once ? 'once' : 'on'](event, (...args) => eventFunction.run(client, ...args));
        } catch (error) {
            console.error(error.stack);
        }
    });
});

client.registry
    .registerDefaultTypes()
    .registerGroups([
        ['divers', 'Divers'],
    ])
    .registerCommandsIn(path.join(__dirname, 'commands'))
;

client.login(process.env.BOT_TOKEN);

Il ne nous reste plus qu'à utiliser notre premier événement. Nous allons déplacer le message affiché lorsque le serveur démarre.

Créez un fichier events/ready.js, contenant le code :

module.exports = {
    run: (client) => {
        client.logger.log('info', `Bot identifié en tant que ${client.user.tag}! (${client.user.id})`);
    }
};

Un événement reçoit toujours en premier argument une instance du client: run: (client).

Lancez le bot, au démarrage vous devriez voir le message :

[2020-11-10 19:42:50] - [INFO] - Je suis prêt !
[2020-11-10 19:42:50] - [INFO] - Bot identifié en tant que GS-Community#9894! (720407780086775808)

On peut supprimer l'ancien message devenu inutile, qui n'utilisait pas le système que nous venons de mettre en place.

Ouvrez le fichier client.js et supprimez la ligne :

this.on('ready', () => this.logger.log('info', `Je suis prêt !`));

Relancez le bot, vous devez voir :

[2020-11-10 19:42:50] - [INFO] - Bot identifié en tant que GS-Community#9894! (720407780086775808)

Vous pouvez voir le code complet, c'est ici.

Les timers: setInterval, setTimeout

JavaScript met à disposition deux fonctions permettant de déclencher du code suite à des intervalles de temps :

  • setTimeout est une fonction qui permet d’exécuter un code après un temps (en millisecondes) donné. Le code ne sera exécuté qu'une fois.
  • setInterval est une fonction qui permet d’exécuter et répéter cycliquement un code, toutes les X millisecondes.

L'avantage d'utiliser ces fonctions est qu'elles seront lancées dans des processus indépendants, permettant de lancer du code en tâche de fond, selon des intervalles de temps. Utile pour de nombreux usages: la vérification d'un flux XML, l'ajout d'un message de présence/activité aléatoire pour le bot, l'annonce journalière des membres dont c'est l'anniversaire, etc.

Le client discord.js implémente un système permettant de gérer pour vous les nombreux timers que vous créerez grâce à des méthodes utilitaires disponibles sur l'objet client :

  • setInterval(fn, delay, ...args): interval - Sets an interval that will be automatically cancelled if the client is destroyed.
  • setTimeout(fn, delay, ...args): timeout - Sets a timeout that will be automatically cancelled if the client is destroyed.*
  • clearInterval(interval)
  • clearTimeout(timeout)

Vous pouvez faire appel à ces méthodes dans vos commandes ou dans les événements. Par exemple dans l’événement ready :

const Discord = require('discord.js')

module.exports = {
    run: () => {
        client.logger.log('info', `Logged in as ${client.user.tag}! (${client.user.id})`);

        client.setTimeout(() => console.log('Hey!'), 5000 * 60) // ce code ne sera exécuté que dans 5 minutes

        client.setInterval(() => console.log('ping!'), 5000 * 60) // toutes les 5 minutes, le message 'ping!' s'affichera dans la console du bot

        client.setInterval(() => {
            // du code plus complexe a executer toutes les 5 minutes
            console.log('another ping!')
        }, 5000 * 60) // toutes les 5 minutes
    }
};

Amélioration du reporting des erreurs

Si une erreur s'est glissée dans votre code source, vous n'avez actuellement que peu de détail pour déterminer la provenance de celle-ci dans son code :

error bot

Pour améliorer le reporting des erreurs, nous allons écouter plusieurs évènements: error, uncaughtException et unhandledRejection.js. Nous l'avons vu précédemment, pour écouter des évènements, nous devons ajouter notre code dans le répertoire events/.

Le code des 3 évènements sera identique. Créez les fichiers event/error.js, event/uncaughtException.js et event/unhandledRejection.js. Ils doivent avoir le contenu suivant :

module.exports = {
    run: (client, error) => {
        if (!error) return;

        client.logger.log('error', error.stack ? error.stack : error.toString());
    }
};

Relancez le bot. Vous aurez désormais le détail de vos erreurs ;)

Création d'un rôle modérateur

Nous allons voir comment créer un rôle Modérateur facilement. Les membres ayant ce rôle doivent pouvoir lire et éditer tous messages, sans pour autant être administrateur. Une bonne pratique de sécurité.

Tout d'abord, créez un rôle Modérateur sur votre serveur et appliquez lui les permissions par défaut que vous souhaitez. Ne donnez pas la permission Administrateur, sécurité avant tout !

Nous allons maintenant créer une commande qui surchargera les permissions du rôle Modérateur sur chaque channel du serveur afin d'être certain des permissions de ceux-ci. Utile si vous utilisez de nombreux rôles sur votre serveur.

Pour l'occasion, nous allons créer un nouveau groupe de commande nommé admin. Ouvrez le fichier bot.js et éditez l'appel à la méthode registerGroups() pour déclarer notre nouveau groupe :

client.registry
    .registerDefaultTypes()
    .registerGroups([
        ['divers', 'Divers'],
        ['admin', 'Admin'], // On ajoute le nouveau groupe 'admin'
    ])
    .registerCommandsIn(path.join(__dirname, 'commands'))
;

Créez un fichier commands/admin/set-moderator.js avec le code suivant :

const { Command } = require('discord.js-commando');
const Discord = require('discord.js')

module.exports = class SetModeratorCommand extends Command {
    constructor(client) {
        super(client, {
            name: 'set-moderator',
            group: 'admin', // on utilise le nouveau groupe 'admin'
            memberName: 'set-moderator',
            description: 'Ajoute le role moderateur a tous les channels, et force les permissions.',
            ownerOnly: true, // owner only
            guildOnly: true // uniquement dans un channel, pas de DM pour cette commande
        });
    }

    async run(msg) {
        const hasRoleModerator = msg.guild.roles.cache.some(role => role.name === 'Modérateur'); // Adaptez le nom si vous changez le nom du rôle modérateur
        if (!hasRoleModerator) {
            this.client.logger.log('error', `Role 'Modérateur' non trouvé sur ce serveur.`);
            return;
        }

        const roleModerator = msg.guild.roles.cache
            .filter(role => role.name === 'Modérateur')
            .first();

        msg.guild.channels.cache.map(channel => {
            this.client.logger.log('info', `Role 'Modérateur' ajouté sur ${channel.name}.`);

            // On surcharge les permissions du rôle 'Modérateur' sur chaque channel du serveur :
            channel.createOverwrite(
                roleModerator.id,
                {
                    ADD_REACTIONS: true, // ajouter des réactions
                    KICK_MEMBERS: true, // kick un membre
                    BAN_MEMBERS: true, // ban un membre
                    VIEW_AUDIT_LOG: true, // voir les audits
                    VIEW_CHANNEL: true, // voir tous les channels
                    READ_MESSAGES: true, // voir les nouveaux messages
                    SEND_MESSAGES: true, // envoyer des messages
                    SEND_TTS_MESSAGES: false, // envoyer des message TTS
                    MANAGE_MESSAGES: true, // peuvent modérer les messages
                    EMBED_LINKS: true, // envoyer des liens sous format intégré
                    ATTACH_FILES: true, // envoyer des fichiers
                    READ_MESSAGE_HISTORY: true, // voir les anciens messages
                    MENTION_EVERYONE: false, // peut mentionner @everyone
                    USE_EXTERNAL_EMOJIS: true,
                    EXTERNAL_EMOJIS: true,
                    SPEAK: true,
                    CONNECT: true,
                    MUTE_MEMBERS: true,
                    DEAFEN_MEMBERS: true,
                    MOVE_MEMBERS: true,
                    USE_VAD: true,
                    CHANGE_NICKNAME: true,
                    MANAGE_NICKNAMES: true,
                }
            );
        });

        const embed = new Discord.MessageEmbed()
            .setDescription(`Mise à jour des permissions pour le role ${roleModerator.name}.`)
            .setColor('GREEN')
        ;

        return msg.say(embed);
    }
};

Il ne vous reste plus qu'à tester la commande ?set-moderator. Si tout se passe bien, vous aurez un message de confirmation :

[2020-11-10 18:38:45] - [INFO] - Role 'Modérateur' ajouté sur bot-commands.
[2020-11-10 18:38:45] - [INFO] - Role 'Modérateur' ajouté sur Vocal #1.
[2020-11-10 18:38:45] - [INFO] - Role 'Modérateur' ajouté sur ⛏-aide-dev.
[2020-11-10 18:38:45] - [INFO] - Role 'Modérateur' ajouté sur general.

Il ne vous restera qu'à vérifier que le rôle Modérateur a bien été ajouté sur tous les channels, avec les bonnes permissions.

Vous pourrez l’exécuter à chaque fois que vous ajouterez de nouveaux channels, cela ajoutera automatiquement les permissions des modérateurs, impossible de vous tromper !

Si vous voulez vérifiez votre code, les sources sont disponibles ici.

Création d'une commande d'aide

Nous allons passer à la création d'une commande help qui va :

  • lister les commandes automatiquement, par groupe.
  • afficher le détail d'une commande, si un argument est passé.
  • récupérer les informations des commandes, directement depuis le code.

Pour commencer, nous allons ajouter un nouveau groupe bot. Vous devriez avoir l'habitude, éditez le fichier bot.js et ajoutez le groupe :

client.registry
    .registerDefaultTypes()
    .registerGroups([
        ['divers', 'Divers'],
        ['admin', 'Admin'],
        ['bot', 'Bot'],
    ])
    .registerCommandsIn(path.join(__dirname, 'commands'))
;

Passons au code de la commande, créez un fichier commands/bot/help.js avec le contenu suivant :

const { stripIndents, oneLine } = require('common-tags');
const { disambiguation } = require('discord.js-commando/src/util');
const { Command } = require('discord.js-commando');
const Discord = require('discord.js')

module.exports = class HelpCommand extends Command {
    constructor(client) {
        super(client, {
            name: 'help',
            group: 'bot',
            memberName: 'help',
            aliases: ['h'],
            description: 'Affiche la liste des commandes disponibles.',
            clientPermissions: ['SEND_MESSAGES'],
            args: [
                {
                    key: 'command',
                    prompt: 'Pour quelle commande voulez-vous de l\'aide ?',
                    type: 'string',
                    default: ''
                }
            ]
        });
    }

    async run(msg, args) { // eslint-disable-line complexity
        const groups = this.client.registry.groups;
        const commands = this.client.registry.findCommands(args.command, false, msg);
        const showAll = args.command && args.command.toLowerCase() === 'all';

        const embed = new Discord.MessageEmbed()
            .setColor('BLUE');

        if(args.command && !showAll) {
            if(commands.length === 1) {
                let help = stripIndents`
					${oneLine`
						${commands[0].description}
						${commands[0].guildOnly ? ' (uniquement sur les serveurs' : ''}
						${commands[0].nsfw ? ' (NSFW)' : ''}
					`}

					**Format:** ${msg.anyUsage(`${commands[0].name}${commands[0].format ? ` ${commands[0].format}` : ''}`)}
				`;

                if(commands[0].aliases.length > 0) help += `\n**Aliases:** ${commands[0].aliases.join(', ')}`;

                help += `\n${oneLine`
					**Groupe:** ${commands[0].group.name}
					(\`${commands[0].groupID}:${commands[0].memberName}\`)
				`}`;

                if(commands[0].details) help += `\n**Detail:** ${commands[0].details}`;
                if(commands[0].examples) help += `\n**Exemple:**\n${commands[0].examples.join('\n')}`;

                embed
                    .setAuthor(commands[0].name.toUpperCase())
                    .setDescription(help.replace(/ or /g, ' ou '))
            } else if(commands.length > 15) {
                embed.setDescription('Plusieurs commandes peuvent correspondrent, merci d\'être plus précis.')
            } else if(commands.length > 1) {
                embed.setDescription(disambiguation(commands, 'commands').replace(/ or /g, ' ou '))
            } else {
                embed.setDescription(`Cette commande n'existe pas. Utilisez ${msg.usage(
                    null, msg.channel.type === 'dm' ? null : undefined, msg.channel.type === 'dm' ? null : undefined
                )} pour voir la liste de toutes les commandes`.replace(/ or /g, ' ou '))
            }
        } else {
            embed
                .setDescription(
                    stripIndents`
					${oneLine`
						Pour lancer une commande, utilisez ${Command.usage('command', msg.guild ? msg.guild.commandPrefix : null, this.client.user)}.
						Par exemple, ${Command.usage('help', msg.guild ? msg.guild.commandPrefix : null, this.client.user)}.
					`}

					Utilisez ${this.usage('<commande>', null, null)} pour voir les informations détaillées de la commande spécifiée.

					__**Commandes disponibles :**__

					${groups.filter(grp => grp.commands.some(cmd => !cmd.hidden && cmd.isUsable(msg)))
                        .map(grp => stripIndents`
							・ **${grp.name}**
							${grp.commands.filter(cmd => !cmd.hidden && cmd.isUsable(msg))
                            .map(cmd => `\`${cmd.name}\`: ${cmd.description}${cmd.nsfw ? ' (NSFW)' : ''}`).join('\n')
                        }
						`).join('\n\n')
                    }`.replace(/ or /g, ' ou '))
            ;
        }

        return msg.say(embed)
    }
};

Lancez le bot et tapez ?help. Le bot va lister (automatiquement) les commandes disponibles :

commande d'aide discord.js

On peut consulter le détail d'une commande avec ?help NOM_DE_LA_COMMANDE, par exemple ?help help :

détail aide commande

Vous pouvez consulter notre code source ici.

Message d'activité ou présence

Vous pouvez associer un message d'activité ou de présence à votre bot, il sera visible dans la liste des membres :

presence

Pour ajouter un message de ce type, vous pouvez ajouter l'option presence lors de la création du client, dans le fichier bot.js.

Voici un exemple :

const client = new CommandoClient({
    commandPrefix: '?',
    owner: process.env.BOT_OWNER_ID,
    disableMentions: 'everyone',
    presence: {
        activity: {
            name: `?help | mTxServ.com`, // message de présence
            type: 'LISTENING' // type d'activité
        }
    }
});

La liste des activités disponibles sur un bot est :

  • PLAYING
  • STREAMING
  • LISTENING
  • WATCHING

Il est également possible de changer le message dynamiquement, en appelant la méthode client.setActivity() sur le client.

Les deux exemples ci-dessous peuvent être testés dans le code d'une commande :

// Exemple 1
this.client.user.setActivity('mTxServ.com', {
    type: 'WATCHING',
});

// Exemple 2 - nombre de serveurs utilisant notre bot
this.client.user.setActivity(`${this.client.guilds.cache.size} serveur(s)`, {
    type: 'LISTENING',
});

Dans le cas de l'exemple 2, il serait bon de mettre à jour régulièrement le message de présence pour prendre en compte tout nouveau serveur qui ajouterai notre bot.

Pour cela, nous allons ajouter le code du changement d'activité dans l'évènement ready. Nous ferrons appel à la fonction setInterval() pour mettre à jour toutes les 5 minutes le message de présence, alors le compteur mis à jour.

Éditez le fichier event/ready.js :

module.exports = {
    run: (client) => {
        client.logger.log('info', `Bot identifié en tant que ${client.user.tag}! (${client.user.id})`);

        const updateActivity = () => {
            client.user.setActivity(`${client.guilds.cache.size} serveur(s)`, { type: 'LISTENING' })
        }

        updateActivity(); // le bot est 'ready', on initialise le message d'activité. Ensuite, le setInterval() permettra de rafraîchir le message toutes les 5 minutes

        client.setInterval( () => updateActivity(), 5000 * 60 ); // toutes les 5 minutes après le lancement du bot, le message de présence sera mis à jour, et le compteur rafraîchi.
    }
};

Vous pouvez consulter le code ici.

Pagination avec des embeds

Nous allons construire un système de pagination avec des messages embeds. Une pagination vous permet de gérer une liste, avec des fonctionnalités du type précédente / suivante, permettant à l'utilisateur de naviguer entre les pages :

pagination discord

Cette fonctionnalité n'existe pas dans Discord, nous allons utiliser la librairie discord.js-pagination pour créer une pagination originale et interactive pour l'utilisateur en quelques lignes de code.

Pour commencer, on installe la librairie :

npm install discord.js-pagination

Voici ce que nous indique la documentation officielle pour son utilisation :

const paginationEmbed = require('discord.js-pagination');

// Liste de message embed, chaque page = 1 embed
pages = [
	embed1,
	embed2,
	//....
	embedn
];

// Création de la pagination
paginationEmbed(msg, pages, emojiList, timeout);

Vous devez donc créer vos embeds et les ajouter dans une variable pages. Ensuite, appelez la méthode paginationEmbed qui crée la pagination et envoie le message sur Discord.

Concernant les arguments :

  • emojiList est un tableau contenant les émoticones utilisés pour page précédente / page suivante. Par défaut: ['⏪', '⏩']
  • timeout est le temps en ms pendant lequel la pagination fonctionnera. Par défaut: 120000 ms.

Nous avons toutes les informations pour tester dans une commande. Créez un fichier commands/divers/pager.js avec le contenu suivant :

const { Command } = require('discord.js-commando');
const Discord = require('discord.js');
const paginationEmbed = require('discord.js-pagination');

module.exports = class PagerCommand extends Command {
    constructor(client) {
        super(client, {
            name: 'pager',
            memberName: 'pager',
            group: 'divers',
            description: 'Test de pagination.',
            ownerOnly: true,
            clientPermissions: ['SEND_MESSAGES', 'EMBED_LINKS'],
            throttling: {
                usages: 2,
                duration: 10,
            },
        });
    }

    async run(msg) {
        const data = [
            {
                titre: 'Titre 1',
                texte: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean eget neque nunc. Nam venenatis leo varius cursus pulvinar. Quisque lacus enim, porta sed molestie quis, mattis quis ex. Mauris vestibulum purus eu nunc tempus viverra. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris aliquet in lectus vel feugiat.'
            },
            {
                titre: 'Titre 2',
                texte: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris pulvinar risus sit amet lorem efficitur, a tristique massa finibus. Praesent egestas, mi ut ultricies pharetra, nibh ante cursus magna, sed tempus neque dui ornare tortor. In lacinia, risus eu luctus fermentum, ligula dolor bibendum lectus, in molestie lacus erat ut nisl.'
            },
            {
                titre: 'Titre 3',
                texte: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam nec pretium mi. Maecenas sed ante mi. Ut ac odio sed neque consequat efficitur at ac augue. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Sed hendrerit ultrices malesuada. Aenean congue eros nec.'
            }
        ];

        const pages = []

        for (const item of data) {
            const embed = new Discord.MessageEmbed()
                .setTitle(item.titre)
                .setDescription(item.texte)
            ;

            pages.push(embed)
        }

        paginationEmbed(msg, pages);
    }
};

Lancez le bot, et testez en envoyant la commande ?pager. Voici le résultat :

pagination

Le code source est visible ici.

Requêter une API et afficher le résultat

Il existe un grand nombre d'API sur internet, pour tout et n'importe quoi. Dans cette partie du tutoriel, nous allons vous montrer comment interroger une API au format JSON et afficher le résultat. Pour nos appels (HTTP/HTTPS), nous utiliserons l'excellente librairie got.

Pour notre cas d'usage, nous avons choisi l'API des tutoriels de mTxServ. Nous allons créer une commande, qui permet d'interroger l'API pour effectuer une recherche et afficher les résultats à l'utilisateur.

On commence par ajouter la librairie got à notre projet :

npm install --save got

Avant d'aller plus loin, quelques détails sur l'API qui va être utilisée. L'adresse du endpoint est https://mtxserv.com/api/v1/articles/?query=. Le paramètre query permet d'indiquer la requête de la recherche.

Voici un exemple de résultat retourné avec la requête discord :

[
  {
    "id": "12098",
    "title": "Créer un bot Discord avec discord.js",
    "category": "Discord.js",
    "locale": "fr",
    "link": "https://mtxserv.com/fr/article/12098/creation_d_un_bot_discord_application_permissions"
  },
  {
    "id": "12247",
    "title": "DiscordSRV: relier Discord à son serveur Minecraft",
    "category": "Plugins",
    "locale": "fr",
    "link": "https://mtxserv.com/fr/article/12247/discordsrv_liez_discord_a_votre_serveur_minecraft"
  },
  {
    "id": "12946",
    "title": "Comment lancer un processus en background + autorestart",
    "category": "VPS",
    "locale": "fr",
    "link": "https://mtxserv.com/fr/article/12946/comment-lancer-un-processus-en-background-autorestart"
  },
  {
    "id": "12207",
    "title": "Installer et configurer l'addon GExtension",
    "category": "Web",
    "locale": "fr",
    "link": "https://mtxserv.com/fr/article/12207/installer_et_configurer_l_addon_gextension"
  },
  {
    "id": "12856",
    "title": "Hytale Discord Bot",
    "category": "Hytale",
    "locale": "en",
    "link": "https://mtxserv.com/fr/article/12856/hytale-discord-bot"
  }
]

Pour notre commande ?tuto :

  • nous n'afficherons que les articles en langue fr.
  • nous n'afficherons que les 3 premiers résultats.

Intéressons nous à l'utilisation de la librairie got. La documentation est disponible ici. Pour appeler une url en GET, qui retourne du format json :

const got = require('got');

const res = await got('https://exemple.com/api/users.json, {
    responseType: 'json'
})

if (!res || !res.body) {
    throw new Error('Invalid response')
}

console.log(res.body)

Nous avons désormais tout pour assembler notre commande. Avant de la créer, nous allons ajouter une nouvelle classe HowToApi qui concentrera tout le code de cette API. Cette bonne pratique permet de garder un code découplé et maintenable.

C'est parti pour la création de la classe. Créez un nouveau répertoire api/ à la racine du projet, vous devez avoir la structure suivante :

api/
commands/
events/

Dans ce répertoire, créez un nouveau fichier api/HowToApi.js avec le contenu suivant :

const got = require('got');

// Vous devez toujours encoder avec encodeURIComponent() les paramètres GET d'une URL
const makeURL = (query) => `https://mtxserv.com/api/v1/articles/?query=${encodeURIComponent(query)}`;

class HowToApi {
    async search(query) {
        const res = await got(makeURL(query), {
            responseType: 'json'
        })

        if (!res || !res.body) {
            throw new Error('Invalid response of mTxServ.com API')
        }

        return res.body
    }
}

module.exports = HowToApi;

Le code de l'appel à l'API sera ainsi séparé de votre commande, plus propre non ?! Et celle-ci n'aura plus qu'à gérer l'affichage. Là encore, une bonne pratique.

Créez un fichier commands/divers/howto.js :

const { Command } = require('discord.js-commando');
const Discord = require('discord.js');
const HowToApi = require('../../api/HowToApi') // on utilise la classe HowToApi que nous venons de créer

module.exports = class TutoCommand extends Command {
    constructor(client) {
        super(client, {
            name: 'tuto',
            memberName: 'tuto',
            aliases: ['howto', 'guide'],
            group: 'divers',
            description: 'Cherche un tutoriel.',
            guildOnly: true,
            clientPermissions: ['SEND_MESSAGES', 'EMBED_LINKS'],
            args: [
                {
                    key: 'query',
                    prompt: 'Quel tutoriel cherchez vous ?',
                    type: 'string',
                    validate: text => text.length >= 3,
                },
            ],
            throttling: {
                usages: 2,
                duration: 10,
            },
        });
    }

    async run(msg, { query}) {
        // appel à l'API de mTxServ pour recherche un tutoriel
        const api = new HowToApi()
        const results = await api.search(query)

        const embed = new Discord.MessageEmbed()
            .setTitle(`:mag: Recherche *${query}*`)
            .setColor('BLUE')
        ;

        results
            // on supprime les articles en anglais
            .filter(article => article.locale === 'fr')

            // on ajoute tous les articles restants à l'embed
            .map(article => embed.addField(
                `${article.locale == 'fr' ? ':flag_fr:' : ':flag_us:'} ${article.title}`,
                `${article.link}`
            ))
        ;

        // on ne garde que les 3 derniers résultats
        embed.fields = embed.fields.slice(0, 3);

        return msg.say({
            embed
        });
    }
};

Si vous avez bien suivi les chapitres précédents, rien de nouveau dans ce code, nous assemblons simplement ce que nous avons vu auparavant, dans un vrai cas d'usage.

Testons la commande?tuto discord :

fetch api discord.js

Voila votre première interaction avec une API externe ! Vous pouvez facilement adapter le principe à toute API qui retourne du json.

Vous trouverez notre code source ici.

Avant de passer à la suite, je vous invite à consulter cette liste d'API gratuite, qui contient de tout, à commencer par des API pour retourner des images de chats ^^

Envoyer un fichier: image aléatoire de chat

Il est possible de joindre un ou plusieurs fichiers à vos messages, avec l'attribut files, qui reçoit un tableau d'url d'images :

msg.say({
    files: [
        'https://mtxserv.com/build/img/mtxserv.png'
    ]
});

Pour tester cette fonctionnalité, nous allons créer une commande qui appel l'API thecatapi.com. Cette API renvoie une image de chat au hasard. Son endpoint est https://api.thecatapi.com/v1/images/search?limit=1.

Voici un exemple de résultat retourné :

[
   {
      "breeds":[
         
      ],
      "id":"KUEJ039io",
      "url":"https://cdn2.thecatapi.com/images/KUEJ039io.jpg",
      "width":639,
      "height":800
   }
]

Il s'agit donc d'un tableau d'image. Cela aura son important pour la suite: il faudra sélectionner le premier élément du tableau.

On commence par créer une classe CatApi. Créez un nouveau fichier api/CatApi.js avec :

const got = require('got');

const makeURL = () => `https://api.thecatapi.com/v1/images/search?limit=1`;

class CatApi {
    async random() {
        const res = await got(makeURL(), {
            responseType: 'json'
        })

        if (!res || !res.body) {
            throw new Error('Invalid response of api.thecatapi.com')
        }

        return res.body
    }
}

module.exports = CatApi;

Passons à la commande qui utilisera cette classe. Créez un fichier commands/divers/cat.js avec :

const { Command } = require('discord.js-commando');
const CatApi = require('../../api/CatApi')

module.exports = class CatCommand extends Command {
    constructor(client) {
        super(client, {
            name: 'cat',
            memberName: 'cat',
            aliases: ['chat'],
            group: 'divers',
            description: 'Affiche une image aléatoire de chat.',
            guildOnly: true,
            clientPermissions: ['SEND_MESSAGES', 'ATTACH_FILES'], // on vérifie que le bot à la permission 'ATTACH_FILES'
            throttling: {
                usages: 2,
                duration: 10,
            },
        });
    }

    async run(msg) {
        const api = new CatApi()
        const images = await api.random()

        if (!images.length) { // si aucun résultat, on arrête là
            return;
        }

        msg.say({
            files: [
                images[0].url // on affiche le contenu de l'attribut 'url' du premier résultat du tableau 'images'
            ]
        });
    }
};

Vous pouvez tester avec ?cat :

commande de chat discord.js

Le code source est disponible ici.

Avant de passer à la suite, vous pouvez vous exercer en créant une commande ?dog avec le endpoint https://dog.ceo/api/breeds/image/random.

Erreurs fréquentes

fetchUser is not a function

Si vous avez une erreur du type TypeError: this.fetchUser is not a function lors du lancement du bot, vous avez probablement un soucis de version de Commando. Pour régler le problème, utilisez la dernière version depuis le dépôt officiel :

npm uninstall discordjs/Commando
npm install --save https://github.com/discordjs/Commando.git

undefined ls-remote -h -t

Une erreur du type undefined ls-remote -h -t lorsque vous utilisez la commande npm install ? Il vous manque probablement Git sur votre ordinateur :

  • Installer Git sur Windows: Git for Windows.
  • Installer Git sur Debian/Ubuntu: sudo -y apt-get install git.

git missing

An invalid token was provided

Si vous avez une erreur [TOKEN_INVALID]: An invalid token was provided lors du démarrage de votre bot, c'est que le token que vous avez spécifié lors de l'appel à la méthode login('VOTRE_TOKEN') est incorrect. Veuillez le vérifier en le comparant avec celui disponible sur Discord Developer.

Discord Hytale, Minecraft, Rust, ARK, FiveM