Skip to content

Commit b40cdc8

Browse files
committed
feat(framework): slash commands
1 parent c08abab commit b40cdc8

File tree

11 files changed

+397
-26
lines changed

11 files changed

+397
-26
lines changed

README.md

Lines changed: 71 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,43 @@
11
# Bot Discord de la communauté
22

3-
## Développement
4-
5-
### Prérequis
3+
## Prérequis
64

75
- Node.js v16
86
- npm v7
97
- Un bot Discord installé sur une copie du serveur ES Community.
10-
- Template: https://discord.new/T3mtuFqjR8Tm
8+
- Template : https://discord.new/T3mtuFqjR8Tm
119

12-
### Préparation de l'environnement
10+
## Préparation de l'environnement
1311

14-
Installez les dépendances avec npm:
12+
Installez les dépendances avec npm :
1513

1614
```console
1715
npm ci
1816
```
1917

20-
Créez un fichier `.env` avec votre token de bot:
18+
Créez un fichier `.env` avec votre token de bot :
2119

2220
```env
2321
DISCORD_TOKEN=votretoken
2422
```
2523

26-
### Exécution du bot
24+
## Exécution du bot
2725

2826
```console
2927
npm start
3028
```
3129

3230
Cette commande exécute le fichier `src/bot.ts`, qui démarre le bot. Les changements dans le dossier `src` sont observés par `nodemon` et le bot est redémarré automatiquement.
3331

34-
### Tests
32+
## Tests
3533

36-
Le projet contient 3 scripts de test qui doivent passer pour tout commit poussé sur la branche `main`. Vous pouvez exécuter tous les tests avec la commande suivante:
34+
Le projet contient 3 scripts de test qui doivent passer pour tout commit poussé sur la branche `main`. Vous pouvez exécuter tous les tests avec la commande suivante :
3735

3836
```console
3937
npm test
4038
```
4139

42-
#### Tests TS
40+
### Tests TS
4341

