Skip to content

Commit

Permalink
refactor(messageCreate): move commands handling into specific file
Browse files Browse the repository at this point in the history
  • Loading branch information
Tinaël Devresse committed Oct 23, 2024
1 parent 1b0315f commit 8c92451
Show file tree
Hide file tree
Showing 13 changed files with 141 additions and 86 deletions.
3 changes: 2 additions & 1 deletion src/commands/announce.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ChannelType, Colors, EmbedBuilder, Message, TextChannel } from 'discord.js';

import { DatadropClient } from '../datadrop.js';
import { Command } from '../models/Command.js';

export default {
name: 'announce',
Expand Down Expand Up @@ -58,4 +59,4 @@ export default {
await message.channel.send('Annonce annulée!');
}
},
};
} as Command;
3 changes: 2 additions & 1 deletion src/commands/email.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { EmbedBuilder, Message } from 'discord.js';

import { DatadropClient } from '../datadrop.js';
import { Command } from '../models/Command.js';

const people = [
{
Expand Down Expand Up @@ -33,4 +34,4 @@ export default {
if (message.channel.isSendable())
await message.channel.send({ embeds: [embed] });
}
};
} as Command;
3 changes: 2 additions & 1 deletion src/commands/eval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { DatadropClient } from '../datadrop.js';
import { clean } from '../helpers.js';
import { Command } from '../models/Command.js';

export default {
name: 'eval',
Expand Down Expand Up @@ -33,4 +34,4 @@ export default {
message.channel.send(codeBlock('xl', content));
}
}
};
} as Command;
2 changes: 1 addition & 1 deletion src/commands/help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export default {
aliases: ['commandes'],
usage: '[commande]',

execute: async (client: DatadropClient, message: Message, args: string[]) => {
async execute(client: DatadropClient, message: Message, args: string[]) {
const { prefix } = client.config;
const { commands } = client;
let embed: EmbedBuilder;
Expand Down
3 changes: 2 additions & 1 deletion src/commands/link.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, Message } from 'discord.js';

import { DatadropClient } from '../datadrop.js';
import { Command } from '../models/Command.js';

export default {
name: 'link',
Expand Down Expand Up @@ -28,4 +29,4 @@ export default {
components: [buttonComponent]
});
},
};
} as Command;
3 changes: 2 additions & 1 deletion src/commands/ping.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Message } from 'discord.js';

import { DatadropClient } from '../datadrop.js';
import { Command } from '../models/Command.js';

export default {
name: 'ping',
Expand All @@ -10,4 +11,4 @@ export default {
const msg = await message.reply('Calcul en cours...');
await msg.edit(`Pong: ${client.ws.ping} ms`);
},
};
} as Command;
5 changes: 3 additions & 2 deletions src/commands/pinmsg.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { Message, MessageReference, MessageResolvable } from 'discord.js';

import { DatadropClient } from '../datadrop.js';
import { Command } from '../models/Command.js';

export default {
name: 'pinmsg',
description: "Ajoute ou retire le message lié des épinglés du canal si l'utilisateur fait partie du rôle 'Professeur(e)'.\n\nLes arguments possibles sont:\n • `--verbose` ou `-v` pour avoir un retour texte pour la commande\n",
aliases: ['pin', 'épingler', 'unpin', 'unpinmsg', 'désépingler'],
guildOnly: true,
usage: '[args]',
usage: '[-v | --verbose]',

async execute(client: DatadropClient, message: Message, args: string[]) {
if (!message.guild || !message.member || !message.reference) return;
Expand All @@ -30,7 +31,7 @@ export default {
}
client.logger.info(`Le membre <${message.member.displayName}> (${message.member.id}) a épinglé/désépinglé le message <${referencedMessage.id}>.`);
}
};
} as Command;

async function replyOnAction(message: Message, emoji: string, content: string, verboseIsActive?: boolean): Promise<void> {
if (verboseIsActive && message.channel.isSendable()) {
Expand Down
3 changes: 2 additions & 1 deletion src/commands/reload.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Message } from 'discord.js';

import { DatadropClient } from '../datadrop.js';
import { Command } from '../models/Command.js';

export default {
name: 'reload',
Expand All @@ -13,4 +14,4 @@ export default {
await client.reloadConfig();
await message.reply(ok_hand);
}
};
} as Command;
3 changes: 2 additions & 1 deletion src/commands/restart.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Message } from 'discord.js';

