feat: ajouter la gestion du cooldown par commande et mise à jour des traductions

This commit is contained in:
Puechberty Arthur
2026-04-12 15:17:43 +02:00
parent e5781cbb51
commit bd5ef564bb
8 changed files with 57 additions and 5 deletions
+3 -2
View File
@@ -4,6 +4,7 @@ Professional command framework template for Discord.js `14.26.2` with:
- Single command object schema
- Minimal command authoring (`name`, `category`, `execute`)
- Optional per-user command cooldown (`cooldown` in seconds)
- Shared execution logic for prefix and slash
- External JSON i18n
- Automatic prefix and slash localizations from locale files
@@ -69,13 +70,13 @@ An example with two bot services is available in `docker-compose.multi-bot.examp
- `src/framework/commands/defineCommand.ts`: default command completion
- `src/framework/commands/registry.ts`: trigger/name mapping generated from locales
- `src/framework/commands/argParser.ts`: prefix/slash args parsing from schema
- `src/framework/execution/CommandExecutor.ts`: unified pipeline (permissions/execute)
- `src/framework/execution/CommandExecutor.ts`: unified pipeline (permissions/cooldown/execute)
- `src/framework/handlers/prefixHandler.ts`: prefix entrypoint
- `src/framework/handlers/slashHandler.ts`: slash entrypoint
- `src/framework/presence/presenceStore.ts`: PostgreSQL presence storage
- `src/framework/presence/presenceTypes.ts`: shared presence types/validation
- `locales/*.json`: external i18n dictionaries
- `src/commands/*`: business commands only (`execute`)
- `src/commands/*`: business commands only (`execute`, optional `cooldown`)
## Included Commands
+2 -1
View File
@@ -11,7 +11,8 @@
"permissions": {
"user": "You are missing permissions: {{permissions}}"
},
"execution": "An unexpected error happened while running this command."
"execution": "An unexpected error happened while running this command.",
"cooldown": "Please wait {{seconds}}s before using this command again."
},
"categories": {
"fun": "Fun",
+2 -1
View File
@@ -11,7 +11,8 @@
"permissions": {
"user": "Te faltan permisos: {{permissions}}"
},
"execution": "Ocurrio un error inesperado al ejecutar este comando."
"execution": "Ocurrio un error inesperado al ejecutar este comando.",
"cooldown": "Espera {{seconds}}s antes de volver a usar este comando."
},
"categories": {
"fun": "Diversion",
+2 -1
View File
@@ -11,7 +11,8 @@
"permissions": {
"user": "Tu n as pas les permissions: {{permissions}}"
},
"execution": "Une erreur inattendue est survenue pendant l execution."
"execution": "Une erreur inattendue est survenue pendant l execution.",
"cooldown": "Patiente {{seconds}}s avant de reutiliser cette commande."
},
"categories": {
"fun": "Fun",
+1
View File
@@ -6,6 +6,7 @@ export const pingCommand = defineCommand({
name: "ping",
category: "utility",
},
cooldown: 5,
examples: [
{
descriptionKey: "examples.basic",
+16
View File
@@ -18,14 +18,30 @@ const assertRequiredArgsBeforeOptional = (input: BotCommandInput): void => {
}
};
const normalizeCooldown = (input: BotCommandInput): number | undefined => {
if (input.cooldown === undefined) {
return undefined;
}
if (!Number.isFinite(input.cooldown) || input.cooldown <= 0) {
throw new Error(
`Invalid cooldown for command "${input.meta.name}": expected a positive number of seconds, received "${input.cooldown}".`,
);
}
return input.cooldown;
};
export const defineCommand = (input: BotCommandInput): BotCommand => {
assertRequiredArgsBeforeOptional(input);
const cooldown = normalizeCooldown(input);
return {
meta: input.meta,
args: [...(input.args ?? [])],
permissions: input.permissions ?? [],
examples: input.examples ?? [],
...(cooldown !== undefined ? { cooldown } : {}),
execute: input.execute,
};
};
@@ -8,6 +8,8 @@ import {
import type { BotCommand, CommandExecutionContext } from "../types/command.js";
export class CommandExecutor {
private readonly cooldowns = new Map<string, number>();
public async run(command: BotCommand, ctx: CommandExecutionContext): Promise<void> {
const missingUserPermissions = this.getMissingPermissions(command.permissions, this.memberPermissions(ctx));
if (missingUserPermissions.length > 0) {
@@ -15,6 +17,12 @@ export class CommandExecutor {
return;
}
const remainingCooldownSeconds = this.consumeCooldown(command, ctx.user.id);
if (remainingCooldownSeconds > 0) {
await ctx.reply(ctx.t("errors.cooldown", { seconds: remainingCooldownSeconds }));
return;
}
try {
await command.execute(ctx);
} catch (error) {
@@ -73,4 +81,25 @@ export class CommandExecutor {
.replace(/_/g, " ")
.trim();
}
private consumeCooldown(command: BotCommand, userId: string): number {
if (command.cooldown === undefined || command.cooldown <= 0) {
return 0;
}
const key = this.cooldownKey(command.meta.name, userId);
const now = Date.now();
const expiresAt = this.cooldowns.get(key);
if (expiresAt !== undefined && expiresAt > now) {
return Math.ceil((expiresAt - now) / 1000);
}
this.cooldowns.set(key, now + command.cooldown * 1000);
return 0;
}
private cooldownKey(commandName: string, userId: string): string {
return `${commandName}:${userId}`;
}
}
+2
View File
@@ -98,6 +98,7 @@ export interface BotCommandInput {
args?: CommandArgument[];
permissions?: PermissionResolvable[];
examples?: CommandExample[];
cooldown?: number;
execute: (ctx: CommandExecutionContext) => Promise<void>;
}
@@ -106,5 +107,6 @@ export interface BotCommand {
args: CommandArgument[];
permissions: PermissionResolvable[];
examples: CommandExample[];
cooldown?: number;
execute: (ctx: CommandExecutionContext) => Promise<void>;
}