4442
```console
4543
# Exécution des tests.
@@ -51,7 +49,7 @@ npm run test-coverage
5149

5250
Le framework de test [Jest](https://jestjs.io/) est utilisé pour exécuter les tests. Ceux-ci doivent être écrits en TypeScript dans le dossier `tests`. Essayez de conserver la même structure de dossiers que dans `src` pour organiser les tests.
5351

54-
#### Lint
52+
### Lint
5553

5654
```console
5755
# Exécution d'ESLint
@@ -61,34 +59,85 @@ npm run lint
6159
npm run lint-fix
6260
```
6361

64-
Nous utilisons [ESLint](https://eslint.org/) ainsi que [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint) pour l'analyse statique du code.
62+
Nous utilisons [ESLint](https://eslint.org/) ainsi que [TypeScript ESLint](https://github.com/typescript-eslint/typescript-eslint) pour l'analyse statique du code.
6563

66-
#### Vérification des types TypeScript
64+
### Vérification des types TypeScript
6765

6866
```console
6967
npm run check-types
7068
```
7169

7270
Cette commande exécute le compilateur TypeScript avec l'option `--noEmit`. Elle permet de valider les types de l'entier du projet, y compris sur les fichiers qui ne sont pas testés avec Jest.
7371

74-
### Écriture de fonctionnalités
72+
## Écriture de fonctionnalités
73+
74+
### Commandes
75+
76+
Chaque commande doit être écrite dans un fichier du dossier `src/commands`. Ce
77+
fichier doit instancier et exporter par défaut une instance de la classe `Command`,
78+
en lui passant les paramètres de configuration suivants :
79+
80+
- `enabled`: boolean. Peut être mis à `false` pour désactiver la commande.
81+
- `name`: string. Nom de la commande..
82+
- `description`: string. Description de ce que fait la commande (en français).
83+
- `options`?: object. Options (arguments) de la commande.
84+
- `guildId`?: Snowflake. L'identifiant d'une guilde, si cette commande est spécifique à une guilde.
85+
- `defaultPermission`?: boolean. Si la commande doit être activé par défaut quand le bot est ajouté à un serveur (`true` par défaut).
86+
- `handle`: function. Fonction exécutée lorsque cette commande est appellée. Elle recevra un argument `context`, avec les propriétés :
87+
- `args`: Objet correctement typé, contenant les options fournis par l'éxecuteur de la commande (abstraction d'`interaction.options`).
88+
- `interaction`: Instance de CommandInteraction (discord.js).
89+
- `client`: Instance du Client (discord.js).
90+
- `logger`: Instance du Logger (pino).
7591

76-
#### Tâches cron
92+
#### Exemple
93+
94+
**Fichier exemplaire :** [src/commands/Hello.ts](src/commands/Hello.ts).
95+
96+
```ts
97+
import { Command, CommandOptionTypes } from '../framework';
98+
99+
// création d'une commande slash (https://discord.com/developers/docs/interactions/slash-commands)
100+
export default new Command({
101+
enabled: true,
102+
name: 'say', // nom de la commande
103+
description: 'Dit ce que vous lui dites.', // description de la commande
104+
options: {
105+
message: {
106+
// ceci est une option ("argument") de la commande slash
107+
type: CommandOptionTypes.String, // type d'option (en l'occurence, chaine de caractère)
108+
description: 'Ce que le bot doit dire.', // description de cette option
109+
required: true, // option obligatoire (par défaut, false)
110+
},
111+
},
112+
handle({ args, interaction }) {
113+
// args aura comme type : `{ message: string }`
114+
return interaction.reply(
115+
`**${interaction.user.username}** m'a dit de dire : « ${args.message} ».`,
116+
);
117+
// si toutefois, vous voulez accéder aux arguments fourni comme telle par discord.js :
118+
// interaction.options.get('message').value;
119+
},
120+
});
121+
```
122+
123+
### Tâches cron
77124

78125
Chaque tâche cron doit être écrite dans un fichier du dossier `src/crons`. Ce
79-
fichier doit instancier et exporter par défaut une instance de la classe Cron,
80-
en lui passant les paramètres de configuration suivants:
126+
fichier doit instancier et exporter par défaut une instance de la classe `Cron`,
127+
en lui passant les paramètres de configuration suivants :
81128

82129
- `enabled`: boolean. Peut être mis à `false` pour désactiver la tâche.
83130
- `name`: string. Nom de la tâche. Utilisé dans les logs.
84131
- `description`: string. Description de ce que fait la tâche (en français).
85132
- `schedule`: string. Programme d'exécution. Vous pouvez utiliser [crontab guru](https://crontab.guru/) pour le préparer.
86-
- `handle`: function. Fonction exécutée selon le programme. Elle recevra un argument `context`, avec les propriétés:
133+
- `handle`: function. Fonction exécutée selon le programme. Elle recevra un argument `context`, avec les propriétés :
87134
- `date`: Date théorique d'exécution de la tâche.
88-
- `client`: Instance du client discord.js.
89-
- `logger`: Instance du logger pino.
135+
- `client`: Instance du Client (discord.js).
136+
- `logger`: Instance du Logger (pino).
137+
138+
#### Exemple
90139

91-
Exemple:
140+
**Fichier exemplaire :** [src/crons/CommitStrip.ts](src/crons/CommitStrip.ts).
92141

93142
```ts
94143
import { Cron } from '../framework';

package-lock.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"@types/ws": "^7.4.6",
4242
"@typescript-eslint/eslint-plugin": "^4.28.2",
4343
"@typescript-eslint/parser": "^4.28.2",
44+
"discord-api-types": "^0.22.0",
4445
"eslint": "^7.30.0",
4546
"jest": "^27.0.6",
4647
"nodemon": "^2.0.12",

src/bot.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { Bot } from './framework';
77

