Skip to content

Commit c4576a5

Browse files
committed
wip: feat(framework): slash commands
1 parent fdae04d commit c4576a5

File tree

10 files changed

+193
-12713
lines changed

10 files changed

+193
-12713
lines changed

package-lock.json

Lines changed: 0 additions & 12683 deletions
This file was deleted.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
"homepage": "https://github.com/ES-Community/bot#readme",
2626
"dependencies": {
2727
"cron": "^1.8.2",
28-
"discord.js": "^12.5.3",
28+
"discord.js": "^13.0.0-dev.d433fe8.1626004976",
2929
"dotenv": "^10.0.0",
3030
"emoji-regex": "^9.2.2",
3131
"got": "^11.8.2",

src/bot.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Dotenv.config();
88

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

src/commands/Hello.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { Command } from '../framework';
2+
3+
export default new Command({
4+
name: 'hello',
5+
description: 'Vous répond Hello, world!',
6+
enabled: true,
7+
handle({ interaction }) {
8+
return interaction.reply('Hello, world!');
9+
},
10+
});

src/crons/CommitStrip.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,14 @@ export default new Cron({
2222
throw new Error('found no #gif channel');
2323
}
2424

25-
await channel.send(
26-
new MessageEmbed()
27-
.setTitle(latestCommitStrip.title)
28-
.setURL(latestCommitStrip.link)
29-
.setImage(latestCommitStrip.imageUrl),
30-
);
25+
await channel.send({
26+
embeds: [
27+
new MessageEmbed()
28+
.setTitle(latestCommitStrip.title)
29+
.setURL(latestCommitStrip.link)
30+
.setImage(latestCommitStrip.imageUrl),
31+
],
32+
});
3133
},
3234
});
3335

@@ -90,7 +92,7 @@ async function getRecentCommitStrip(now: Date): Promise<CommitStrip | null> {
9092

9193
const [strip] = posts;
9294

93-
const stripDate = new Date(strip.date_gmt + ".000Z");
95+
const stripDate = new Date(strip.date_gmt + '.000Z');
9496
const stripTime = stripDate.getTime();
9597
const nowTime = now.getTime();
9698
const thirtyMinutes = 1000 * 60 * 30;

src/crons/EpicGames.ts

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -32,22 +32,27 @@ export default new Cron({
3232
for (const game of games) {
3333
context.logger.info(`Found a new offered game (${game.title})`);
3434

35-
await channel.send(
36-
new MessageEmbed({ title: game.title, url: game.link })
37-
.setThumbnail(game.thumbnail)
38-
.addField(
39-
'Début',
40-
game.discountStartDate.toLocaleDateString('fr-FR', dateFmtOptions),
41-
true,
42-
)
43-
.addField(
44-
'Fin',
45-
game.discountEndDate.toLocaleDateString('fr-FR', dateFmtOptions),
46-
true,
47-
)
48-
.addField('Prix', `${game.originalPrice} → **Gratuit**`)
49-
.setTimestamp(),
50-
);
35+
await channel.send({
36+
embeds: [
37+
new MessageEmbed({ title: game.title, url: game.link })
38+
.setThumbnail(game.thumbnail)
39+
.addField(
40+
'Début',
41+
game.discountStartDate.toLocaleDateString(
42+
'fr-FR',
43+
dateFmtOptions,
44+
),
45+
true,
46+
)
47+
.addField(
48+
'Fin',
49+
game.discountEndDate.toLocaleDateString('fr-FR', dateFmtOptions),
50+
true,
51+
)
52+
.addField('Prix', `${game.originalPrice} → **Gratuit**`)
53+
.setTimestamp(),
54+
],
55+
});
5156
}
5257
},
5358
});

src/framework/Bot.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ import { once } from 'events';
22
import fs from 'fs';
33
import path from 'path';
44

