From a777ea18e05fb507e53f81194a5cf9f6b3cf1248 Mon Sep 17 00:00:00 2001 From: Puechberty Arthur Date: Sun, 12 Apr 2026 22:31:56 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20ajouter=20la=20documentation=20JSDoc=20?= =?UTF-8?q?pour=20les=20commandes=20et=20=C3=A9v=C3=A9nements,=20et=20r?= =?UTF-8?q?=C3=A9organiser=20l'enregistrement=20des=20=C3=A9v=C3=A9nements?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- STRUCTURE.md | 86 +++++++++++++++ src/commands/core/help.ts | 12 +++ src/commands/fun/kiss.ts | 7 ++ src/commands/index.ts | 7 ++ src/commands/utility/advanced.ts | 10 ++ src/commands/utility/goodbye.ts | 7 ++ src/commands/utility/memberMessagePanel.ts | 17 +++ src/commands/utility/ping.ts | 6 ++ src/commands/utility/presence.ts | 17 +++ src/commands/utility/welcome.ts | 7 ++ src/events/guildCreate.ts | 12 +++ src/events/guildDelete.ts | 12 +++ src/events/guildMemberAdd.ts | 21 ++++ ...rMessageEvents.ts => guildMemberRemove.ts} | 19 +--- src/events/index.ts | 37 +++++++ src/events/interactionCreate.ts | 32 ++++++ src/events/messageCreate.ts | 15 +++ src/events/ready.ts | 37 +++++++ src/index.ts | 100 ++++++++++-------- 19 files changed, 402 insertions(+), 59 deletions(-) create mode 100644 STRUCTURE.md create mode 100644 src/events/guildCreate.ts create mode 100644 src/events/guildDelete.ts create mode 100644 src/events/guildMemberAdd.ts rename src/events/{memberMessageEvents.ts => guildMemberRemove.ts} (56%) create mode 100644 src/events/index.ts create mode 100644 src/events/interactionCreate.ts create mode 100644 src/events/messageCreate.ts create mode 100644 src/events/ready.ts diff --git a/STRUCTURE.md b/STRUCTURE.md new file mode 100644 index 0000000..86c201a --- /dev/null +++ b/STRUCTURE.md @@ -0,0 +1,86 @@ +# Structure du projet + +Objectif +-------- +Décrire l'organisation recommandée du dépôt et les conventions à suivre. + +Organisation proposée +--------------------- + +- `package.json`, `tsconfig.json`, `README.md`, `Dockerfile`, `docker-compose.yml` — fichiers racine et scripts. +- `dist/` — sortie build (doit être ignoré par git). +- `locales/` — fichiers de traduction (`en.json`, `es.json`, `fr.json`). +- `STRUCTURE.md` — ce fichier : documentation structure/conventions. + +Arborescence `src/` (principale) +-------------------------------- + +- `src/index.ts` — point d'entrée, boot du bot (initialisation, appel des enregistreurs d'événements). +- `src/commands/` — définitions de commandes (meta, args, examples, `execute`). Les commandes doivent rester minces et déléguer la logique lourde. + - `core/` — commandes fondamentales (ex: `help`). + - `fun/` — petites commandes stateless (ex: `kiss`). + - `utility/` — commandes qui orchestrent des services (ex: `presence`, `welcome`, `goodbye`). +- `src/events/` — fonctions d'enregistrement d'événements Discord (ex: `registerMemberMessageEvents`). Favoriser des fonctions `registerX(client, i18n)` simples. +- `src/framework/` — bibliothèque interne, responsables techniques réutilisables : + - `commands/` — helpers pour définir/parse/registry des commandes (`defineCommand`, `usage`, etc.). + - `i18n/` — `I18nService` et ressources locales. + - `memberMessages/` — `panel.ts` (UI/panels), `sender.ts`, `store.ts`, `types.ts` (extraction recommandée depuis `commands/utility/memberMessagePanel.ts`). + - `presence/` — `manager.ts` (timers, rotation, apply/save), `store.ts`, `templateVariables.ts` (extraction recommandée depuis `commands/utility/presence.ts`). + - `execution/`, `handlers/`, `config/`, `types/` — autres utilitaires et adaptateurs. +- `src/utils/` — helpers orthogonaux (ex: `templateVariables.ts`). +- `src/scripts/` — scripts réutilisables (ex: `deployCommands.ts`). + +- `tests/` — tests unitaires; organiser pour refléter la logique testée (stores, managers, utils). + +Conventions et règles simples +---------------------------- + +- Commandes : `defineCommand({...})` doit exposer uniquement la configuration + un `execute` qui fait appel à des services/managers. Eviter grosses fonctions de logique métier dans les fichiers de commandes. +- Services / managers : code testable, découplé de Discord.js; n'exposer que des fonctions pures ou des adaptateurs (injections de `client` uniquement dans l'adaptateur). +- UI / panels : centraliser sous `framework/memberMessages` et exposer des factories (ex: `createMemberMessageExecute(kind)` reste en commande mais la UI est extraite). +- Events : chaque fichier exporte une fonction `registerX(client, i18n)` ; centraliser l'appel dans `src/events/index.ts`. +- Tests : cibler les managers et stores en priorité; mocks/fixtures pour les adaptateurs Discord. + +Plan de migration (prioritaire) +------------------------------ +1. Extraire `memberMessagePanel.ts` → `src/framework/memberMessages/panel.ts` et mettre à jour `welcome.ts`/`goodbye.ts` pour utiliser l'API extraite. (À FAIRE) +2. Extraire la logique `presence` (timers, rotation, apply) → `src/framework/presence/manager.ts`, laisser `presence` command comme wrapper léger. (À FAIRE) +3. Scinder les événements et centraliser dans `src/events/` — un fichier par événement + `src/events/index.ts`. (FAIT) +4. Documenter les conventions (ce fichier) et ouvrir PRs petites et ciblées. (PARTIELLEMENT FAIT) + +Modifications récentes appliquées +-------------------------------- + +- Scission de `src/events/` en fichiers par événement : + - `messageCreate.ts`, `interactionCreate.ts`, `guildMemberAdd.ts`, `guildMemberRemove.ts`, `guildCreate.ts`, `guildDelete.ts`, `ready.ts`. + - `src/events/index.ts` centralise l'enregistrement via `registerEvents(client, i18n, handlers, registry)`. +- `ready` : la logique de restauration de présence et de déploiement des slash commands a été déplacée de `src/index.ts` vers `src/events/ready.ts`. +- Signatures des handlers mises à jour pour être fortement typées : `onPrefixMessage(message: Message)` et `onSlashInteraction(interaction: ChatInputCommandInteraction)`. +- Documentation (JSDoc / commentaires) ajoutée dans plusieurs fichiers de `src/commands/` et `src/events/`. + +Conventions actualisées +---------------------- + +- Un fichier par événement Discord, nommé exactement comme l'événement (ex: `guildMemberAdd.ts`). +- `src/events/index.ts` expose `registerEvents(...)` qui reçoit : + - `client`, `i18n`, `handlers` ({ `onPrefixMessage`, `onSlashInteraction` }) et `registry`. +- Les commandes restent des wrappers légers : configuration + `execute()` qui délègue aux services/managers dans `src/framework/`. + +Prochaines étapes recommandées +------------------------------ + +- Extraire `memberMessagePanel.ts` vers `src/framework/memberMessages/panel.ts` (priorité haute). +- Extraire la logique `presence` (timers, rotation) vers `src/framework/presence/manager.ts`. +- Ajouter un petit `README.md` de démarrage (build & run) et tenir ce fichier `STRUCTURE.md` à jour après chaque PR. +- Lancer `npm run build` et les tests pour valider les changements et corriger les erreurs de typage éventuelles. + +Comment travailler ensemble +------------------------- + +- Proposez ici les modifications souhaitées (renommage, dossiers additionnels, conventions strictes). Je peux appliquer chaque changement en petites PRs (déplacement de fichiers + correction d'import). +- Pour chaque modification, indiquer l'objectif et le résultat attendu. Je ferai les edits et lancerai les tests/build. + +Historique +--------- + +- Créé le 12 avril 2026 — version initiale. diff --git a/src/commands/core/help.ts b/src/commands/core/help.ts index ea1a981..26cf6b4 100644 --- a/src/commands/core/help.ts +++ b/src/commands/core/help.ts @@ -1,3 +1,12 @@ +/** + * Commande `help` + * + * Construit un embed d'aide global listant les commandes par catégorie, ou + * affiche les détails pour une commande spécifique si un argument est fourni. + * + * Export: + * - `helpCommand`: BotCommand (défini via `defineCommand`) + */ import { EmbedBuilder } from "discord.js"; import { buildPrefixUsage, buildSlashUsage, resolvePrefixTrigger, resolveSlashName } from "../../framework/commands/usage.js"; @@ -113,6 +122,9 @@ const buildCommandDetailsEmbed = (ctx: CommandExecutionContext, command: BotComm .setFooter({ text: ctx.ct("embed.footer", { source: command.meta.category }) }); }; +/** + * Commande `help` — renvoie un embed d'aide global ou les détails d'une commande. + */ export const helpCommand = defineCommand({ meta: { name: "help", diff --git a/src/commands/fun/kiss.ts b/src/commands/fun/kiss.ts index 9c26592..1da4793 100644 --- a/src/commands/fun/kiss.ts +++ b/src/commands/fun/kiss.ts @@ -1,5 +1,12 @@ +/** + * Commande `kiss` (fun) + * + * 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"; +/** Commande `kiss` — envoie un message de type `kiss` vers la cible. */ export const kissCommand = defineCommand({ meta: { name: "kiss", diff --git a/src/commands/index.ts b/src/commands/index.ts index c185652..d3d9ae7 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -1,3 +1,9 @@ +/** + * Liste des commandes exportées par le module `commands`. + * + * Ce fichier centralise l'ordre par défaut des commandes et permet de + * récupérer facilement la liste pour l'enregistrement (registry/dispatch). + */ import { helpCommand } from "./core/help.js"; import { kissCommand } from "./fun/kiss.js"; import { advancedCommand } from "./utility/advanced.js"; @@ -8,6 +14,7 @@ import { welcomeCommand } from "./utility/welcome.js"; import type { BotCommand } from "../framework/types/command.js"; +/** CommandList: tableau ordonné des commandes disponibles. */ export const commandList: BotCommand[] = [ kissCommand, pingCommand, diff --git a/src/commands/utility/advanced.ts b/src/commands/utility/advanced.ts index f82cc3b..70d1c2a 100644 --- a/src/commands/utility/advanced.ts +++ b/src/commands/utility/advanced.ts @@ -1,3 +1,10 @@ +/** + * Commande `advanced` (utility) + * + * Exemple de commande démontrant la gestion de plusieurs types d'arguments + * (string, int, user, number, boolean, channel, role) et l'utilisation d'un + * template de réponse configurable via `commandText.responses.summary`. + */ import { PermissionFlagsBits } from "discord.js"; import { defineCommand } from "../../framework/commands/defineCommand.js"; @@ -24,6 +31,9 @@ const toDisplayValue = (value: unknown, formatter?: (id: string) => string): str return String(value); }; +/** + * Commande `advanced` — affiche un résumé formaté des arguments fournis. + */ export const advancedCommand = defineCommand({ meta: { name: "advanced", diff --git a/src/commands/utility/goodbye.ts b/src/commands/utility/goodbye.ts index 09dacb8..8c400a2 100644 --- a/src/commands/utility/goodbye.ts +++ b/src/commands/utility/goodbye.ts @@ -1,8 +1,15 @@ +/** + * Commande `goodbye` (utility) + * + * Wrapper léger qui utilise la factory `createMemberMessageExecute` pour + * afficher un panneau de configuration des messages d'au revoir. + */ import { PermissionFlagsBits } from "discord.js"; import { defineCommand } from "../../framework/commands/defineCommand.js"; import { createMemberMessageExecute } from "./memberMessagePanel.js"; +/** Commande `goodbye` — ouvre le panneau de configuration des messages 'goodbye'. */ export const goodbyeCommand = defineCommand({ meta: { name: "goodbye", diff --git a/src/commands/utility/memberMessagePanel.ts b/src/commands/utility/memberMessagePanel.ts index ddd4c02..d12a8d6 100644 --- a/src/commands/utility/memberMessagePanel.ts +++ b/src/commands/utility/memberMessagePanel.ts @@ -1,3 +1,14 @@ +/** + * Panneau de configuration pour les messages de bienvenue / départ + * + * Contient la logique UI (Components v2), collectors et sessions pour afficher + * et gérer un panneau interactif permettant de configurer les messages + * d'accueil et d'au revoir par guild. + * + * Export: + * - `createMemberMessageExecute(kind)` : factory utilisée par les commandes + * `welcome` et `goodbye` pour attacher le panneau. + */ import { ActionRowBuilder, ButtonBuilder, @@ -203,6 +214,12 @@ const testFeedbackKey = (reason: string): string => { } }; +/** + * Factory qui crée l'exécuteur de commande pour un `MemberMessageKind` donné. + * + * Le handler retourné gère l'affichage du panneau, la collecte d'interactions + * et la persistance de la configuration via `memberMessageStore`. + */ export const createMemberMessageExecute = (kind: MemberMessageKind) => { return async (ctx: CommandExecutionContext): Promise => { if (!ctx.guild) { diff --git a/src/commands/utility/ping.ts b/src/commands/utility/ping.ts index ca26f3b..6781a84 100644 --- a/src/commands/utility/ping.ts +++ b/src/commands/utility/ping.ts @@ -1,6 +1,12 @@ +/** + * Commande `ping` (utility) + * + * Répond avec un message court contenant la latence websocket du bot. + */ import { MessageFlags } from "discord.js"; import { defineCommand } from "../../framework/commands/defineCommand.js"; +/** Commande `ping` — affiche la latence du bot. */ export const pingCommand = defineCommand({ meta: { name: "ping", diff --git a/src/commands/utility/presence.ts b/src/commands/utility/presence.ts index 2a6fea1..bce2c11 100644 --- a/src/commands/utility/presence.ts +++ b/src/commands/utility/presence.ts @@ -1,3 +1,11 @@ +/** + * Module `presence` — gestion des présences du bot + * + * Contient la logique de timers, rotation d'activités, rendu de templates + * et l'UI panel d'administration. Le module expose des helpers pour restaurer + * l'état (`restorePresenceFromStorage`), arrêter les timers (`shutdownPresenceRuntime`) + * et la commande `presence` qui affiche le panneau. + */ import { ActivityType, ActionRowBuilder, @@ -96,6 +104,9 @@ const resolveRuntimeState = (client: Client): PresenceRuntimeState => { return next; }; +/** + * Stoppe toutes les tâches runtime liées aux présences et libère les sessions. + */ export const shutdownPresenceRuntime = (): void => { for (const runtimeState of presenceRuntimeByBotId.values()) { clearRuntimeTimers(runtimeState); @@ -395,6 +406,9 @@ const persistAndApplyPresence = async ( await savePresenceState(ctx.client, state); }; +/** + * Charge et applique l'état de présence depuis le stockage pour un client. + */ export const restorePresenceFromStorage = async (client: Client): Promise => { const state = await loadPresenceState(client); const runtimeState = resolveRuntimeState(client); @@ -403,6 +417,9 @@ export const restorePresenceFromStorage = async (client: Client): Promise syncDynamicPresenceTimers(client, state, runtimeState); }; +/** + * Commande `presence` — ouvre le panneau de gestion de la présence pour le bot. + */ export const presenceCommand = defineCommand({ meta: { name: "presence", diff --git a/src/commands/utility/welcome.ts b/src/commands/utility/welcome.ts index 547622f..957d640 100644 --- a/src/commands/utility/welcome.ts +++ b/src/commands/utility/welcome.ts @@ -1,8 +1,15 @@ +/** + * Commande `welcome` (utility) + * + * Wrapper léger qui utilise la factory `createMemberMessageExecute` pour + * afficher un panneau de configuration des messages d'accueil. + */ import { PermissionFlagsBits } from "discord.js"; import { defineCommand } from "../../framework/commands/defineCommand.js"; import { createMemberMessageExecute } from "./memberMessagePanel.js"; +/** Commande `welcome` — ouvre le panneau de configuration des messages 'welcome'. */ export const welcomeCommand = defineCommand({ meta: { name: "welcome", diff --git a/src/events/guildCreate.ts b/src/events/guildCreate.ts new file mode 100644 index 0000000..57ca936 --- /dev/null +++ b/src/events/guildCreate.ts @@ -0,0 +1,12 @@ +import { Events, type Client } from "discord.js"; + +/** + * Enregistre le listener `guildCreate` (bot ajouté à un serveur). + * + * Action minimale: log pour monitoring ; peut être étendu (initialisation de configs, etc.). + */ +export const registerGuildCreate = (client: Client): void => { + client.on(Events.GuildCreate, (guild) => { + console.log(`[event:guildCreate] joined guild ${guild.id} (${guild.name})`); + }); +}; diff --git a/src/events/guildDelete.ts b/src/events/guildDelete.ts new file mode 100644 index 0000000..125c1f6 --- /dev/null +++ b/src/events/guildDelete.ts @@ -0,0 +1,12 @@ +import { Events, type Client } from "discord.js"; + +/** + * Enregistre le listener `guildDelete` (bot retiré d'un serveur). + * + * Action minimale: log pour monitoring ; peut être étendu (cleanup, etc.). + */ +export const registerGuildDelete = (client: Client): void => { + client.on(Events.GuildDelete, (guild) => { + console.log(`[event:guildDelete] left guild ${guild.id} (${guild.name})`); + }); +}; diff --git a/src/events/guildMemberAdd.ts b/src/events/guildMemberAdd.ts new file mode 100644 index 0000000..415b19f --- /dev/null +++ b/src/events/guildMemberAdd.ts @@ -0,0 +1,21 @@ +import { Events, type Client } from "discord.js"; +import type { I18nService } from "../framework/i18n/I18nService.js"; +import { dispatchMemberMessage } from "../framework/memberMessages/memberMessageSender.js"; + +/** + * Enregistre le listener `guildMemberAdd` et déclenche l'envoi d'un message + * de bienvenue via `dispatchMemberMessage`. + */ +export const registerGuildMemberAdd = (client: Client, i18n: I18nService): void => { + client.on(Events.GuildMemberAdd, (member) => { + void dispatchMemberMessage({ + client, + i18n, + guild: member.guild, + user: member.user, + kind: "welcome", + }).catch((error) => { + console.error("[event:guildMemberAdd] failed to send welcome message", error); + }); + }); +}; diff --git a/src/events/memberMessageEvents.ts b/src/events/guildMemberRemove.ts similarity index 56% rename from src/events/memberMessageEvents.ts rename to src/events/guildMemberRemove.ts index e3fb13c..6fc7933 100644 --- a/src/events/memberMessageEvents.ts +++ b/src/events/guildMemberRemove.ts @@ -1,21 +1,12 @@ import { Events, type Client } from "discord.js"; - import type { I18nService } from "../framework/i18n/I18nService.js"; import { dispatchMemberMessage } from "../framework/memberMessages/memberMessageSender.js"; -export const registerMemberMessageEvents = (client: Client, i18n: I18nService): void => { - client.on(Events.GuildMemberAdd, (member) => { - void dispatchMemberMessage({ - client, - i18n, - guild: member.guild, - user: member.user, - kind: "welcome", - }).catch((error) => { - console.error("[event:guildMemberAdd] failed to send welcome message", error); - }); - }); - +/** + * Enregistre le listener `guildMemberRemove` et déclenche l'envoi d'un message + * d'au revoir via `dispatchMemberMessage`. + */ +export const registerGuildMemberRemove = (client: Client, i18n: I18nService): void => { client.on(Events.GuildMemberRemove, (member) => { void dispatchMemberMessage({ client, diff --git a/src/events/index.ts b/src/events/index.ts new file mode 100644 index 0000000..ab08aba --- /dev/null +++ b/src/events/index.ts @@ -0,0 +1,37 @@ +import type { Client, Message, ChatInputCommandInteraction } from "discord.js"; +import type { I18nService } from "../framework/i18n/I18nService.js"; + +import { registerMessageCreate } from "./messageCreate.js"; +import { registerInteractionCreate } from "./interactionCreate.js"; +import { registerGuildMemberAdd } from "./guildMemberAdd.js"; +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"; + +/** + * Regroupe l'enregistrement des événements Discord les plus courants. + * + * @param client - instance du Client Discord + * @param i18n - instance de service i18n + * @param handlers - objets contenant les handlers pour message/interaction + */ +export const registerEvents = ( + client: Client, + i18n: I18nService, + handlers: { onPrefixMessage: (m: Message) => Promise; onSlashInteraction: (i: ChatInputCommandInteraction) => Promise }, + registry: CommandRegistry, +): void => { + registerMessageCreate(client, handlers.onPrefixMessage); + registerInteractionCreate(client, handlers.onSlashInteraction); + + registerGuildMemberAdd(client, i18n); + registerGuildMemberRemove(client, i18n); + + registerGuildCreate(client); + registerGuildDelete(client); + + // Ready: tâches à exécuter au démarrage du client + registerClientReady(client, registry, i18n); +}; diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts new file mode 100644 index 0000000..dd65f52 --- /dev/null +++ b/src/events/interactionCreate.ts @@ -0,0 +1,32 @@ +import { Events, type Client, type ChatInputCommandInteraction } from "discord.js"; + +/** + * Enregistre le listener `interactionCreate` pour les commandes slash (chat input). + * + * @param client - instance du Client Discord + * @param onSlashInteraction - fonction à appeler pour traiter les interactions slash + */ +export const registerInteractionCreate = ( + client: Client, + onSlashInteraction: (interaction: ChatInputCommandInteraction) => Promise, +): void => { + client.on(Events.InteractionCreate, (interaction) => { + // On ne traite que les `ChatInputCommand` (slash) + if (!interaction || typeof (interaction as any).isChatInputCommand !== "function") { + return; + } + + try { + if (!(interaction as any).isChatInputCommand()) { + return; + } + } catch { + return; + } + + // Cast sécurisé par le test `isChatInputCommand()` effectué ci‑dessus + void onSlashInteraction(interaction as ChatInputCommandInteraction).catch((error) => { + console.error("[event:interactionCreate] handler failed", error); + }); + }); +}; diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts new file mode 100644 index 0000000..e6feac9 --- /dev/null +++ b/src/events/messageCreate.ts @@ -0,0 +1,15 @@ +import { Events, type Client, type Message } from "discord.js"; + +/** + * Enregistre le listener `messageCreate` en déléguant au handler fourni. + * + * @param client - instance du Client Discord + * @param onPrefixMessage - fonction à appeler pour traiter les messages (préfixe) + */ +export const registerMessageCreate = (client: Client, onPrefixMessage: (message: Message) => Promise): void => { + client.on(Events.MessageCreate, (message: Message) => { + void onPrefixMessage(message).catch((error) => { + console.error("[event:messageCreate] handler failed", error); + }); + }); +}; diff --git a/src/events/ready.ts b/src/events/ready.ts new file mode 100644 index 0000000..345f533 --- /dev/null +++ b/src/events/ready.ts @@ -0,0 +1,37 @@ +import { Events, type Client } from "discord.js"; +import { deployApplicationCommands } from "../framework/commands/deploy.js"; +import { env } from "../framework/config/env.js"; +import { restorePresenceFromStorage } from "../commands/utility/presence.js"; +import type { CommandRegistry } from "../framework/commands/registry.js"; +import type { I18nService } from "../framework/i18n/I18nService.js"; + +/** + * Attache le listener `ready` et exécute les tâches post-démarrage: + * - restauration de la présence + * - (optionnel) déploiement des commandes slash + */ +export const registerClientReady = (client: Client, registry: CommandRegistry, i18n: I18nService): void => { + client.once(Events.ClientReady, async () => { + console.log(`[ready] logged as ${client.user?.tag ?? "unknown"}`); + try { + await restorePresenceFromStorage(client); + } catch (error) { + console.error("[ready] failed to restore bot presence", error); + } + + if (env.AUTO_DEPLOY_SLASH) { + try { + const result = await deployApplicationCommands({ + token: env.DISCORD_TOKEN, + clientId: env.DISCORD_CLIENT_ID, + registry, + i18n, + ...(env.DEV_GUILD_ID ? { guildId: env.DEV_GUILD_ID } : {}), + }); + console.log(`[ready] slash sync done (${result.scope}, ${result.count} commands)`); + } catch (error) { + console.error("[ready] slash sync failed", error); + } + } + }); +}; diff --git a/src/index.ts b/src/index.ts index 7114755..fca8fb0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,18 @@ -import { Client, Events, GatewayIntentBits } from "discord.js"; +/** + * Entrypoint du bot — bootstrap et configuration runtime. + * + * Rôles principaux: + * - initialiser les stores et ressources (presence, member messages) + * - configurer le client Discord (intents) + * - construire i18n, registry et executor + * - attacher les handlers (prefix/slash) et listeners d'événements + * - restaurer la présence et (optionnel) déployer les commandes slash + */ +import { Client, GatewayIntentBits } from "discord.js"; import { commandList } from "./commands/index.js"; -import { restorePresenceFromStorage, shutdownPresenceRuntime } from "./commands/utility/presence.js"; -import { registerMemberMessageEvents } from "./events/memberMessageEvents.js"; -import { deployApplicationCommands } from "./framework/commands/deploy.js"; +import { shutdownPresenceRuntime } from "./commands/utility/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"; @@ -16,16 +25,31 @@ import { } from "./framework/memberMessages/memberMessageStore.js"; import { initPresenceStore, shutdownPresenceStore } from "./framework/presence/presenceStore.js"; +/** + * Attache des handlers pour un arrêt gracieux du process. + * + * Actions effectuées lors du shutdown: + * - arrêt des timers/runtime (présence) + * - fermeture des stores (member messages / presence) + * - sortie du process + */ const bindGracefulShutdown = (): void => { const shutdown = async (signal: string): Promise => { console.log(`[shutdown] ${signal}`); + + // Stoppe les timers et runtime liés à la présence shutdownPresenceRuntime(); + + // Ferme proprement le store des messages de membre await shutdownMemberMessageStore().catch((error) => { console.error("[shutdown] member message store close failed", error); }); + + // Ferme proprement le store de présence await shutdownPresenceStore().catch((error) => { console.error("[shutdown] presence store close failed", error); }); + process.exit(0); }; @@ -38,11 +62,27 @@ const bindGracefulShutdown = (): void => { }); }; +/** + * Sequence d'initialisation principale. + * + * Etapes: + * 1. initialiser les stores (presence, member messages) + * 2. attacher shutdown handler + * 3. configurer la gestion d'erreurs process + * 4. créer le client Discord et les services (i18n, registry, executor) + * 5. construire les handlers prefix/slash et attacher les listeners + * 6. enregistrer events métier (member messages) + * 7. sur ready: restaurer la présence et déployer les slash commands si activé + */ const bootstrap = async (): Promise => { + // Initialisation des stores persistants utilisés par le bot await initPresenceStore(); await initMemberMessageStore(); + + // Prépare la gestion d'un arrêt gracieux (SIGINT / SIGTERM) bindGracefulShutdown(); + // Log des erreurs non catchées pour debugging process.on("unhandledRejection", (reason) => { console.error("[process] unhandled rejection", reason); }); @@ -51,6 +91,7 @@ const bootstrap = async (): Promise => { console.error("[process] uncaught exception", error); }); + // Création du client Discord avec les intents nécessaires const client = new Client({ intents: [ GatewayIntentBits.Guilds, @@ -60,10 +101,13 @@ const bootstrap = async (): Promise => { ], }); + // Services transverses: i18n, registry des commandes et executor const i18n = new I18nService(env.DEFAULT_LANG); const registry = new CommandRegistry(commandList, i18n); const executor = new CommandExecutor(); + // Création des handlers : ces factories retournent une fonction utilitaire + // qui prend un Message / Interaction et exécute la logique (parse args, permissions, etc.) const onPrefixMessage = createPrefixHandler({ registry, i18n, @@ -80,53 +124,19 @@ const bootstrap = async (): Promise => { defaultLang: env.DEFAULT_LANG, }); - client.on(Events.MessageCreate, (message) => { - void onPrefixMessage(message).catch((error) => { - console.error("[event:messageCreate] handler failed", error); - }); - }); - - client.on(Events.InteractionCreate, (interaction) => { - if (!interaction.isChatInputCommand()) { - return; - } - - void onSlashInteraction(interaction).catch((error) => { - console.error("[event:interactionCreate] handler failed", error); - }); - }); - - registerMemberMessageEvents(client, i18n); - - client.once(Events.ClientReady, async () => { - console.log(`[ready] logged as ${client.user?.tag ?? "unknown"}`); - try { - await restorePresenceFromStorage(client); - } catch (error) { - console.error("[ready] failed to restore bot presence", error); - } - - if (env.AUTO_DEPLOY_SLASH) { - try { - const result = await deployApplicationCommands({ - token: env.DISCORD_TOKEN, - clientId: env.DISCORD_CLIENT_ID, - registry, - i18n, - ...(env.DEV_GUILD_ID ? { guildId: env.DEV_GUILD_ID } : {}), - }); - console.log(`[ready] slash sync done (${result.scope}, ${result.count} commands)`); - } catch (error) { - console.error("[ready] slash sync failed", error); - } - } - }); + // Enregistre les événements principaux (séparés par fichier) + // `registerEvents` inclut désormais l'enregistrement du listener `ready`. + registerEvents(client, i18n, { onPrefixMessage, onSlashInteraction }, registry); + // Connexion du client au gateway await client.login(env.DISCORD_TOKEN); }; +// Démarre la séquence d'initialisation et gère les erreurs fatales bootstrap().catch(async (error) => { console.error("[boot] fatal error", error); + + // Nettoyage partiel en cas d'erreur critique pendant le boot shutdownPresenceRuntime(); await shutdownMemberMessageStore().catch((closeError) => { console.error("[boot] failed to close member message store", closeError);