mirror of
https://github.com/arthur-pbty/flint.git
synced 2026-08-01 20:29:03 +02:00
feat: ajouter la gestion du cooldown par commande et mise à jour des traductions
This commit is contained in:
@@ -4,6 +4,7 @@ Professional command framework template for Discord.js `14.26.2` with:
|
|||||||
|
|
||||||
- Single command object schema
|
- Single command object schema
|
||||||
- Minimal command authoring (`name`, `category`, `execute`)
|
- Minimal command authoring (`name`, `category`, `execute`)
|
||||||
|
- Optional per-user command cooldown (`cooldown` in seconds)
|
||||||
- Shared execution logic for prefix and slash
|
- Shared execution logic for prefix and slash
|
||||||
- External JSON i18n
|
- External JSON i18n
|
||||||
- Automatic prefix and slash localizations from locale files
|
- 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/defineCommand.ts`: default command completion
|
||||||
- `src/framework/commands/registry.ts`: trigger/name mapping generated from locales
|
- `src/framework/commands/registry.ts`: trigger/name mapping generated from locales
|
||||||
- `src/framework/commands/argParser.ts`: prefix/slash args parsing from schema
|
- `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/prefixHandler.ts`: prefix entrypoint
|
||||||
- `src/framework/handlers/slashHandler.ts`: slash entrypoint
|
- `src/framework/handlers/slashHandler.ts`: slash entrypoint
|
||||||
- `src/framework/presence/presenceStore.ts`: PostgreSQL presence storage
|
- `src/framework/presence/presenceStore.ts`: PostgreSQL presence storage
|
||||||
- `src/framework/presence/presenceTypes.ts`: shared presence types/validation
|
- `src/framework/presence/presenceTypes.ts`: shared presence types/validation
|
||||||
- `locales/*.json`: external i18n dictionaries
|
- `locales/*.json`: external i18n dictionaries
|
||||||
- `src/commands/*`: business commands only (`execute`)
|
- `src/commands/*`: business commands only (`execute`, optional `cooldown`)
|
||||||
|
|
||||||
## Included Commands
|
## Included Commands
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -11,7 +11,8 @@
|
|||||||
"permissions": {
|
"permissions": {
|
||||||
"user": "You are missing permissions: {{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": {
|
"categories": {
|
||||||
"fun": "Fun",
|
"fun": "Fun",
|
||||||
|
|||||||
+2
-1
@@ -11,7 +11,8 @@
|
|||||||
"permissions": {
|
"permissions": {
|
||||||
"user": "Te faltan permisos: {{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": {
|
"categories": {
|
||||||
"fun": "Diversion",
|
"fun": "Diversion",
|
||||||
|
|||||||
+2
-1
@@ -11,7 +11,8 @@
|
|||||||
"permissions": {
|
"permissions": {
|
||||||
"user": "Tu n as pas les permissions: {{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": {
|
"categories": {
|
||||||
"fun": "Fun",
|
"fun": "Fun",
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export const pingCommand = defineCommand({
|
|||||||
name: "ping",
|
name: "ping",
|
||||||
category: "utility",
|
category: "utility",
|
||||||
},
|
},
|
||||||
|
cooldown: 5,
|
||||||
examples: [
|
examples: [
|
||||||
{
|
{
|
||||||
descriptionKey: "examples.basic",
|
descriptionKey: "examples.basic",
|
||||||
|
|||||||
@@ -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 => {
|
export const defineCommand = (input: BotCommandInput): BotCommand => {
|
||||||
assertRequiredArgsBeforeOptional(input);
|
assertRequiredArgsBeforeOptional(input);
|
||||||
|
const cooldown = normalizeCooldown(input);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
meta: input.meta,
|
meta: input.meta,
|
||||||
args: [...(input.args ?? [])],
|
args: [...(input.args ?? [])],
|
||||||
permissions: input.permissions ?? [],
|
permissions: input.permissions ?? [],
|
||||||
examples: input.examples ?? [],
|
examples: input.examples ?? [],
|
||||||
|
...(cooldown !== undefined ? { cooldown } : {}),
|
||||||
execute: input.execute,
|
execute: input.execute,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import {
|
|||||||
import type { BotCommand, CommandExecutionContext } from "../types/command.js";
|
import type { BotCommand, CommandExecutionContext } from "../types/command.js";
|
||||||
|
|
||||||
export class CommandExecutor {
|
export class CommandExecutor {
|
||||||
|
private readonly cooldowns = new Map<string, number>();
|
||||||
|
|
||||||
public async run(command: BotCommand, ctx: CommandExecutionContext): Promise<void> {
|
public async run(command: BotCommand, ctx: CommandExecutionContext): Promise<void> {
|
||||||
const missingUserPermissions = this.getMissingPermissions(command.permissions, this.memberPermissions(ctx));
|
const missingUserPermissions = this.getMissingPermissions(command.permissions, this.memberPermissions(ctx));
|
||||||
if (missingUserPermissions.length > 0) {
|
if (missingUserPermissions.length > 0) {
|
||||||
@@ -15,6 +17,12 @@ export class CommandExecutor {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const remainingCooldownSeconds = this.consumeCooldown(command, ctx.user.id);
|
||||||
|
if (remainingCooldownSeconds > 0) {
|
||||||
|
await ctx.reply(ctx.t("errors.cooldown", { seconds: remainingCooldownSeconds }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await command.execute(ctx);
|
await command.execute(ctx);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -73,4 +81,25 @@ export class CommandExecutor {
|
|||||||
.replace(/_/g, " ")
|
.replace(/_/g, " ")
|
||||||
.trim();
|
.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}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ export interface BotCommandInput {
|
|||||||
args?: CommandArgument[];
|
args?: CommandArgument[];
|
||||||
permissions?: PermissionResolvable[];
|
permissions?: PermissionResolvable[];
|
||||||
examples?: CommandExample[];
|
examples?: CommandExample[];
|
||||||
|
cooldown?: number;
|
||||||
execute: (ctx: CommandExecutionContext) => Promise<void>;
|
execute: (ctx: CommandExecutionContext) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,5 +107,6 @@ export interface BotCommand {
|
|||||||
args: CommandArgument[];
|
args: CommandArgument[];
|
||||||
permissions: PermissionResolvable[];
|
permissions: PermissionResolvable[];
|
||||||
examples: CommandExample[];
|
examples: CommandExample[];
|
||||||
|
cooldown?: number;
|
||||||
execute: (ctx: CommandExecutionContext) => Promise<void>;
|
execute: (ctx: CommandExecutionContext) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user