Skip to content

Commit

Permalink
wip: feat(framework): slash commands
Browse files Browse the repository at this point in the history
  • Loading branch information
Mesteery committed Jul 11, 2021
1 parent fdae04d commit c4576a5
Show file tree
Hide file tree
Showing 10 changed files with 193 additions and 12,713 deletions.
12,683 changes: 0 additions & 12,683 deletions package-lock.json

This file was deleted.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"homepage": "https://github.com/ES-Community/bot#readme",
"dependencies": {
"cron": "^1.8.2",
"discord.js": "^12.5.3",
"discord.js": "^13.0.0-dev.d433fe8.1626004976",
"dotenv": "^10.0.0",
"emoji-regex": "^9.2.2",
"got": "^11.8.2",
Expand Down
1 change: 1 addition & 0 deletions src/bot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Dotenv.config();

const bot = new Bot({
token: process.env.DISCORD_TOKEN,
commands: path.join(__dirname, 'commands'),
crons: path.join(__dirname, 'crons'),
formatCheckers: path.join(__dirname, 'format-checkers'),
});
Expand Down
10 changes: 10 additions & 0 deletions src/commands/Hello.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Command } from '../framework';

export default new Command({
name: 'hello',
description: 'Vous répond Hello, world!',
enabled: true,
handle({ interaction }) {
return interaction.reply('Hello, world!');
},
});
16 changes: 9 additions & 7 deletions src/crons/CommitStrip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,14 @@ export default new Cron({
throw new Error('found no #gif channel');
}

await channel.send(
new MessageEmbed()
.setTitle(latestCommitStrip.title)
.setURL(latestCommitStrip.link)
.setImage(latestCommitStrip.imageUrl),
);
await channel.send({
embeds: [
new MessageEmbed()
.setTitle(latestCommitStrip.title)
.setURL(latestCommitStrip.link)
.setImage(latestCommitStrip.imageUrl),
],
});
},
});

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

const [strip] = posts;

const stripDate = new Date(strip.date_gmt + ".000Z");
const stripDate = new Date(strip.date_gmt + '.000Z');
const stripTime = stripDate.getTime();
const nowTime = now.getTime();
const thirtyMinutes = 1000 * 60 * 30;
Expand Down
37 changes: 21 additions & 16 deletions src/crons/EpicGames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,22 +32,27 @@ export default new Cron({
for (const game of games) {
context.logger.info(`Found a new offered game (${game.title})`);

await channel.send(
new MessageEmbed({ title: game.title, url: game.link })
.setThumbnail(game.thumbnail)
.addField(
'Début',
game.discountStartDate.toLocaleDateString('fr-FR', dateFmtOptions),
true,
)
.addField(
'Fin',
game.discountEndDate.toLocaleDateString('fr-FR', dateFmtOptions),
true,
)
.addField('Prix', `${game.originalPrice} → **Gratuit**`)
.setTimestamp(),
);
await channel.send({
embeds: [
new MessageEmbed({ title: game.title, url: game.link })
.setThumbnail(game.thumbnail)
.addField(
'Début',
game.discountStartDate.toLocaleDateString(
'fr-FR',
dateFmtOptions,
),
true,
)
.addField(
'Fin',
game.discountEndDate.toLocaleDateString('fr-FR', dateFmtOptions),
true,
)
.addField('Prix', `${game.originalPrice} → **Gratuit**`)
.setTimestamp(),
],
});
}
},
});
Expand Down
30 changes: 26 additions & 4 deletions src/framework/Bot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import { once } from 'events';
import fs from 'fs';
import path from 'path';

import { Client } from 'discord.js';
import { Client, Intents } from 'discord.js';
import pino from 'pino';

import { Cron } from './Cron';
import { Base, BaseConfig } from './Base';
import { Command, CommandManager } from './Command';
import { Cron } from './Cron';
import { FormatChecker } from './FormatChecker';

