feat: implement command definition, registration, and execution framework

- Add `defineCommand` function to validate and normalize command inputs.
- Introduce `deployApplicationCommands` for deploying slash commands to Discord.
- Create `CommandRegistry` class for managing command registration and retrieval.
- Implement `buildSlashPayload` to construct slash command payloads with localization support.
- Add usage utilities for prefix and slash commands.
- Develop `CommandExecutor` to handle command execution with permission checks and cooldown management.
- Create member message rendering service for welcome and leave messages with customizable templates.
- Introduce presence template variables for dynamic bot status updates.
- Implement template variable utilities for rendering and extracting variables from strings.
This commit is contained in:
Puechberty Arthur
2026-04-13 21:10:51 +02:00
parent 1de2a76b05
commit e47cb68bc7
34 changed files with 44 additions and 44 deletions
+8 -8
View File
@@ -79,13 +79,13 @@ An example with two bot services is available in `docker-compose.multi-bot.examp
## Architecture
- `src/framework/types/command.ts`: strict command schema
- `src/framework/commands/defineCommand.ts`: default command completion
- `src/framework/commands/registry.ts`: trigger/name mapping generated from i18n dictionaries
- `src/framework/commands/argParser.ts`: prefix/slash args parsing from schema
- `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/types/command.ts`: strict command schema
- `src/core/commands/defineCommand.ts`: default command completion
- `src/core/commands/registry.ts`: trigger/name mapping generated from i18n dictionaries
- `src/core/commands/argParser.ts`: prefix/slash args parsing from schema
- `src/core/execution/CommandExecutor.ts`: unified pipeline (permissions/cooldown/execute)
- `src/handlers/prefixHandler.ts`: prefix entrypoint
- `src/handlers/slashHandler.ts`: slash entrypoint
- `src/database/presence/presenceStore.ts`: PostgreSQL presence storage
- `src/types/presenceTypes.ts`: shared presence types/validation
- `src/i18n/*.json`: external i18n dictionaries
@@ -102,6 +102,6 @@ An example with two bot services is available in `docker-compose.multi-bot.examp
## Adding A Command
1. Create a command object in `src/commands/...`
2. Follow the schema in `src/framework/types/command.ts`
2. Follow the schema in `src/types/command.ts`
3. Add command to `src/commands/index.ts`
4. If `AUTO_DEPLOY_SLASH=true`, restart the bot to sync slash commands automatically
+1 -1
View File
@@ -6,7 +6,7 @@
*/
import { PermissionFlagsBits } from "discord.js";
import { defineCommand } from "../framework/commands/defineCommand.js";
import { defineCommand } from "../core/commands/defineCommand.js";
import { createMemberMessageExecute } from "./memberMessagePanel.js";
/** Commande `goodbye` — ouvre le panneau de configuration des messages 'goodbye'. */
+2 -2
View File
@@ -9,8 +9,8 @@
*/
import { EmbedBuilder } from "discord.js";
import { buildPrefixUsage, buildSlashUsage, resolvePrefixTrigger, resolveSlashName } from "../framework/commands/usage.js";
import { defineCommand } from "../framework/commands/defineCommand.js";
import { buildPrefixUsage, buildSlashUsage, resolvePrefixTrigger, resolveSlashName } from "../core/commands/usage.js";
import { defineCommand } from "../core/commands/defineCommand.js";
import type { BotCommand, CommandExecutionContext } from "../types/command.js";
const categoryName = (command: BotCommand): string => command.meta.category;
+1 -1
View File
@@ -4,7 +4,7 @@
* Envoie une réponse de type `kissing` ciblant un utilisateur mentionné.
* Utilise un seul argument `user` de type `user`.
*/
import { defineCommand } from "../framework/commands/defineCommand.js";
import { defineCommand } from "../core/commands/defineCommand.js";
/** Commande `kiss` — envoie un message de type `kiss` vers la cible. */
export const kissCommand = defineCommand({
+2 -2
View File
@@ -22,9 +22,9 @@ import {
type Message,
} from "discord.js";
import { env } from "../framework/config/env.js";
import { env } from "../config/env.js";
import { I18nService } from "../i18n/index.js";
import { dispatchMemberMessage } from "../framework/memberMessages/memberMessageSender.js";
import { dispatchMemberMessage } from "../services/memberMessages/memberMessageSender.js";
import { getMemberMessageStore } from "../database/memberMessages/memberMessageStore.js";
import {
MEMBER_MESSAGE_RENDER_TYPES,
+1 -1
View File
@@ -4,7 +4,7 @@
* Répond avec un message court contenant la latence websocket du bot.
*/
import { MessageFlags } from "discord.js";
import { defineCommand } from "../framework/commands/defineCommand.js";
import { defineCommand } from "../core/commands/defineCommand.js";
/** Commande `ping` — affiche la latence du bot. */
export const pingCommand = defineCommand({
+3 -3
View File
@@ -21,8 +21,8 @@ import {
type Client,
type Message,
} from "discord.js";
import { defineCommand } from "../framework/commands/defineCommand.js";
import { env } from "../framework/config/env.js";
import { defineCommand } from "../core/commands/defineCommand.js";
import { env } from "../config/env.js";
import type {
DiscordPresenceStatus,
PresenceActivityTypeValue,
@@ -38,7 +38,7 @@ import {
containsPresenceTemplateVariables,
getPresenceTemplateHelpText,
renderPresenceTemplate,
} from "../framework/presence/presenceTemplateVariables.js";
} from "../services/presence/presenceTemplateVariables.js";
import {
PRESENCE_ACTIVITY_TYPES,
PRESENCE_STATUSES,
+1 -1
View File
@@ -6,7 +6,7 @@
*/
import { PermissionFlagsBits } from "discord.js";
import { defineCommand } from "../framework/commands/defineCommand.js";
import { defineCommand } from "../core/commands/defineCommand.js";
import { createMemberMessageExecute } from "./memberMessagePanel.js";
/** Commande `welcome` — ouvre le panneau de configuration des messages 'welcome'. */
@@ -1,7 +1,7 @@
import { config as loadEnv } from "dotenv";
import { z } from "zod";
import { SUPPORTED_LANGS } from "../../types/command.js";
import { SUPPORTED_LANGS } from "../types/command.js";
loadEnv();
@@ -1,6 +1,6 @@
import { Pool } from "pg";
import { env } from "../../framework/config/env.js";
import { env } from "../../config/env.js";
import type {
MemberMessageConfig,
MemberMessageKind,
+1 -1
View File
@@ -1,6 +1,6 @@
import { Pool } from "pg";
import { env } from "../../framework/config/env.js";
import { env } from "../../config/env.js";
import type {
PresenceActivityTypeValue,
PresenceRow,
+1 -1
View File
@@ -1,6 +1,6 @@
import { Events, type Client } from "discord.js";
import type { I18nService } from "../i18n/index.js";
import { dispatchMemberMessage } from "../framework/memberMessages/memberMessageSender.js";
import { dispatchMemberMessage } from "../services/memberMessages/memberMessageSender.js";
/**
* Enregistre le listener `guildMemberAdd` et déclenche l'envoi d'un message
+1 -1
View File
@@ -1,6 +1,6 @@
import { Events, type Client } from "discord.js";
import type { I18nService } from "../i18n/index.js";
import { dispatchMemberMessage } from "../framework/memberMessages/memberMessageSender.js";
import { dispatchMemberMessage } from "../services/memberMessages/memberMessageSender.js";
/**
* Enregistre le listener `guildMemberRemove` et déclenche l'envoi d'un message
+1 -1
View File
@@ -8,7 +8,7 @@ import { registerGuildMemberRemove } from "./guildMemberRemove.js";
import { registerGuildCreate } from "./guildCreate.js";
import { registerGuildDelete } from "./guildDelete.js";
import { registerClientReady } from "./ready.js";
import type { CommandRegistry } from "../framework/commands/registry.js";
import type { CommandRegistry } from "../core/commands/registry.js";
/**
* Regroupe l'enregistrement des événements Discord les plus courants.
+3 -3
View File
@@ -1,8 +1,8 @@
import { Events, type Client } from "discord.js";
import { deployApplicationCommands } from "../framework/commands/deploy.js";
import { env } from "../framework/config/env.js";
import { deployApplicationCommands } from "../core/commands/deploy.js";
import { env } from "../config/env.js";
import { restorePresenceFromStorage } from "../commands/presence.js";
import type { CommandRegistry } from "../framework/commands/registry.js";
import type { CommandRegistry } from "../core/commands/registry.js";
import type { I18nService } from "../i18n/index.js";
/**
+2 -2
View File
@@ -2,8 +2,8 @@ import type { Message } from "discord.js";
import type { BotCommand, SupportedLang } from "../types/command.js";
import type { PrefixHandlerDeps } from "../types/handlers.js";
import { parsePrefixArgs } from "../framework/commands/argParser.js";
import { buildPrefixUsage } from "../framework/commands/usage.js";
import { parsePrefixArgs } from "../core/commands/argParser.js";
import { buildPrefixUsage } from "../core/commands/usage.js";
import {
buildCommandExecutionContext,
createTranslator,
+2 -2
View File
@@ -1,8 +1,8 @@
import { MessageFlags, type ChatInputCommandInteraction } from "discord.js";
import type { SlashHandlerDeps } from "../types/handlers.js";
import { parseSlashArgs } from "../framework/commands/argParser.js";
import { buildSlashUsage } from "../framework/commands/usage.js";
import { parseSlashArgs } from "../core/commands/argParser.js";
import { buildSlashUsage } from "../core/commands/usage.js";
import {
buildCommandExecutionContext,
createTranslator,
+3 -3
View File
@@ -13,9 +13,9 @@ import { Client, GatewayIntentBits } from "discord.js";
import { commandList } from "./commands/index.js";
import { shutdownPresenceRuntime } from "./commands/presence.js";
import { registerEvents } from "./events/index.js";
import { CommandRegistry } from "./framework/commands/registry.js";
import { env } from "./framework/config/env.js";
import { CommandExecutor } from "./framework/execution/CommandExecutor.js";
import { CommandRegistry } from "./core/commands/registry.js";
import { env } from "./config/env.js";
import { CommandExecutor } from "./core/execution/CommandExecutor.js";
import { createPrefixHandler } from "./handlers/prefixHandler.js";
import { createSlashHandler } from "./handlers/slashHandler.js";
import { I18nService } from "./i18n/index.js";
@@ -1,7 +1,7 @@
import type { Client } from "discord.js";
import { env } from "../config/env.js";
import { hasTemplateVariable, renderTemplate } from "../utils/templateVariables.js";
import { env } from "../../config/env.js";
import { hasTemplateVariable, renderTemplate } from "../../utils/templateVariables.js";
import { sanitizeActivityText } from "../../types/presenceTypes.js";
export const PRESENCE_TEMPLATE_REFRESH_INTERVAL_MS = 60_000;
+1 -1
View File
@@ -1,4 +1,4 @@
import type { CommandRegistry } from "../framework/commands/registry.js";
import type { CommandRegistry } from "../core/commands/registry.js";
import type { I18nService } from "../i18n/index.js";
export interface DeployCommandsOptions {
+2 -2
View File
@@ -1,5 +1,5 @@
import type { CommandRegistry } from "../framework/commands/registry.js";
import type { CommandExecutor } from "../framework/execution/CommandExecutor.js";
import type { CommandRegistry } from "../core/commands/registry.js";
import type { CommandExecutor } from "../core/execution/CommandExecutor.js";
import type { I18nService } from "../i18n/index.js";
import type {
BotCommand,
@@ -1,4 +1,4 @@
import type { TemplateRenderOptions } from "../../types/templateVariables.js";
import type { TemplateRenderOptions } from "../types/templateVariables.js";
const createTemplateTokenRegex = (): RegExp => /\{\{([a-zA-Z0-9_]+)\}\}/g;
+1 -1
View File
@@ -1,7 +1,7 @@
import assert from "node:assert/strict";
import test from "node:test";
import { tokenizePrefixInput } from "../src/framework/commands/argParser.js";
import { tokenizePrefixInput } from "../src/core/commands/argParser.js";
test("tokenizePrefixInput parse les quotes simples et doubles", () => {
const tokens = tokenizePrefixInput('"hello world" test \'foo bar\'');
+1 -1
View File
@@ -1,7 +1,7 @@
import assert from "node:assert/strict";
import test from "node:test";
import { defineCommand } from "../src/framework/commands/defineCommand.js";
import { defineCommand } from "../src/core/commands/defineCommand.js";
test("defineCommand refuse un argument requis apres un optionnel", () => {
assert.throws(() => {
+1 -1
View File
@@ -5,7 +5,7 @@ import {
extractTemplateVariables,
hasTemplateVariable,
renderTemplate,
} from "../src/framework/utils/templateVariables.js";
} from "../src/utils/templateVariables.js";
test("renderTemplate remplace les variables et applique les alias", () => {
const output = renderTemplate(