Créer un module
Un module dans OmniBot est une unité fonctionnelle autonome qui peut être installée et désinstallée par serveur Discord. Chaque module peut contenir des commandes, des écouteurs d'événements, des gestionnaires d'interactions, des services, une configuration et des modèles de base de données.
Générer un module
Ne partez pas de zéro. Générez un squelette fonctionnel (définition, schéma config, une commande, un listener, i18n/en+fr.json, un modèle Prisma commenté, et un test) :
pnpm new-module mon-module "Mon Module"Le module généré compile et son test passe sans retouche. Remplissez la description TODO, puis activez-le via /modules sur votre serveur dev. Aucune étape d'enregistrement : les modules sont auto-découverts depuis src/modules/ au démarrage.
Pour les tests, utilisez les helpers partagés de #lib/testing.js (makeTestConfig, fakeGuild, fakeMessage, initTestI18n) plutôt que des mocks maison. Notez que vi.mock("#core/runtime.js") et vi.mock("#lib/database.js") doivent toujours être déclarés par fichier de test — Vitest les hisse, aucun helper ne peut les masquer.
src/modules/mon-module/
├── mon-module.module.ts # Définition principale du module
├── i18n/ # Fichiers de traduction
│ ├── en.json
│ └── fr.json
├── commands/ # Commandes slash
│ └── saluer.command.ts
├── listeners/ # Écouteurs d'événements
│ └── message-create.listener.ts
├── interactions/ # Gestionnaires de boutons, modales, selects
│ ├── confirmer.button.ts
│ └── retour.modal.ts
├── services/ # Logique métier
│ └── mon-service.service.ts
└── models/ # Schémas Prisma (optionnel)
└── mon-modele.prismaGuide pas à pas : Créer un module « Salut »
TIP
Le name et description du module doivent être en anglais par défaut — ils servent de valeur de repli quand aucun fichier de traduction ne correspond à la locale du serveur. Ajoutez les traductions dans les fichiers i18n/ pour les autres locales.
1. Créer le dossier et le fichier principal
// src/modules/salut/salut.module.ts
import logger from "#lib/logger.js";
import { defineModule } from "#lib/module.js";
export default defineModule({
id: "salut",
name: "Salut",
description: "Accueille les nouveaux membres et dit bonjour.",
version: "1.0.0",
author: "Vous",
onLoad(_client, registry) {
logger.info("Module Salut chargé");
},
onInstall(_client, guild) {
logger.info(`Salut installé sur ${guild.name}`);
},
onUninstall(_client, guild) {
logger.info(`Salut désinstallé de ${guild.name}`);
},
});2. Ajouter une commande slash
// src/modules/salut/commands/bonjour.command.ts
import { SlashCommandBuilder } from "discord.js";
import { declareCommand } from "#lib/command.js";
export default declareCommand({
data: new SlashCommandBuilder()
.setName("bonjour")
.setDescription("Dit bonjour !"),
async execute(interaction) {
await interaction.reply(`Bonjour, ${interaction.user.username} !`);
},
});3. Enregistrer la commande dans le module
// src/modules/salut/salut.module.ts
import bonjourCommand from "./commands/bonjour.command.js";
export default defineModule({
// ... id, nom, etc.
onLoad(_client, registry) {
registry.register(bonjourCommand);
logger.info("Module Salut chargé");
},
// ...
});4. Lancer et tester
Démarrez le bot (voir Pour commencer), installez le module Salut via /modules, puis exécutez /bonjour.
API ModuleDeclaration
interface ModuleDeclaration {
id: string; // Identifiant unique en kebab-case
name: string; // Nom affiché dans l'interface
description: string; // Description affichée dans l'interface
version: Version; // Semver "x.y.z"
author?: string; // Nom de l'auteur (optionnel)
intents?: GatewayIntentBits[]; // Intentions Discord Gateway
partials?: Partials[]; // Partials discord.js, ex. événements sur des messages hors cache
devOnly?: boolean; // Chargé seulement en mode dev
config?: ConfigSchema; // Schéma de configuration
onLoad: (client: Client, registry: Registry) => void;
onInstall: (client: Client, guild: Guild, registry: Registry) => void;
onUninstall: (client: Client, guild: Guild, registry: Registry) => void;
maintenanceTasks?: () => ScheduledTask[];
deleteMemberData?: (guildId: string, userId: string) => Promise<void>;
}Cycles de vie
| Hook | Quand | Utilisation typique |
|---|---|---|
onLoad | Démarrage du bot | Enregistrer commandes, écouteurs, interactions via registry.register() |
onInstall | Module activé sur un serveur | Créer des rôles, envoyer des messages de bienvenue, initialiser des données |
onUninstall | Module désactivé sur un serveur | Nettoyer les données, supprimer les rôles, effacer les configurations |
deleteMemberData | Un membre lance /privacy delete sur un serveur | Supprimer tout ce que le module garde sur ce membre sur ce serveur |
Un module qui enregistre quoi que ce soit lié à l'ID d'un membre doit implémenter deleteMemberData, et décrire ce qu'il enregistre dans la politique de confidentialité (docs/site/*/legal/privacy.md). Il est appelé que le module soit activé ou non sur le serveur, et aussi pour un module écarté parce que Discord n'a pas accordé son intention privilégiée.
Les purges de données stockées vont dans maintenanceTasks, pas dans des tâches enregistrées depuis onLoad : elles sont planifiées pour chaque module découvert, y compris un module écarté parce que Discord n'a pas accordé son intention privilégiée, pour que les durées de conservation de la politique de confidentialité tiennent dans tous les cas.
Gestion des intentions
Déclarez uniquement les intentions nécessaires à votre module. Les intentions requises doivent également être activées dans le Discord Developer Portal sous Bot > Privileged Gateway Intents.
Au démarrage, le bot lit les intentions privilégiées (GuildMembers, GuildPresences, MessageContent) accordées à l'application, en version complète ou limitée. Un module qui en déclare une absente est écarté avec un avertissement, au lieu que Discord refuse la connexion, et /modules le montre comme indisponible en nommant l'intention manquante ; il est rechargé au premier redémarrage qui suit l'octroi de l'intention. Si les flags ne peuvent pas être lus, tous les modules sont chargés, comme avant. Le cœur, lui, ne peut pas être écarté : s'il déclarait un jour une intention privilégiée non accordée, le bot s'arrêterait au démarrage avec une erreur qui la nomme.
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
],Comment fonctionne l'auto-découverte
Le chargeur de modules (src/core/loaders/module-loader.ts) parcourt src/modules/ au démarrage :
- Liste chaque sous-dossier
- Trouve un fichier
*.module.tsà l'intérieur - L'importe dynamiquement
- Vérifie que l'export a
type: DeclarationType.Module - Retourne l'objet
Module
Modules devOnly (avec devOnly: true) sont ignorés quand NODE_ENV n'est pas "development". Utilisez ceci pour les modules de test ou de débogage.
Internationalisation
Les modules peuvent fournir des traductions pour leurs chaînes d'interface (nom, description, champs de configuration et messages du bot) via des fichiers de locale par module.
Ajouter des traductions
Créez un dossier i18n/ dans votre module avec un fichier JSON par locale :
src/modules/mon-module/
├── mon-module.module.ts
├── i18n/
│ ├── en.json
│ └── fr.json
└── commands/
└── ...Le chargeur de modules découvre automatiquement ces fichiers au démarrage et les enregistre dans i18next.
Format des fichiers de traduction
Chaque fichier contient des paires clé-valeur. Le name et la description des champs de configuration sont automatiquement résolus depuis ces fichiers, remplaçant les valeurs par défaut TypeScript quand la locale correspondante est active. Le name et la description du module lui-même se traduisent en revanche dans les fichiers du core, sous modules.<id>.name et modules.<id>.description (voir le guide de localisation) :
{
"config.monChamp.name": "Mon Champ",
"config.monChamp.description": "Description de mon champ.",
"salutation": "Bonjour {{name}} !"
}Utiliser les traductions dans le code
Le ConfigProvider injecté dans les commandes, écouteurs et interactions expose une méthode t() :
async execute(interaction, config) {
const salutation = config.t("salutation", { name: interaction.user.username });
await interaction.reply(salutation);
}Les clés sont d'abord cherchées dans le namespace du module, puis dans celui du cœur. Les libellés communs (config.previous, config.next, modules.enable, etc.) sont fournis par le namespace cœur — pas besoin de les redéfinir dans chaque module.
Sélection de la locale
La locale du serveur est configurée via les paramètres du module Cœur (/config core > locale). Quand un fichier de locale n'existe pas pour la langue sélectionnée, le système utilise l'anglais par défaut.
Liste de vérification avant publication
Avant que votre module n'atteigne des serveurs de production :
- Incrémentez
versionquand vous ajoutez, renommez ou modifiez une commande — la production ne ré-enregistre les commandes que lors d'un changement de version (le dev resynchronise à chaque démarrage, facile à rater en local) - Listez les commandes supprimées ou renommées dans
retiredCommands(anciens noms) en plus d'incrémenter la version — la mise à jour en production ne supprime que les commandes que le module nomme encore, sinon l'ancienne commande reste sur tous les serveurs - Renseignez
DEV_GUILD_IDdans.env— sans lui, le mode dev n'enregistre aucune commande - Préfixez noms de commandes et
customIdavec votre id de module — les deux sont résolus premier-arrivé-premier-servi entre tous les modules, et un:dans un argument décale le parsing descustomId - Gardez
if (!config) returndans les listeners — hors serveurs (MP), ils tournent avec une configundefined - Mettez
requiresAdmin: truesur chaque commande et handler sensible — sinon exposé à tout le monde - Jamais d'objet en
defaultValued'entité (USER/ROLE/CHANNEL/CATEGORYrestent non renseignés ; les ids sont stockés, les objets sont réhydratés) - Déclarez les
optionsdesENUMavecas constpour typerconfig.get()en union littérale - Ordre Prisma : écrire
models/*.prisma, puispnpm prisma:generate, puispnpm prisma:migrate— et gardez des noms de modèles uniques entre modules - Pas de side-effect au top-level — les modules
devOnlysont quand même importés en production ; seul leur enregistrement est sauté
Bonnes pratiques
- Gardez
onLoadléger — enregistrez les artefacts et loggez, déplacez la logique dans les services - Utilisez le kebab-case pour les IDs de module et les noms de dossiers
- Gérez les erreurs dans les hooks de cycle de vie — utilisez try/catch dans
onInstall/onUninstall - Organisez les fichiers — un artefact par fichier, utilisez des sous-dossiers
- Déclarez uniquement les intentions nécessaires — minimisez l'utilisation des intentions privilégiées
Prochaines étapes
- Commandes — Ajouter des commandes slash avec options et autocomplétion
- Écouteurs — Réagir aux événements Discord
- Interactions — Boutons, menus de sélection et modales
- Configuration — Déclarer un schéma de configuration typé
- Services — Organiser la logique métier
- Base de données — Persister les données avec Prisma