export interface BotOptions {
Expand All @@ -15,6 +16,10 @@ export interface BotOptions {
* Defaults to `process.env.DISCORD_TOKEN`.
*/
token?: string;
/**
* Directory that contains the `Command` definitions.
*/
commands?: string;
/**
* Directory that contains the `Cron` definitions.
*/
Expand All @@ -32,6 +37,7 @@ type Constructor<T extends Base, U extends BaseConfig> = {
export class Bot {
private readonly token?: string;
private _client: Client | null;
private commandManager?: CommandManager;
private crons: Cron[] = [];
private formatCheckers: FormatChecker[] = [];

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

if (options.commands) {
this.commandManager = new CommandManager(
this.loadDirectory(options.commands, 'commands', Command),
);
}

if (options.crons) {
this.crons = this.loadDirectory(options.crons, 'crons', Cron);
}

if (options.formatCheckers) {
this.formatCheckers = this.loadDirectory(
options.formatCheckers,
Expand Down Expand Up @@ -132,12 +145,18 @@ export class Bot {
if (this._client) {
throw new Error('Bot can only be started once');
}
this._client = new Client();
this._client = new Client({
intents: new Intents(['GUILDS', 'GUILD_MESSAGES']),
});

try {
await Promise.all([
this.client.login(this.token),
once(this.client, 'ready'),
]);
if (this.commandManager) {
await this.commandManager.start(this);
}
this.startCrons();
this.startFormatCheckers();
} catch (error) {
Expand All @@ -149,10 +168,13 @@ export class Bot {
/**
* Stop the bot.
*/
public stop(): void {
public async stop(): Promise<void> {
if (!this._client) {
throw new Error('Bot was not started');
}
if (this.commandManager) {
await this.commandManager.stop(this);
}
this.stopCrons();
this.stopFormatCheckers();
this._client.destroy();
Expand Down
122 changes: 122 additions & 0 deletions src/framework/Command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { randomUUID } from 'crypto';
import { Client, CommandInteraction, Interaction, Snowflake } from 'discord.js';
import { Logger } from 'pino';
import { Base, BaseConfig } from './Base';
import { Bot } from './Bot';

export class CommandManager {
/**
* Registered slash commands.
*/
public readonly commands = new Map<Snowflake, Command>();

private bot!: Bot;

public constructor(private holds: Command[]) {
this._interactionHandler = this._interactionHandler.bind(this);
}

public async _interactionHandler(interaction: Interaction): Promise<void> {
if (!interaction.isCommand()) {
return;
}

const command = this.commands.get(interaction.commandId);

if (!command) {
this.bot.logger
.child({
id: randomUUID(),
type: 'CommandManager',
})
.error(
'unregistred slash command %s (id: %s)',
interaction.commandName,
interaction.commandId,
);
return;
}

const logger = this.bot.logger.child({
id: randomUUID(),
type: 'Command',
commandName: command.name,
});

try {
logger.debug('execute command handler');
await command.handler({ client: this.bot.client, logger, interaction });
} catch (error) {
logger.error(error, 'command handler error');
}
}

public async start(bot: Bot): Promise<void> {
this.bot = bot;

// The application cannot be null if the Client is ready.
const application = this.bot.client.application!; // eslint-disable-line @typescript-eslint/no-non-null-assertion

await Promise.all(
this.holds.map(async (hold) => {
const command = await application.commands.create({
name: hold.name,
description: hold.description,
});
this.commands.set(command.id, hold);
}),
);

// We don't need this.holds anymore.
this.holds = [];

this.bot.client.on('interactionCreate', this._interactionHandler);
}

public async stop(bot: Bot): Promise<void> {
bot.client.off('interactionCreate', this._interactionHandler);

// The application cannot be null if the Client is ready.
const application = bot.client.application!; // eslint-disable-line @typescript-eslint/no-non-null-assertion

// Unregister *all* registered slash commands.
await Promise.all(
[...application.commands.cache.values()].map((command) =>
command.delete(),
),
);
}
}

export type CommandHandler = (context: CommandContext) => Promise<unknown>;

export interface CommandContext {
/**
* discord.js Client instance.
*/
client: Client;
/**
* Pino logger.
*/
logger: Logger;
/**
* CommandInteraction instance.
*/
interaction: CommandInteraction;
}

export interface CommandConfig extends BaseConfig {
/**
* Command handler.
*/
handle: CommandHandler;
}

export class Command extends Base {
public readonly handler: CommandHandler;

public constructor(config: CommandConfig) {
super(config);
this.handler = config.handle;
}
}
4 changes: 2 additions & 2 deletions src/framework/FormatChecker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,11 +109,11 @@ export class FormatChecker extends Base {

public start(bot: Bot): void {
this.bot = bot;
this.bot.client.on('message', this._messageHandler);
this.bot.client.on('messageCreate', this._messageHandler);
this.bot.client.on('messageUpdate', this._messageUpdateHandler);
}

public stop(bot: Bot): void {
bot.client.off('message', this._messageHandler);
bot.client.off('messageCreate', this._messageHandler);
}
}
1 change: 1 addition & 0 deletions src/framework/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './Bot';
export * from './Command';
export * from './Cron';
export * from './FormatChecker';
export * from './helpers';

0 comments on commit c4576a5

Please sign in to comment.