import { DatadropClient } from '../datadrop.js';
import { Command } from '../models/Command.js';

export default {
name: 'restart',
Expand All @@ -13,4 +14,4 @@ export default {
await message.reply(ok_hand);
process.exit();
}
};
} as Command;
3 changes: 2 additions & 1 deletion src/datadrop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@ import { Configuration } from './models/Configuration.js';
import { readConfig } from './config.js';
import { User } from './models/User.js';
import { IDatabaseService } from './models/IDatabaseService.js';
import { Command } from './models/Command.js';

export class DatadropClient extends Client {
#config: Configuration;
readonly database: IDatabaseService;
readonly logger: DefaultLogger;
readonly commands: Collection<string, any>;
readonly commands: Collection<string, Command>;
readonly selfRoleManager: InteractionsSelfRoleManager;
readonly tempChannelsManager: TempChannelsManager;
readonly verificationManager: VerificationManager<User>;
Expand Down
82 changes: 7 additions & 75 deletions src/events/messageCreate.ts
Original file line number Diff line number Diff line change
@@ -1,81 +1,13 @@
import { ChannelType, Message } from 'discord.js';
import { DatadropClient } from '../datadrop.js';
import { Message } from 'discord.js';

const escapeRegex = (str: string | null | undefined) => str?.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
import { DatadropClient } from '../datadrop.js';
import { CommandHandler } from '../services/CommandHandler.js';

export default async function messageCreate(client: DatadropClient, message: Message) {
if (message.author.bot) return;

const lowerCasedContent = message.content.toLowerCase();
const prefixRegex = new RegExp(`^(<@!?${client.user!.id}>|${escapeRegex(client.config.prefix)})\\s*`);
if (!isCommand(lowerCasedContent, prefixRegex)) return;

const commandName = getCommandNameFromMessage(message.content, prefixRegex);
const command = getCommand(client, commandName);
if (!command) return;

logCommandUsage(client, message, command);

if (!isCommandAllowed(client, message, command)) return;

try {
executeCommand(client, message, command);
} catch (err) {
handleCommandError(client, message);
}
}

function isCommand(content: string, prefixRegex: RegExp): boolean {
return prefixRegex.test(content.split(' ').join(''));
}

function getCommandNameFromMessage(content: string, prefixRegex: RegExp): string {
const matches = prefixRegex.exec(content);
if (!matches) return '';
const [, matchedPrefix] = matches;
const args = content.slice(matchedPrefix.length).trim().split(/ +/g) ?? [];
if (!args || args.length === 0) return '';
return args.shift()!.toLowerCase();
}
if (message.author.bot) return;

function getCommand(client: DatadropClient, commandName: string) {
return client.commands.get(commandName) || client.commands.find((cmd) => cmd.aliases?.includes(commandName));
}

function logCommandUsage(client: DatadropClient, message: Message, command: any) {
const channelInfo = message.channel.type === ChannelType.GuildText ? `dans #${message.channel.name} (${message.channel.id})` : 'en DM';
client.logger.info(`${message.author.tag} (${message.author.id}) a utilisé '${command.name}' ${channelInfo}`);
}

function isCommandAllowed(client: DatadropClient, message: Message, command: any): boolean {
const { ownerIds, communitymanagerRoleid, adminRoleid } = client.config;

const isAuthorized = ownerIds.includes(message.author.id) || message.member!.roles.cache.get(communitymanagerRoleid) || message.member!.roles.cache.get(adminRoleid);
if ((command.adminOnly || command.ownerOnly) && !isAuthorized) return false;

if (command.guildOnly && message.channel.type !== ChannelType.GuildText) {
message.reply("Je ne peux pas exécuter cette commande en dehors d'une guilde!");
return false;
}

if (command.args && !message.content.includes(' ')) {
const reply = `Vous n'avez pas donné d'arguments, ${message.author}!`;
if (command.usage) {
message.reply(`${reply}\nL'utilisation correcte de cette commande est : \`${message.content} ${command.usage}\``);
} else {
message.reply(reply);
const commandHandler = new CommandHandler(client);
if (commandHandler.shouldExecute(message.content.toLowerCase())) {
await commandHandler.execute(message);
}
return false;
}

return true;
}

function executeCommand(client: DatadropClient, message: Message, command: any) {
command.execute(client, message, message.content.split(' ').slice(1));
}

function handleCommandError(client: DatadropClient, message: Message) {
client.logger.error('An error occurred while executing the command');
message.reply(":x: **Oups!** - Une erreur est apparue en essayant cette commande. Reporte-le à un membre du Staff s'il te plaît!");
}
15 changes: 15 additions & 0 deletions src/models/Command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Message } from 'discord.js';

import { DatadropClient } from '../datadrop.js';

export interface Command {
name: string;
aliases?: string[];
args?: boolean;
description: string;
usage?: string;
guildOnly?: boolean;
ownerOnly?: boolean;

execute(client: DatadropClient, message: Message, args: string[]): Promise<void>;
}
99 changes: 99 additions & 0 deletions src/services/CommandHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { ChannelType, Message } from 'discord.js';

import { DatadropClient } from '../datadrop.js';
import { Command } from '../models/Command.js';

const escapeRegex = (str: string | null | undefined) => str?.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');

type AuthorizationResponse = {
error?: string;
};

export class CommandHandler {
#prefixRegex: RegExp;
#client: DatadropClient;

constructor(client: DatadropClient) {
this.#client = client;
this.#prefixRegex = new RegExp(`^(<@!?${client.user!.id}>|${escapeRegex(client.config.prefix)})\\s*`);
}

shouldExecute(content: string): boolean {
return this.#prefixRegex.test(content.split(' ').join(''));
}

async execute(message: Message): Promise<void> {
const args = this.#getArgs(message.content);
const command = this.#inferCommandFromArgs(args);
if (!command) {
await message.reply(":x: **Oups!** - Cette commande n'existe pas. Utilisez la commande `help` pour voir la liste des commandes disponibles.");
return;
}

const authorizationResponse = this.#checkAuthorization(message, command);
this.#logUsage(message, command, !!authorizationResponse.error);

if (authorizationResponse.error) {
await message.reply(authorizationResponse.error);
return;
}

try {
await command.execute(this.#client, message, args);
} catch (err) {
this.#client.logger.error(`Une erreur est survenue lors de l'exécution de la commande "${command.name}": ${err}`);
await message.reply(":x: **Oups!** - Une erreur est apparue en essayant cette commande. Reporte-le à un membre du Staff s'il te plaît!");
}
}

#getArgs(content: string): string[] {
const matches = this.#prefixRegex.exec(content);
if (!matches) return [];
const [, matchedPrefix] = matches;
return content.slice(matchedPrefix.length).trim().split(/ +/g) ?? [];
}

#inferCommandFromArgs(args: string[]): Command | undefined {
if (!args || args.length === 0) return;
const commandName = args.shift()!.toLowerCase();

return this.#client.commands.get(commandName) || this.#client.commands.find((cmd) => cmd.aliases?.includes(commandName));
}

/**
* Side-effect function to log the usage of the command.
* @param message The message
* @param command The command
* @param isAuthorized Whether the user responsible of the message is authorized to use the command or not.
*/
#logUsage(message: Message, command: any, isAuthorized: boolean): void {
const channelInfo = message.channel.type === ChannelType.GuildText ? `dans #${message.channel.name} (${message.channel.id})` : 'en DM';
this.#client.logger.info(`${message.author.tag} (${message.author.id}) a utilisé '${command.name}' ${channelInfo} ${isAuthorized ? 'avec' : 'sans'} autorisation`);
}

#checkAuthorization(message: Message, command: any): AuthorizationResponse {
const { ownerIds, communitymanagerRoleid, adminRoleid } = this.#client.config;

const canBypassAuthorization = ownerIds.includes(message.author.id)
|| message.member!.roles.cache.get(communitymanagerRoleid)
|| message.member!.roles.cache.get(adminRoleid);
if ((command.adminOnly || command.ownerOnly) && !canBypassAuthorization) {
return { error: ":x: **Oups!** - Cette commande est réservée à un nombre limité de personnes dont vous ne faites pas partie." };
}

if (command.guildOnly && message.channel.type !== ChannelType.GuildText) {
return { error: ":x: **Oups!** - Je ne peux pas exécuter cette commande en dehors d'une guilde!" };
}

if (command.args && !message.content.slice(command.name.length).startsWith(' ')) {
const reply = `:x: **Oups!** - Vous n'avez pas donné d'arguments, ${message.author}!`;
return {
error: command.usage
? `${reply}\nL'utilisation correcte de cette commande est : \`${message.content} ${command.usage}\``
: reply
};
}

return {};
}
}

0 comments on commit 8c92451

Please sign in to comment.