5-
import { Client } from 'discord.js';
5+
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
*/
@@ -32,6 +37,7 @@ type Constructor<T extends Base, U extends BaseConfig> = {
3237
export class Bot {
3338
private readonly token?: string;
3439
private _client: Client | null;
40+
private commandManager?: CommandManager;
3541
private crons: Cron[] = [];
3642
private formatCheckers: FormatChecker[] = [];
3743

@@ -42,9 +48,16 @@ export class Bot {
4248
this._client = null;
4349
this.logger = pino();
4450

51+
if (options.commands) {
52+
this.commandManager = new CommandManager(
53+
this.loadDirectory(options.commands, 'commands', Command),
54+
);
55+
}
56+
4557
if (options.crons) {
4658
this.crons = this.loadDirectory(options.crons, 'crons', Cron);
4759
}
60+
4861
if (options.formatCheckers) {
4962
this.formatCheckers = this.loadDirectory(
5063
options.formatCheckers,
@@ -132,12 +145,18 @@ export class Bot {
132145
if (this._client) {
133146
throw new Error('Bot can only be started once');
134147
}
135-
this._client = new Client();
148+
this._client = new Client({
149+
intents: new Intents(['GUILDS', 'GUILD_MESSAGES']),
150+
});
151+
136152
try {
137153
await Promise.all([
138154
this.client.login(this.token),
139155
once(this.client, 'ready'),
140156
]);
157+
if (this.commandManager) {
158+
await this.commandManager.start(this);
159+
}
141160
this.startCrons();
142161
this.startFormatCheckers();
143162
} catch (error) {
@@ -149,10 +168,13 @@ export class Bot {
149168
/**
150169
* Stop the bot.
151170
*/
152-
public stop(): void {
171+
public async stop(): Promise<void> {
153172
if (!this._client) {
154173
throw new Error('Bot was not started');
155174
}
175+
if (this.commandManager) {
176+
await this.commandManager.stop(this);
177+
}
156178
this.stopCrons();
157179
this.stopFormatCheckers();
158180
this._client.destroy();

src/framework/Command.ts

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { randomUUID } from 'crypto';
2+
import { Client, CommandInteraction, Interaction, Snowflake } from 'discord.js';
3+
import { Logger } from 'pino';
4+
import { Base, BaseConfig } from './Base';
5+
import { Bot } from './Bot';
6+
7+
export class CommandManager {
8+
/**
9+
* Registered slash commands.
10+
*/
11+
public readonly commands = new Map<Snowflake, Command>();
12+
13+
private bot!: Bot;
14+
15+
public constructor(private holds: Command[]) {
16+
this._interactionHandler = this._interactionHandler.bind(this);
17+
}
18+
19+
public async _interactionHandler(interaction: Interaction): Promise<void> {
20+
if (!interaction.isCommand()) {
21+
return;
22+
}
23+
24+
const command = this.commands.get(interaction.commandId);
25+
26+
if (!command) {
27+
this.bot.logger
28+
.child({
29+
id: randomUUID(),
30+
type: 'CommandManager',
31+
})
32+
.error(
33+
'unregistred slash command %s (id: %s)',
34+
interaction.commandName,
35+
interaction.commandId,
36+
);
37+
return;
38+
}
39+
40+
const logger = this.bot.logger.child({
41+
id: randomUUID(),
42+
type: 'Command',
43+
commandName: command.name,
44+
});
45+
46+
try {
47+
logger.debug('execute command handler');
48+
await command.handler({ client: this.bot.client, logger, interaction });
49+
} catch (error) {
50+
logger.error(error, 'command handler error');
51+
}
52+
}
53+
54+
public async start(bot: Bot): Promise<void> {
55+
this.bot = bot;
56+
57+
// The application cannot be null if the Client is ready.
58+
const application = this.bot.client.application!; // eslint-disable-line @typescript-eslint/no-non-null-assertion
59+
60+
await Promise.all(
61+
this.holds.map(async (hold) => {
62+
const command = await application.commands.create({
63+
name: hold.name,
64+
description: hold.description,
65+
});
66+
this.commands.set(command.id, hold);
67+
}),
68+
);
69+
70+
// We don't need this.holds anymore.
71+
this.holds = [];
72+
73+
this.bot.client.on('interactionCreate', this._interactionHandler);
74+
}
75+
76+
public async stop(bot: Bot): Promise<void> {
77+
bot.client.off('interactionCreate', this._interactionHandler);
78+
79+
// The application cannot be null if the Client is ready.
80+
const application = bot.client.application!; // eslint-disable-line @typescript-eslint/no-non-null-assertion
81+
82+
// Unregister *all* registered slash commands.
83+
await Promise.all(
84+
[...application.commands.cache.values()].map((command) =>
85+
command.delete(),
86+
),
87+
);
88+
}
89+
}
90+
91+
export type CommandHandler = (context: CommandContext) => Promise<unknown>;
92+
93+
export interface CommandContext {
94+
/**
95+
* discord.js Client instance.
96+
*/
97+
client: Client;
98+
/**
99+
* Pino logger.
100+
*/
101+
logger: Logger;
102+
/**
103+
* CommandInteraction instance.
104+
*/
105+
interaction: CommandInteraction;
106+
}
107+
108+
export interface CommandConfig extends BaseConfig {
109+
/**
110+
* Command handler.
111+
*/
112+
handle: CommandHandler;
113+
}
114+
115+
export class Command extends Base {
116+
public readonly handler: CommandHandler;
117+
118+
public constructor(config: CommandConfig) {
119+
super(config);
120+
this.handler = config.handle;
121+
}
122+
}

src/framework/FormatChecker.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,11 +109,11 @@ export class FormatChecker extends Base {
109109

110110
public start(bot: Bot): void {
111111
this.bot = bot;
112-
this.bot.client.on('message', this._messageHandler);
112+
this.bot.client.on('messageCreate', this._messageHandler);
113113
this.bot.client.on('messageUpdate', this._messageUpdateHandler);
114114
}
115115

116116
public stop(bot: Bot): void {
117-
bot.client.off('message', this._messageHandler);
117+
bot.client.off('messageCreate', this._messageHandler);
118118
}
119119
}

src/framework/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from './Bot';
2+
export * from './Command';
23
export * from './Cron';
34
export * from './FormatChecker';
45
export * from './helpers';

0 commit comments

Comments
 (0)