feat: add utility commands for member messages and presence management

- Implement `goodbye` command to configure goodbye messages.
- Implement `help` command to provide a global help embed with command details.
- Implement `kiss` command to send a kiss message to a mentioned user.
- Create `memberMessagePanel` for managing welcome and goodbye messages.
- Implement `ping` command to check bot latency.
- Implement `presence` command to manage bot presence settings.
- Implement `welcome` command to configure welcome messages.
This commit is contained in:
Puechberty Arthur
2026-04-12 23:08:03 +02:00
parent a777ea18e0
commit 0f82c7e287
13 changed files with 116 additions and 231 deletions
+89
View File
@@ -0,0 +1,89 @@
# AGENT.md - Guide du projet template_discordjs
Version: 1.0
But
---
Ce fichier décrit le projet, sa structure, les conventions de code et la procédure recommandée pour ajouter de nouvelles fonctionnalités (commandes, événements, services). Il sert de référence pour les contributeurs et pour les agents automatisés qui travaillent sur le dépôt.
Vue d'ensemble du projet
-----------------------
- Tech stack: TypeScript + Discord.js (bot template). Le code source est dans `src/` et la sortie build dans `dist/` (à ignorer).
- Localisation: `locales/` contient `en.json`, `fr.json`, `es.json`.
- Scripts utiles: `scripts/deployCommands.ts` pour (re)déployer les slash commands.
Organisation des fichiers
------------------------
- Racine:
- `package.json`, `tsconfig.json`, `Dockerfile`, `docker-compose.yml` — scripts et infra.
- `locales/` — fichiers de traduction.
- `src/` (code TypeScript):
- `index.ts` — point d'entrée, boot du bot.
- `commands/` — définitions de commandes (chaque fichier expose une commande).
- `events/` — un fichier par événement Discord (ex: `guildMemberAdd.ts`). `src/events/index.ts` centralise l'enregistrement.
- `framework/` — bibliothèque interne: helpers commandes, `i18n/`, `memberMessages/`, `presence/`, `execution/`, `handlers/`, `config/`, `types/`.
- `utils/` — helpers génériques (ex: `templateVariables.ts`).
- `scripts/` — utilitaires (ex: `deployCommands.ts`).
- `tests/` — tests unitaires ciblant managers, stores et utilitaires.
Principes et conventions de code
-------------------------------
- Commands: chaque commande doit être définie avec `defineCommand({...})` et exposer uniquement la configuration + une fonction `execute()` très mince qui délègue la logique métier à un service/manager.
- Services / Managers: placer la logique métier testable dans `src/framework/*`. Ces modules doivent être découplés de Discord.js — recevoir des adaptateurs ou des interfaces plutôt que des objets Discord directement.
- Events: un seul fichier par événement; exporter une fonction `registerX(client, i18n)` ou une fonction d'enregistrement équivalente. Centraliser les imports dans `src/events/index.ts`.
- Typage: utiliser TypeScript strict, signatures fortement typées pour les handlers (`onPrefixMessage(message: Message)` etc.).
- Tests: privilégier les tests unitaires pour managers/stores/transformations. Mockez les adaptateurs Discord.
- i18n: utiliser `I18nService` pour traductions. Ajouter toutes les clés dans `locales/*.json`.
- Nommage: fichiers en lowerCamelCase (ex: `welcome.ts`, `memberMessagePanel.ts`). Exports nommés préférés pour faciliter le mocking.
- Sécurité: ne pas committer de secrets (`.env*` doit être dans `.gitignore`).
Procédure standard pour ajouter une nouvelle commande
----------------------------------------------------
1. Créer le fichier dans `src/commands/myCommand.ts`.
2. Utiliser `defineCommand({ meta, args, examples, execute })` pour déclarer la commande.
3. Implémenter `execute()` de façon minimale: valider les args et appeler un service dans `src/framework/` si la logique est non triviale.
4. Ajouter les clés de traduction dans `locales/en.json`, `locales/fr.json`, `locales/es.json` (ex: `commands.myCommand.success`).
5. Écrire des tests unitaires dans `tests/` pour le service/manager; si la commande est juste un wrapper, testez le service.
6. Si c'est une slash command, vérifier que `scripts/deployCommands.ts`/le registre inclut la commande; exécuter le déploiement si nécessaire.
7. Lancer `npm run build` puis `npm test` (ou la suite de scripts définie dans `package.json`).
8. Ouvrir une PR documentant le changement et incluant les tests et les traductions.
Procédure standard pour ajouter un nouvel événement
--------------------------------------------------
1. Créer `src/events/<eventName>.ts` et y exporter `register<EventName>(client, i18n)`.
2. Respecter la signature projet (voir `src/events/index.ts` pour l'exemple d'enregistrement).
3. Mettre la logique testable dans un manager/service et tester cette logique séparément.
4. Ajouter l'import et l'appel d'enregistrement dans `src/events/index.ts`.
Procédure pour ajouter un service/manager dans `framework`
--------------------------------------------------------
1. Créer un dossier `src/framework/<feature>/` contenant `manager.ts`, `store.ts` (si nécessaire), `types.ts` et tests.
2. Interfacez le manager pour qu'il accepte des adaptateurs (ex: `DiscordAdapter`) au lieu d'utiliser directement `client`.
3. Documenter les comportements et écrire des tests unitaires couvrant les cas critiques.
i18n — bonnes pratiques
-----------------------
- Utiliser des clés structurées: `commands.<name>.<key>` ou `presence.<action>`.
- Garder `en.json` comme référence complète; synchroniser les autres langues.
Tests et CI
-----------
- Prioriser les tests sur managers, stores et utilitaires.
- Mocks: extraire les dépendances externes (Discord API) derrière des adaptateurs pour pouvoir les mocker.
- Scripts: utiliser `npm test` et `npm run build` depuis la racine (vérifier `package.json`).
Déploiement / exécution
-----------------------
- Utiliser `Dockerfile` / `docker-compose.yml` fournis pour le déploiement en conteneur.
- Pour les slash commands, exécuter `scripts/deployCommands.ts` (ou le script npm associé).
Revue et PR
-----------
- Inclure toujours:
- modifications de traduction,
- tests unitaires pour toute logique métier ajoutée,
- notes de migration si la structure des commandes/events change.
Remarques finales
-----------------
- Ce fichier est la source de vérité pour les agents et contributeurs. Pour toute modification structurelle majeure (réorganisation de dossiers, changement d'API interne), mettez à jour `AGENT.md` et ouvrez une PR dédiée.
-86
View File
@@ -1,86 +0,0 @@
# 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.
@@ -6,7 +6,7 @@
*/ */
import { PermissionFlagsBits } from "discord.js"; import { PermissionFlagsBits } from "discord.js";
import { defineCommand } from "../../framework/commands/defineCommand.js"; import { defineCommand } from "../framework/commands/defineCommand.js";
import { createMemberMessageExecute } from "./memberMessagePanel.js"; import { createMemberMessageExecute } from "./memberMessagePanel.js";
/** Commande `goodbye` — ouvre le panneau de configuration des messages 'goodbye'. */ /** Commande `goodbye` — ouvre le panneau de configuration des messages 'goodbye'. */
@@ -9,9 +9,9 @@
*/ */
import { EmbedBuilder } from "discord.js"; import { EmbedBuilder } from "discord.js";
import { buildPrefixUsage, buildSlashUsage, resolvePrefixTrigger, resolveSlashName } from "../../framework/commands/usage.js"; import { buildPrefixUsage, buildSlashUsage, resolvePrefixTrigger, resolveSlashName } from "../framework/commands/usage.js";
import { defineCommand } from "../../framework/commands/defineCommand.js"; import { defineCommand } from "../framework/commands/defineCommand.js";
import type { BotCommand, CommandExecutionContext } from "../../framework/types/command.js"; import type { BotCommand, CommandExecutionContext } from "../framework/types/command.js";
const categoryName = (command: BotCommand): string => command.meta.category; const categoryName = (command: BotCommand): string => command.meta.category;
+6 -8
View File
@@ -4,13 +4,12 @@
* Ce fichier centralise l'ordre par défaut des commandes et permet de * Ce fichier centralise l'ordre par défaut des commandes et permet de
* récupérer facilement la liste pour l'enregistrement (registry/dispatch). * récupérer facilement la liste pour l'enregistrement (registry/dispatch).
*/ */
import { helpCommand } from "./core/help.js"; import { helpCommand } from "./help.js";
import { kissCommand } from "./fun/kiss.js"; import { kissCommand } from "./kiss.js";
import { advancedCommand } from "./utility/advanced.js"; import { goodbyeCommand } from "./goodbye.js";
import { goodbyeCommand } from "./utility/goodbye.js"; import { presenceCommand } from "./presence.js";
import { presenceCommand } from "./utility/presence.js"; import { pingCommand } from "./ping.js";
import { pingCommand } from "./utility/ping.js"; import { welcomeCommand } from "./welcome.js";
import { welcomeCommand } from "./utility/welcome.js";
import type { BotCommand } from "../framework/types/command.js"; import type { BotCommand } from "../framework/types/command.js";
@@ -18,7 +17,6 @@ import type { BotCommand } from "../framework/types/command.js";
export const commandList: BotCommand[] = [ export const commandList: BotCommand[] = [
kissCommand, kissCommand,
pingCommand, pingCommand,
advancedCommand,
welcomeCommand, welcomeCommand,
goodbyeCommand, goodbyeCommand,
presenceCommand, presenceCommand,
@@ -4,7 +4,7 @@
* Envoie une réponse de type `kissing` ciblant un utilisateur mentionné. * Envoie une réponse de type `kissing` ciblant un utilisateur mentionné.
* Utilise un seul argument `user` de type `user`. * Utilise un seul argument `user` de type `user`.
*/ */
import { defineCommand } from "../../framework/commands/defineCommand.js"; import { defineCommand } from "../framework/commands/defineCommand.js";
/** Commande `kiss` — envoie un message de type `kiss` vers la cible. */ /** Commande `kiss` — envoie un message de type `kiss` vers la cible. */
export const kissCommand = defineCommand({ export const kissCommand = defineCommand({
@@ -22,18 +22,18 @@ import {
type Message, type Message,
} from "discord.js"; } from "discord.js";
import { env } from "../../framework/config/env.js"; import { env } from "../framework/config/env.js";
import { I18nService } from "../../framework/i18n/I18nService.js"; import { I18nService } from "../framework/i18n/I18nService.js";
import { dispatchMemberMessage } from "../../framework/memberMessages/memberMessageSender.js"; import { dispatchMemberMessage } from "../framework/memberMessages/memberMessageSender.js";
import { getMemberMessageStore } from "../../framework/memberMessages/memberMessageStore.js"; import { getMemberMessageStore } from "../framework/memberMessages/memberMessageStore.js";
import { import {
MEMBER_MESSAGE_RENDER_TYPES, MEMBER_MESSAGE_RENDER_TYPES,
isMemberMessageRenderTypeValue, isMemberMessageRenderTypeValue,
type MemberMessageConfig, type MemberMessageConfig,
type MemberMessageKind, type MemberMessageKind,
type MemberMessageRenderType, type MemberMessageRenderType,
} from "../../framework/memberMessages/memberMessageTypes.js"; } from "../framework/memberMessages/memberMessageTypes.js";
import type { CommandExecutionContext } from "../../framework/types/command.js"; import type { CommandExecutionContext } from "../framework/types/command.js";
const memberMessageI18n = new I18nService(env.DEFAULT_LANG); const memberMessageI18n = new I18nService(env.DEFAULT_LANG);
@@ -4,7 +4,7 @@
* Répond avec un message court contenant la latence websocket du bot. * Répond avec un message court contenant la latence websocket du bot.
*/ */
import { MessageFlags } from "discord.js"; import { MessageFlags } from "discord.js";
import { defineCommand } from "../../framework/commands/defineCommand.js"; import { defineCommand } from "../framework/commands/defineCommand.js";
/** Commande `ping` — affiche la latence du bot. */ /** Commande `ping` — affiche la latence du bot. */
export const pingCommand = defineCommand({ export const pingCommand = defineCommand({
@@ -21,15 +21,15 @@ import {
type Client, type Client,
type Message, type Message,
} from "discord.js"; } from "discord.js";
import { defineCommand } from "../../framework/commands/defineCommand.js"; import { defineCommand } from "../framework/commands/defineCommand.js";
import { env } from "../../framework/config/env.js"; import { env } from "../framework/config/env.js";
import { getPresenceStore } from "../../framework/presence/presenceStore.js"; import { getPresenceStore } from "../framework/presence/presenceStore.js";
import { import {
PRESENCE_TEMPLATE_REFRESH_INTERVAL_MS, PRESENCE_TEMPLATE_REFRESH_INTERVAL_MS,
containsPresenceTemplateVariables, containsPresenceTemplateVariables,
getPresenceTemplateHelpText, getPresenceTemplateHelpText,
renderPresenceTemplate, renderPresenceTemplate,
} from "../../framework/presence/presenceTemplateVariables.js"; } from "../framework/presence/presenceTemplateVariables.js";
import { import {
PRESENCE_ACTIVITY_TYPES, PRESENCE_ACTIVITY_TYPES,
PRESENCE_STATUSES, PRESENCE_STATUSES,
@@ -45,8 +45,8 @@ import {
type PresenceActivityTypeValue, type PresenceActivityTypeValue,
type PresenceState, type PresenceState,
type PresenceStatusValue, type PresenceStatusValue,
} from "../../framework/presence/presenceTypes.js"; } from "../framework/presence/presenceTypes.js";
import type { CommandExecutionContext } from "../../framework/types/command.js"; import type { CommandExecutionContext } from "../framework/types/command.js";
interface PresenceCustomIds { interface PresenceCustomIds {
statusSelect: string; statusSelect: string;
-116
View File
@@ -1,116 +0,0 @@
/**
* 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";
const extractId = (value: unknown): string | null => {
if (!value || typeof value !== "object" || !("id" in value)) {
return null;
}
const id = (value as { id?: unknown }).id;
return typeof id === "string" ? id : null;
};
const toDisplayValue = (value: unknown, formatter?: (id: string) => string): string => {
const id = extractId(value);
if (id && formatter) {
return formatter(id);
}
if (value === undefined || value === null) {
return "-";
}
return String(value);
};
/**
* Commande `advanced` — affiche un résumé formaté des arguments fournis.
*/
export const advancedCommand = defineCommand({
meta: {
name: "advanced",
category: "utility",
},
permissions: [PermissionFlagsBits.ManageMessages],
args: [
{
name: "text",
type: "string",
required: true,
descriptionKey: "args.text",
},
{
name: "count",
type: "int",
required: true,
descriptionKey: "args.count",
},
{
name: "user",
type: "user",
required: true,
descriptionKey: "args.user",
},
{
name: "ratio",
type: "number",
required: false,
descriptionKey: "args.ratio",
},
{
name: "enabled",
type: "boolean",
required: false,
descriptionKey: "args.enabled",
},
{
name: "channel",
type: "channel",
required: false,
descriptionKey: "args.channel",
},
{
name: "role",
type: "role",
required: false,
descriptionKey: "args.role",
},
],
examples: [
{
args: '"hello world" 5 @Arthur 1.5 true #general @Moderators',
descriptionKey: "examples.full",
},
{
source: "slash",
descriptionKey: "examples.slash",
},
],
execute: async (ctx) => {
const responses = (ctx.commandText.responses as Record<string, unknown> | undefined) ?? {};
const template = typeof responses.summary === "string"
? responses.summary
: "text={{text}} count={{count}} ratio={{ratio}} enabled={{enabled}} user={{user}} channel={{channel}} role={{role}} source={{source}}";
await ctx.reply(
ctx.format(template, {
text: toDisplayValue(ctx.args.text),
count: toDisplayValue(ctx.args.count),
ratio: toDisplayValue(ctx.args.ratio),
enabled: toDisplayValue(ctx.args.enabled),
user: toDisplayValue(ctx.args.user, (id) => `<@${id}>`),
channel: toDisplayValue(ctx.args.channel, (id) => `<#${id}>`),
role: toDisplayValue(ctx.args.role, (id) => `<@&${id}>`),
source: ctx.source,
}),
);
},
});
@@ -6,7 +6,7 @@
*/ */
import { PermissionFlagsBits } from "discord.js"; import { PermissionFlagsBits } from "discord.js";
import { defineCommand } from "../../framework/commands/defineCommand.js"; import { defineCommand } from "../framework/commands/defineCommand.js";
import { createMemberMessageExecute } from "./memberMessagePanel.js"; import { createMemberMessageExecute } from "./memberMessagePanel.js";
/** Commande `welcome` — ouvre le panneau de configuration des messages 'welcome'. */ /** Commande `welcome` — ouvre le panneau de configuration des messages 'welcome'. */
+1 -1
View File
@@ -1,7 +1,7 @@
import { Events, type Client } from "discord.js"; import { Events, type Client } from "discord.js";
import { deployApplicationCommands } from "../framework/commands/deploy.js"; import { deployApplicationCommands } from "../framework/commands/deploy.js";
import { env } from "../framework/config/env.js"; import { env } from "../framework/config/env.js";
import { restorePresenceFromStorage } from "../commands/utility/presence.js"; import { restorePresenceFromStorage } from "../commands/presence.js";
import type { CommandRegistry } from "../framework/commands/registry.js"; import type { CommandRegistry } from "../framework/commands/registry.js";
import type { I18nService } from "../framework/i18n/I18nService.js"; import type { I18nService } from "../framework/i18n/I18nService.js";
+1 -1
View File
@@ -11,7 +11,7 @@
import { Client, GatewayIntentBits } from "discord.js"; import { Client, GatewayIntentBits } from "discord.js";
import { commandList } from "./commands/index.js"; import { commandList } from "./commands/index.js";
import { shutdownPresenceRuntime } from "./commands/utility/presence.js"; import { shutdownPresenceRuntime } from "./commands/presence.js";
import { registerEvents } from "./events/index.js"; import { registerEvents } from "./events/index.js";
import { CommandRegistry } from "./framework/commands/registry.js"; import { CommandRegistry } from "./framework/commands/registry.js";
import { env } from "./framework/config/env.js"; import { env } from "./framework/config/env.js";