88
const bot = new Bot({
99
token: process.env.DISCORD_TOKEN,
10+
commands: path.join(__dirname, 'commands'),
1011
crons: path.join(__dirname, 'crons'),
1112
formatCheckers: path.join(__dirname, 'format-checkers'),
1213
});

src/commands/Hello.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { Command, CommandOptionTypes } from '../framework';
2+
3+
export default new Command({
4+
name: 'hello',
5+
description: 'Vous salue.',
6+
enabled: true,
7+
options: {
8+
stars: {
9+
description: "Nombre d'étoiles accompagnant le salut.",
10+
required: true,
11+
type: CommandOptionTypes.Integer,
12+
choices: [
13+
{ name: '1 étoile', value: 1 },
14+
{ name: '2 étoiles', value: 2 },
15+
{ name: '3 étoiles', value: 3 },
16+
{ name: '4 étoiles', value: 4 },
17+
{ name: '5 étoiles', value: 5 },
18+
] as const,
19+
},
20+
user: {
21+
description: 'Mentionne un utilisateur spécifique (vous par défaut).',
22+
type: CommandOptionTypes.User,
23+
},
24+
},
25+
handle({ args, interaction }) {
26+
return interaction.reply(
27+
`Salut ${(args.user ?? interaction.user).toString()} ${'⭐'.repeat(
28+
args.stars,
29+
)}`,
30+
);
31+
},
32+
});

src/framework/Bot.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ import path from 'path';
55
import { Client, Intents } from 'discord.js';
66
import pino from 'pino';
77

8-
import { Cron } from './Cron';
98
import { Base, BaseConfig } from './Base';
9+
import { Command, CommandManager } from './command';
10+
import { Cron } from './Cron';
1011
import { FormatChecker } from './FormatChecker';
1112

1213
export interface BotOptions {
@@ -15,6 +16,10 @@ export interface BotOptions {
1516
* Defaults to `process.env.DISCORD_TOKEN`.
1617
*/
1718
token?: string;
19+
/**
20+
* Directory that contains the `Command` definitions.
21+
*/
22+
commands?: string;
1823
/**
1924
* Directory that contains the `Cron` definitions.
2025
*/
@@ -27,11 +32,14 @@ export interface BotOptions {
2732

2833
type Constructor<T extends Base, U extends BaseConfig> = {
2934
new (config: U): T;
35+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
36+
new <Any>(config: U): T;
3037
};
3138

3239
export class Bot {
3340
private readonly token?: string;
3441
private _client: Client | null;
42+
private commandManager?: CommandManager;
3543
private crons: Cron[] = [];
3644
private formatCheckers: FormatChecker[] = [];
3745

@@ -42,9 +50,16 @@ export class Bot {
4250
this._client = null;
4351
this.logger = pino();
4452

53+
if (options.commands) {
54+
this.commandManager = new CommandManager(
55+
this.loadDirectory(options.commands, 'commands', Command),
56+
);
57+
}
58+
4559
if (options.crons) {
4660
this.crons = this.loadDirectory(options.crons, 'crons', Cron);
4761
}
62+
4863
if (options.formatCheckers) {
4964
this.formatCheckers = this.loadDirectory(
5065
options.formatCheckers,
@@ -151,6 +166,9 @@ export class Bot {
151166
this.client.login(this.token),
152167
once(this.client, 'ready'),
153168
]);
169+
if (this.commandManager) {
170+
await this.commandManager.start(this);
171+
}
154172
this.startCrons();
155173
this.startFormatCheckers();
156174
} catch (error) {
@@ -162,10 +180,13 @@ export class Bot {
162180
/**
163181
* Stop the bot.
164182
*/
165-
public stop(): void {
183+
public async stop(): Promise<void> {
166184
if (!this._client) {
167185
throw new Error('Bot was not started');
168186
}
187+
if (this.commandManager) {
188+
await this.commandManager.stop(this);
189+
}
169190
this.stopCrons();
170191
this.stopFormatCheckers();
171192
this._client.destroy();

0 commit comments

Comments
 (0)