From bd5ef564bb5c8af08d7cdf0a1b1496207ab63813 Mon Sep 17 00:00:00 2001 From: Puechberty Arthur Date: Sun, 12 Apr 2026 15:17:43 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20ajouter=20la=20gestion=20du=20cooldown?= =?UTF-8?q?=20par=20commande=20et=20mise=20=C3=A0=20jour=20des=20traductio?= =?UTF-8?q?ns?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 5 ++-- locales/en.json | 3 ++- locales/es.json | 3 ++- locales/fr.json | 3 ++- src/commands/utility/ping.ts | 1 + src/framework/commands/defineCommand.ts | 16 ++++++++++++ src/framework/execution/CommandExecutor.ts | 29 ++++++++++++++++++++++ src/framework/types/command.ts | 2 ++ 8 files changed, 57 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 4bc88b7..23a2652 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/locales/en.json b/locales/en.json index 720ba47..483a890 100644 --- a/locales/en.json +++ b/locales/en.json @@ -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", diff --git a/locales/es.json b/locales/es.json index 647004e..3e48958 100644 --- a/locales/es.json +++ b/locales/es.json @@ -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", diff --git a/locales/fr.json b/locales/fr.json index 3287ce1..d0ca897 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -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", diff --git a/src/commands/utility/ping.ts b/src/commands/utility/ping.ts index 8eccfa5..ca26f3b 100644 --- a/src/commands/utility/ping.ts +++ b/src/commands/utility/ping.ts @@ -6,6 +6,7 @@ export const pingCommand = defineCommand({ name: "ping", category: "utility", }, + cooldown: 5, examples: [ { descriptionKey: "examples.basic", diff --git a/src/framework/commands/defineCommand.ts b/src/framework/commands/defineCommand.ts index b95fea2..7fdd5fb 100644 --- a/src/framework/commands/defineCommand.ts +++ b/src/framework/commands/defineCommand.ts @@ -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, }; }; diff --git a/src/framework/execution/CommandExecutor.ts b/src/framework/execution/CommandExecutor.ts index f2f3ae3..98bef8e 100644 --- a/src/framework/execution/CommandExecutor.ts +++ b/src/framework/execution/CommandExecutor.ts @@ -8,6 +8,8 @@ import { import type { BotCommand, CommandExecutionContext } from "../types/command.js"; export class CommandExecutor { + private readonly cooldowns = new Map(); + public async run(command: BotCommand, ctx: CommandExecutionContext): Promise { 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}`; + } } diff --git a/src/framework/types/command.ts b/src/framework/types/command.ts index e2df09c..fe9c265 100644 --- a/src/framework/types/command.ts +++ b/src/framework/types/command.ts @@ -98,6 +98,7 @@ export interface BotCommandInput { args?: CommandArgument[]; permissions?: PermissionResolvable[]; examples?: CommandExample[]; + cooldown?: number; execute: (ctx: CommandExecutionContext) => Promise; } @@ -106,5 +107,6 @@ export interface BotCommand { args: CommandArgument[]; permissions: PermissionResolvable[]; examples: CommandExample[]; + cooldown?: number; execute: (ctx: CommandExecutionContext) => Promise; }