diff --git a/docs/site/.vitepress/config.ts b/docs/site/.vitepress/config.ts index d7be5d8..964a663 100644 --- a/docs/site/.vitepress/config.ts +++ b/docs/site/.vitepress/config.ts @@ -40,6 +40,7 @@ export default defineConfig({ text: "Guide", items: [ { text: "Pour commencer", link: "/fr/guide/getting-started" }, + { text: "Architecture", link: "/fr/guide/architecture" }, { text: "Créer un module", link: "/fr/guide/creating-a-module", @@ -50,6 +51,14 @@ export default defineConfig({ { text: "Configuration", link: "/fr/guide/configuration" }, { text: "Services", link: "/fr/guide/services" }, { text: "Base de données", link: "/fr/guide/database" }, + { text: "Contribuer", link: "/fr/guide/contributing" }, + ], + }, + { + text: "Juridique", + items: [ + { text: "Confidentialité", link: "/fr/legal/privacy" }, + { text: "CGU", link: "/fr/legal/terms" }, ], }, ], @@ -85,6 +94,7 @@ export default defineConfig({ text: "Guide", items: [ { text: "Getting Started", link: "/en/guide/getting-started" }, + { text: "Architecture", link: "/en/guide/architecture" }, { text: "Creating a Module", link: "/en/guide/creating-a-module", @@ -95,6 +105,14 @@ export default defineConfig({ { text: "Configuration", link: "/en/guide/configuration" }, { text: "Services", link: "/en/guide/services" }, { text: "Database", link: "/en/guide/database" }, + { text: "Contributing", link: "/en/guide/contributing" }, + ], + }, + { + text: "Legal", + items: [ + { text: "Privacy", link: "/en/legal/privacy" }, + { text: "Terms", link: "/en/legal/terms" }, ], }, ], diff --git a/docs/site/en/guide/architecture.md b/docs/site/en/guide/architecture.md new file mode 100644 index 0000000..9e16bc5 --- /dev/null +++ b/docs/site/en/guide/architecture.md @@ -0,0 +1,146 @@ +# Architecture + +This page explains how OmniBot works under the hood. Understanding this will help you create better modules and debug issues. + +## Boot Sequence + +When the bot starts (`src/index.ts`), it follows this sequence: + +``` +1. Database health check + └─ prisma.$queryRaw`SELECT 1` + └─ exits with error if DB is unavailable + +2. Module discovery + └─ loadModules("./modules") + └─ scans each subdirectory in src/modules/ + └─ imports the *.module.ts file + └─ skips devOnly modules in production + +3. Intent aggregation + └─ collects GatewayIntentBits from all modules + └─ creates the Discord Client with the union of all intents + +4. ClientReady handler (async) + ├─ For each module: + │ ├─ module.onLoad(client, registry) + │ └─ loadModuleEvents(client, module) + ├─ coreModule.onLoad(client, coreModule.registry) + ├─ syncCommands(client, modules) + │ ├─ Dev mode: bulk PUT on DEV_GUILD_ID + │ └─ Prod mode: version-gated guild commands + global core commands + └─ loadGlobalEvents(client) + +5. Login + └─ client.login(token) + +6. Shutdown handlers + └─ SIGTERM / SIGINT → client.destroy() + prisma.$disconnect() +``` + +## Module Auto-Discovery + +Modules are **not registered manually**. The loader (`src/core/loaders/module-loader.ts`) discovers them automatically: + +1. Lists all subdirectories in `src/modules/` +2. For each directory, finds a file matching `*.module.ts` (or `*.module.js`) +3. Dynamically imports the module +4. Validates that it has `type: DeclarationType.Module` +5. Returns a `Module` object with its `Registry` + +This means adding a new module is as simple as creating a folder with a `*.module.ts` file. No configuration file to edit, no imports to add. + +## Registry & Declared Pattern + +Each module has its own **Registry** instance (`src/lib/registry.ts`) that collects three kinds of artifacts: + +- `commands: Declared[]` +- `listeners: Declared[]` +- `interactionHandlers: Declared[]` + +The `register()` method uses a discriminated union — it checks `handler.type` (from the `DeclarationType` enum) and pushes into the correct array. + +Every dynamically imported file (command, listener, interaction) is wrapped with a `declare*()` function that tags it with its `DeclarationType`. This allows the loaders to identify what kind of artifact they're dealing with without naming conventions or configuration. + +```typescript +// The declared pattern +export enum DeclarationType { + Module = "module", + Command = "command", + Listener = "listener", + Interaction = "interaction", + Service = "service", +} +``` + +## Centralized Interaction Dispatch + +A single `interactionCreate` listener (`src/core/listeners/interaction-create.listener.ts`) handles all user interactions: + +``` +interactionCreate +├─ isChatInputCommand() +│ ├─ find command by name across all modules +│ ├─ check module activation state (DB) +│ ├─ load config (ConfigProvider) +│ └─ execute(interaction, config) +├─ isAutocomplete() +│ ├─ find command +│ ├─ check activation +│ └─ complete(interaction, config) +├─ isMessageComponent() or isModalSubmit() +│ ├─ split customId on ":" → prefix + args +│ ├─ find handler by prefix +│ ├─ check module activation +│ ├─ if requiresAdmin → check Administrator permission +│ ├─ run type guard (check()) +│ └─ execute(interaction, args, config) +``` + +This centralised approach means: + +- **Permissions** are checked in one place (the `requiresAdmin` flag on `InteractionHandler`) +- **Module activation** is verified automatically +- **Config injection** happens transparently + +## Command Propagation + +### Development Mode + +When `NODE_ENV=development`, all commands (core + enabled modules) are registered in a **single PUT** on `DEV_GUILD_ID`. This is **instant** — no propagation delay. Every bot restart re-syncs all commands. + +### Production Mode + +- **Core commands** (`/modules`, `/config`): registered **globally** (~1 hour propagation). +- **Module commands**: registered **per guild**. To avoid re-registering on every startup, the system uses **version gating**: it compares the module's declared version with the `activatedVersion` stored in the database. Commands are only re-registered on guilds where the version differs. + +## Config System Overview + +The configuration system has several layers: + +1. **Schema declaration** — Modules declare typed schemas in `defineModule({ config: {...} })` +2. **Storage** — All configs are stored as a single JSON blob per guild in the `GuildConfiguration` table +3. **Cache** — `ConfigService` holds an in-memory cache (`configCache`) to avoid DB reads on every interaction +4. **Deserialization** — Entity IDs (user, role, channel) are stored as strings and resolved to Discord objects at read time +5. **Admin UI** — `/config ` renders an interactive panel with per-field edit controls + +## Service Layer + +Unlike modules, **services are not auto-discovered**. They are plain TypeScript classes or objects tagged with `declareService()`. You import them directly where needed: + +```typescript +import queue from "../services/thread-creation-queue.js"; +``` + +This keeps the service layer simple and dependency-free. + +## Listener Auto-Activation + +When a module listener is registered, the loader wraps it with logic that: + +1. Extracts the `guildId` from the event arguments +2. Looks up the module's activation state in the database +3. If disabled → silently returns (no-op) +4. If enabled → fetches the config and calls `execute()` with the config as the last argument + +For events that can occur outside a guild (e.g., DMs), the listener must handle activation checks manually — there's no guild context to look up. diff --git a/docs/site/en/guide/commands.md b/docs/site/en/guide/commands.md index 56e920b..7a67ebd 100644 --- a/docs/site/en/guide/commands.md +++ b/docs/site/en/guide/commands.md @@ -1,66 +1,174 @@ # Commands -This guide explains how to create Discord slash commands in your modules. Commands are only available when the module is enabled on the guild. +This guide explains how to create Discord slash commands in your modules. Commands are only available when the module is enabled on the server where they're used. -## Creating a command +## Creating a Command ```typescript -// src/modules/my-module/commands/test.command.ts +// src/modules/greeter/commands/hello.command.ts import { SlashCommandBuilder } from "discord.js"; import { declareCommand } from "#lib/command.js"; export default declareCommand({ data: new SlashCommandBuilder() - .setName("test") - .setDescription("A test command"), + .setName("hello") + .setDescription("Says hello!"), async execute(interaction) { - await interaction.reply("Test!"); + await interaction.reply(`Hello, ${interaction.user.username}!`); }, }); ``` -## Command interface +### Command Interface ```typescript interface Command { - data: SlashCommandBuilder; // Command configuration + data: + | SlashCommandBuilder + | SlashCommandSubcommandBuilder + | SlashCommandSubcommandGroupBuilder + | SlashCommandSubcommandsOnlyBuilder + | SlashCommandOptionsOnlyBuilder; execute: ( - // Execution function (required) - interaction, - config + interaction: ChatInputCommandInteraction, + config: ConfigProvider ) => Promise; complete?: ( - // Autocomplete (optional) - interaction, - config + interaction: AutocompleteInteraction, + config: ConfigProvider ) => Promise; } ``` -The `config` parameter is a `ConfigProvider` giving access to the module's configuration (see [Configuration](./configuration)). +| Field | Required | Description | +| ---------- | -------- | --------------------------------------------------------- | +| `data` | Yes | The slash command definition (name, description, options) | +| `execute` | Yes | Called when a user runs the command | +| `complete` | No | Called when a user types in an autocomplete option | -## Registering in the module +### Config Access + +The `config` parameter is a `ConfigProvider` that gives access to the module's configuration (see [Configuration](./configuration)). It's always injected — even for modules without a config schema. + +> [!NOTE] +> In `complete()` (autocomplete) handlers, the injected `config` currently comes from the **Core** module rather than the command's module. This means module-specific config values are not available during autocomplete — only the Core module's config is accessible. + +```typescript +async execute(interaction, config) { + const channel = config.get("logChannel"); + const max = config.get("maxWarnings"); + // ... +} +``` + +## Options & Subcommands + +### Basic Options + +```typescript +data: new SlashCommandBuilder() + .setName("greet") + .setDescription("Greet someone") + .addUserOption((option) => + option.setName("target").setDescription("Who to greet").setRequired(true) + ) + .addStringOption((option) => + option.setName("message").setDescription("Custom message").setMaxLength(200) + ); +``` + +### Autocomplete + +```typescript +export default declareCommand({ + data: new SlashCommandBuilder() + .setName("color") + .setDescription("Pick a color") + .addStringOption((option) => + option + .setName("color") + .setDescription("Choose a color") + .setAutocomplete(true) + .setRequired(true) + ), + + async execute(interaction, config) { + const color = interaction.options.getString("color", true); + await interaction.reply(`You picked ${color}!`); + }, + + async complete(interaction, config) { + const colors = ["red", "green", "blue", "yellow", "purple"]; + const input = interaction.options.getFocused().toLowerCase(); + const filtered = colors.filter((c) => c.startsWith(input)); + await interaction.respond(filtered.map((c) => ({ name: c, value: c }))); + }, +}); +``` + +### Subcommands + +```typescript +const builder = new SlashCommandBuilder() + .setName("config") + .setDescription("Configuration commands") + .addSubcommand((sub) => + sub.setName("view").setDescription("View configuration") + ) + .addSubcommand((sub) => + sub + .setName("set") + .setDescription("Set a value") + .addStringOption((opt) => + opt.setName("key").setDescription("Config key").setRequired(true) + ) + .addStringOption((opt) => + opt.setName("value").setDescription("Config value").setRequired(true) + ) + ); +``` + +## Registration & Propagation + +### In the Module ```typescript -// src/modules/my-module/my-module.module.ts -import testCommand from "./commands/test.command.js"; +// src/modules/greeter/greeter.module.ts +import helloCommand from "./commands/hello.command.js"; export default defineModule({ onLoad(_client, registry) { - registry.register(testCommand); + registry.register(helloCommand); }, }); ``` -## Registration & propagation +### Dev vs Production -| Commands | Production | Development (`NODE_ENV=development`) | -| -------------------------------- | ------------------------------------------------- | -------------------------------------------------------------------- | -| **core** (`/config`, `/modules`) | Global (~1 h propagation) | Registered on `DEV_GUILD_ID` — **instant** | -| **module** | Per guild, (re)installed on module `version` bump | Re-synced **at every startup** on `DEV_GUILD_ID` for enabled modules | +| Aspect | Development (`NODE_ENV=development`) | Production | +| ------------------- | ------------------------------------------------------------- | -------------------------------------------- | +| **Core commands** | Registered on `DEV_GUILD_ID` (instant) | Global (~1h propagation) | +| **Module commands** | Re-synced every startup on `DEV_GUILD_ID` for enabled modules | Registered per guild, only on version change | +| **Propagation** | Instant (single PUT) | Delayed (global) or on-demand (per guild) | -In production, version gating prevents re-pushing module commands to all guilds on every startup. In development, all commands are registered in a single PUT on the dev guild for instant updates. +The version gating system in production uses the `activatedVersion` field in the `ModuleActivation` database record. Commands are re-registered on a guild only when the module's declared version differs from the stored version. This avoids unnecessary API calls on every restart. > `DEV_GUILD_ID` is **required** in development mode. See `.env.example`. + +## Core Commands + +OmniBot provides two built-in commands in the **Core** module (always active, non-uninstallable): + +| Command | Permission | Description | +| ------------------ | ------------- | ------------------------------------------------------ | +| `/modules` | Administrator | Lists all modules with enable/disable buttons | +| `/config ` | Administrator | Opens the interactive configuration panel for a module | + +## Best Practices + +- **Use reply, deferReply, or editReply** appropriately — defer for operations that take longer than 3 seconds +- **Use ephemeral replies** for user-specific responses (`flags: MessageFlags.Ephemeral`) +- **Handle errors** — wrap risky operations in try/catch and reply with a user-friendly message +- **Validate option values** — use the builder's built-in validation (min/max length, min/max value, etc.) diff --git a/docs/site/en/guide/configuration.md b/docs/site/en/guide/configuration.md index f535961..f4eea9f 100644 --- a/docs/site/en/guide/configuration.md +++ b/docs/site/en/guide/configuration.md @@ -1,8 +1,8 @@ # Configuration -OmniBot provides a typed configuration system for modules. Each module can declare a configuration schema that is automatically exposed through an interactive Discord interface accessible to administrators. +OmniBot provides a typed configuration system for modules. Each module can declare a configuration schema that is automatically exposed through an interactive Discord interface, accessible to administrators via `/config `. -## Declaring a configuration schema +## Declaring a Schema A module declares its schema in `defineModule()` via the `config` property: @@ -45,20 +45,22 @@ export default defineModule({ }); ``` -## Supported field types +## Field Types -| Type | Input | Storage (JSON) | Read | -| ---------- | --------------------- | ----------------- | ----------------- | -| `STRING` | Modal (text) | `string` | `string` | -| `NUMBER` | Modal (text → number) | `number` | `number` | -| `BOOLEAN` | Toggle button | `boolean` | `boolean` | -| `USER` | User select menu | `string` (id) | `User` | -| `ROLE` | Role select menu | `string` (id) | `Role` | -| `CHANNEL` | Channel select menu | `string` (id) | `Channel` | -| `CATEGORY` | Category select menu | `string` (id) | `CategoryChannel` | -| `ENUM` | Select menu (choices) | `string` (choice) | Literal union | +### Supported Types -## Lists +| Type | Input | Storage (JSON) | Read (deserialized) | +| ---------- | --------------------------- | ----------------- | ------------------- | +| `STRING` | Modal (text input) | `string` | `string` | +| `NUMBER` | Modal (text → number) | `number` | `number` | +| `BOOLEAN` | Toggle button | `boolean` | `boolean` | +| `USER` | User select menu | `string` (id) | `User` | +| `ROLE` | Role select menu | `string` (id) | `Role` | +| `CHANNEL` | Channel select menu | `string` (id) | `Channel` | +| `CATEGORY` | Category select menu | `string` (id) | `CategoryChannel` | +| `ENUM` | Select menu (fixed choices) | `string` (choice) | Literal union | + +### Lists Any type can be declared as a **list** using `type: [ConfigType.X]`: @@ -66,20 +68,25 @@ Any type can be declared as a **list** using `type: [ConfigType.X]`: config: { channels: { name: "Monitored channels", - description: "Channels to monitor (one or more)", - type: [ConfigType.CHANNEL], // Channel list + description: "Channels to monitor", + type: [ConfigType.CHANNEL], // List of channels }, - warnings: { - name: "Warnings", - description: "List of warnings", - type: [ConfigType.NUMBER], // Number list + scores: { + name: "Scores", + description: "High scores list", + type: [ConfigType.NUMBER], // List of numbers }, } ``` -The returned type of `config.get("channels")` is `Channel[]`. +List editing depends on the element type: + +- **Entities** (`USER`, `ROLE`, `CHANNEL`, `CATEGORY`): native multi-select menus +- **ENUM**: multi-select menu with the declared options +- **Scalars** (`STRING`, `NUMBER`): dedicated add/remove editor +- **`BOOLEAN`**: add/remove editor with toggle buttons -## ENUM type (fixed choices) +### ENUM Type (Fixed Choices) The `ENUM` type restricts a field to a fixed set of values: @@ -95,59 +102,91 @@ config: { features: { name: "Features", description: "Enabled features", - type: [ConfigType.ENUM], // Multi-select list + type: [ConfigType.ENUM], options: ["welcome", "logs", "automod"] as const, }, } ``` -- `options` is **required** on an `ENUM` entry +- `options` is **required** on `ENUM` entries (enforced by the type system) - Using `as const` gives precise typing: `config.get("theme")` returns `"light" | "dark" | "auto"` - Without `as const`, the returned type is `string` -## Default values +## Default Values + +- A field **with** `defaultValue` is always present — the type is `T` (never `undefined`) +- A field **without** `defaultValue` may be `T | undefined` (never set by the admin) +- Entity fields without defaults return `undefined` (not a fake value) -- A field **with** `defaultValue` is always present (`T`) -- A field **without** `defaultValue` may be `undefined` (`T | undefined`) -- Discord entities are automatically deserialized (stored as id, read as Discord objects) +This is an important distinction — always check for `undefined` when accessing fields without defaults. -## Accessing configuration +## Accessing Configuration -Configuration is accessible via the `config` parameter in commands, listeners, and interactions: +The `config` parameter is passed automatically to commands, listeners, and interactions: ```typescript -// In a command -async execute(interaction, config) { - const channel = config.get("logChannel"); // Channel | undefined - const max = config.get("maxWarnings"); // number (has defaultValue) - const level = config.get("logLevel"); // "debug" | "info" | "warn" | "error" +async function handler(interaction, config) { + const channel = config.get("logChannel"); // Channel | undefined + const max = config.get("maxWarnings"); // number (always present) + const level = config.get("logLevel"); // "debug" | "info" | "warn" | "error" + + // Use with default + const safe = config.get("logChannel") ?? someDefaultChannel; } ``` -## Admin interface +## Admin Interface + +### `/config ` Command + +- **Admin only** (`Administrator` permission required) +- **Public response** — configuration is visible to everyone (transparency) +- **Autocomplete** on module name (core + enabled modules) -The `/config ` command (admin-only) displays an interactive configuration panel featuring: +The panel displays: -- All configurable fields with their type and current value -- Edit buttons adapted to each type (modal, toggle, select menu) -- Automatic pagination if the module has more than 10 fields -- Navigation bar when multiple pages +- All configurable fields with their type, description, and current value +- Edit buttons adapted to each type (button toggle, modal, select menu) +- Pagination when there are more than 10 fields (Discord's 40-component limit) -Changes are persisted and take effect immediately. +### Editing Flow -## Best practices +| Field Type | Editor Type | Persistence | UI Update | +| ------------------------------------- | ------------------------------ | ----------- | ------------------------------- | +| `STRING` | Modal (text input) | On submit | Public message updated in place | +| `NUMBER` | Modal (number input) | On submit | Public message updated in place | +| `BOOLEAN` | Toggle button | On click | Public message updated in place | +| `USER`, `ROLE`, `CHANNEL`, `CATEGORY` | Entity select menu (ephemeral) | On select | Public message refreshed | +| `ENUM` | String select menu (ephemeral) | On select | Public message refreshed | +| List of scalars | Add/remove editor (ephemeral) | On action | Public message refreshed | -### Field naming +**Ephemeral editors** (select menus, list editors) refresh the public config message after each change using `refreshSourceConfigMessage()`. The source message ID is threaded through the `customId` of the ephemeral components. + +## Persistence + +Configuration is stored as a single JSON blob per guild in the `GuildConfiguration` table: + +```prisma +model GuildConfiguration { + guildId String @id + data Json // Record +} +``` -- **Key**: camelCase (`logChannel`, `maxWarnings`) -- **name**: human-readable, shown in the UI (`Log channel`) -- **description**: explains the field's purpose +- Entity IDs (user, role, channel) are stored as strings and **deserialized** to Discord objects at read time +- An in-memory cache (`configCache`) avoids database reads on every interaction +- The cache is invalidated on every write -### Validation +## Best Practices -Validation is automatic based on the declared type. For `ENUM`, only values listed in `options` are accepted. +- **Use camelCase for keys**, human-readable `name` and clear `description` +- **Provide `defaultValue`** for fields that should always have a value +- **Keep schemas focused** — too many fields makes the admin panel unwieldy +- **Use `as const`** on ENUM options for type-safe literal unions +- **Check for `undefined`** when accessing fields without defaults -## Next steps +## Next Steps - [Services](./services) for organizing business logic - [Database](./database) for advanced persistent data +- [Commands](./commands) for creating slash commands diff --git a/docs/site/en/guide/contributing.md b/docs/site/en/guide/contributing.md new file mode 100644 index 0000000..d068a2b --- /dev/null +++ b/docs/site/en/guide/contributing.md @@ -0,0 +1,139 @@ +# Contributing + +Contributions are welcome from everyone — Graven - Développement community staff and members alike. This project is developed for and by the [Graven - Développement](https://discord.gg/graven) Discord community. + +## Getting Started + +1. Fork the repository on GitHub +2. Clone your fork +3. Set up the development environment (see [Getting Started](./getting-started)) +4. Create a branch for your work + +```bash +git checkout -b feat/my-feature +``` + +## Conventional Commits + +Commit messages must follow the [Conventional Commits](https://www.conventionalcommits.org/) format. This is enforced by **commitlint** via a lefthook commit-msg hook. + +``` +type(scope): description + +feat(thread-creator): add paced creation queue +fix(config): handle undefined values in number handler +docs(commands): explain autocomplete flow +chore(deps): update pnpm to v11 +``` + +### Allowed Types + +| Type | Usage | +| ---------- | ------------------------------------------ | +| `feat` | A new feature | +| `fix` | A bug fix | +| `chore` | Maintenance, dependencies, tooling | +| `docs` | Documentation changes | +| `refactor` | Code restructuring without feature changes | +| `test` | Adding or updating tests | +| `ci` | CI/CD configuration changes | + +## Before Opening a PR + +Run these checks locally — the same checks run in CI: + +```bash +pnpm build # Must succeed +pnpm test # All tests must pass (unit + integration) +pnpm exec oxlint --deny-warnings # No lint warnings +pnpm exec oxfmt --check # Code formatting must be correct +``` + +To auto-fix formatting and lint issues: + +```bash +pnpm format # oxfmt --write + oxlint --fix --fix-suggestions +``` + +## PR Workflow + +1. **Branch from `master`** — keep your branch up to date +2. **Make focused commits** — each commit should represent one logical change +3. **Keep PRs small** — easier to review, faster to merge +4. **Open a draft PR early** — get feedback while you work +5. **Ensure CI is green** — fix any failing checks +6. **Request review** — at least one approval required + +### PR Title Convention + +Follow the same Conventional Commits format for PR titles: + +``` +feat(module): add message logging functionality +fix(core): resolve permission check race condition +``` + +## Code Style + +### Formatting + +- **oxfmt** handles all formatting (replaces Prettier) +- Run `pnpm format` before committing +- The `pre-commit` lefthook hook runs oxfmt + oxlint on staged files + +### Linting + +- **oxlint** enforces code quality rules +- Run `pnpm exec oxlint --deny-warnings` to check +- Some warnings can be auto-fixed with `pnpm format` + +### Imports + +- Cross-directory: use `#` subpath imports (`#lib/config.js`, `#core/...`) +- Same-directory: use relative imports (`./file.js`) +- Always include the `.js` extension (NodeNext) + +### Naming + +- Files use role suffixes: `*.command.ts`, `*.listener.ts`, `*.button.ts`, `*.service.ts` +- Module IDs are kebab-case: `thread-creator`, `my-module` +- Types and interfaces are PascalCase + +## Testing + +We use **Vitest** for testing. Tests are co-located with source files: + +``` +src/lib/config.ts # Source +src/lib/config.test.ts # Tests +src/core/services/module.service.ts +src/core/services/module.service.test.ts +``` + +### Running Tests + +```bash +pnpm test # All tests +pnpm test:unit # Unit tests only +pnpm test:integration # Integration tests only +pnpm test -- --watch # Watch mode +``` + +### Writing Tests + +- **Unit tests** test isolated logic (parsers, utilities, algorithms) +- **Integration tests** test interactions with Discord API (mocked) and database +- Tests use `vi.mock()` to isolate dependencies +- Check existing tests for patterns and conventions + +## Finding Tasks + +- **[AUDIT.md](https://github.com/GravenDev/OmniBot/blob/master/AUDIT.md)** — tracks tech debt, known issues, and follow-ups +- **[GitHub Issues](https://github.com/GravenDev/OmniBot/issues)** — feature requests and bug reports +- **[Review docs](https://github.com/GravenDev/OmniBot/tree/master/docs/review)** — code review findings with actionable items + +## Need Help? + +- Join the [Graven - Développement](https://discord.gg/graven) Discord server +- Open a [GitHub Discussion](https://github.com/GravenDev/OmniBot/discussions) +- Check the [documentation](/) for guides and reference diff --git a/docs/site/en/guide/creating-a-module.md b/docs/site/en/guide/creating-a-module.md index 3ae7543..0c64c89 100644 --- a/docs/site/en/guide/creating-a-module.md +++ b/docs/site/en/guide/creating-a-module.md @@ -1,146 +1,160 @@ # Creating a Module -A module in OmniBot is a self-contained functional unit that can be installed and uninstalled per Discord guild. Each module can contain commands, event listeners, interactions, and its own data. +A module in OmniBot is a self-contained functional unit that can be installed and uninstalled per Discord server. Each module can contain commands, event listeners, interaction handlers, services, configuration, and database models. -## Module structure +## Module Structure ``` src/modules/my-module/ ├── my-module.module.ts # Main module definition -├── commands/ # Module commands -│ └── my-command.command.ts +├── commands/ # Slash commands +│ └── greet.command.ts ├── listeners/ # Event listeners -│ └── my-listener.listener.ts -├── interactions/ # Interaction handlers (buttons, etc.) -│ └── my-button.button.ts -├── services/ # Business services +│ └── message-create.listener.ts +├── interactions/ # Button, modal, select handlers +│ ├── confirm.button.ts +│ └── feedback.modal.ts +├── services/ # Business logic │ └── my-service.service.ts -└── models/ # Prisma models (optional) +└── models/ # Prisma schemas (optional) └── my-model.prisma ``` -## Creating a module +## Step-by-Step: Creating a "Greeter" Module -### 1. Define the main module file +### 1. Create the directory and main file ```typescript -// src/modules/my-module/my-module.module.ts +// src/modules/greeter/greeter.module.ts -import { GatewayIntentBits } from "discord.js"; import { defineModule } from "#lib/module.js"; import logger from "#lib/logger.js"; export default defineModule({ - id: "my-module", - name: "My Module", - description: "Description of my custom module.", + id: "greeter", + name: "Greeter", + description: "Welcomes new members and says hello.", version: "1.0.0", - author: "Your Name", // Optional + author: "You", - // Discord intents required by the module - intents: [ - GatewayIntentBits.Guilds, - GatewayIntentBits.GuildMessages, - GatewayIntentBits.MessageContent, - ], - - // Called at bot startup onLoad(_client, registry) { - // Register commands, listeners, and interactions - // registry.register(myCommand); - // registry.register(myListener); - // registry.register(myInteraction); - - logger.info("My module loaded successfully"); + logger.info("Greeter module loaded"); }, - // Called when the module is installed on a guild onInstall(_client, guild) { - logger.info(`My module installed on ${guild.name}`); + logger.info(`Greeter installed on ${guild.name}`); }, - // Called when the module is uninstalled from a guild onUninstall(_client, guild) { - logger.info(`My module uninstalled from ${guild.name}`); + logger.info(`Greeter uninstalled from ${guild.name}`); }, }); ``` -### 2. Lifecycle hooks +### 2. Add a slash command + +```typescript +// src/modules/greeter/commands/hello.command.ts + +import { SlashCommandBuilder } from "discord.js"; +import { declareCommand } from "#lib/command.js"; -A module exposes three hooks: +export default declareCommand({ + data: new SlashCommandBuilder() + .setName("hello") + .setDescription("Says hello!"), -| Hook | When | Usage | -| ------------- | -------------------- | ------------------------------------------ | -| `onLoad` | Bot startup | Register commands, listeners, interactions | -| `onInstall` | Guild installation | Create roles, initialize data | -| `onUninstall` | Guild uninstallation | Clean up data, remove configurations | + async execute(interaction) { + await interaction.reply(`Hello, ${interaction.user.username}!`); + }, +}); +``` -### 3. ModuleDeclaration interface +### 3. Register the command in the module + +```typescript +// src/modules/greeter/greeter.module.ts +import helloCommand from "./commands/hello.command.js"; + +export default defineModule({ + // ... id, name, etc. + + onLoad(_client, registry) { + registry.register(helloCommand); + logger.info("Greeter module loaded"); + }, + // ... +}); +``` + +### 4. Run and test + +Start the bot (see [Getting Started](./getting-started)), install the Greeter module via `/modules`, then run `/hello`. + +## ModuleDeclaration API ```typescript interface ModuleDeclaration { - id: string; // Unique identifier (kebab-case) - name: string; // Display name - description: string; // Description - version: string; // Semver version (e.g. "1.0.0") - author?: string; // Author (optional) - intents?: GatewayIntentBits[]; // Required Discord intents + id: string; // Unique kebab-case identifier + name: string; // Display name shown in UI + description: string; // Description shown in UI + version: Version; // Semver "x.y.z" + author?: string; // Optional author name + intents?: GatewayIntentBits[]; // Discord gateway intents devOnly?: boolean; // Only loaded in dev mode - config?: ConfigSchema; // Configuration schema (see Configuration) + config?: ConfigSchema; // Configuration schema - onLoad: (client, registry) => void; - onInstall: (client, guild, registry) => void; - onUninstall: (client, guild, registry) => void; + onLoad: (client: Client, registry: Registry) => void; + onInstall: (client: Client, guild: Guild, registry: Registry) => void; + onUninstall: (client: Client, guild: Guild, registry: Registry) => void; } ``` -## Best practices - -### Naming conventions +### Lifecycle Hooks -- **Module ID**: kebab-case (`my-super-module`) -- **File name**: `{id}.module.ts` -- **Directory**: same as module ID +| Hook | When | Typical Use | +| ------------- | -------------------------- | -------------------------------------------------------------------- | +| `onLoad` | Bot startup | Register commands, listeners, interactions via `registry.register()` | +| `onInstall` | Module enabled on a guild | Create roles, send welcome messages, initialize data | +| `onUninstall` | Module disabled on a guild | Clean up data, remove roles, delete configurations | -### Intent management +### Intent Management -Only declare the intents your module actually needs. See the [Discord documentation](https://discord.com/developers/docs/events/gateway#list-of-intents) for which intents are required. - -### Error handling +Only declare the intents your module actually needs. Required intents must also be enabled in the Discord Developer Portal under Bot > Privileged Gateway Intents. ```typescript -onInstall(_client, guild) { - try { - // Installation logic - } catch (error) { - logger.error(`Installation error: ${error.message}`); - } -} +intents: [ + GatewayIntentBits.Guilds, + GatewayIntentBits.GuildMessages, + GatewayIntentBits.MessageContent, +], ``` -### File organization +## How Auto-Discovery Works -- Keep the main module file lightweight -- Move complex logic to services -- Use subdirectories to organize commands, listeners, and interactions +The module loader (`src/core/loaders/module-loader.ts`) scans `src/modules/` at startup: -## Auto-discovery +1. Lists each subdirectory +2. Finds a `*.module.ts` file inside +3. Imports it dynamically +4. Checks the export has `type: DeclarationType.Module` +5. Returns the `Module` object -Modules are automatically loaded from `src/modules/` at startup. The loader (`module-loader.ts`): +**Dev-only modules** (with `devOnly: true`) are skipped when `NODE_ENV` is not `"development"`. Use this for test or debug modules. -1. Iterates over each subdirectory in `src/modules/` -2. Looks for a `*.module.ts` (or `*.module.js`) file -3. Imports the module and validates it exposes a `Module`-typed default export -4. Skips modules marked `devOnly: true` in production +## Best Practices -No manual registration is needed. +- **Keep `onLoad` lean** — register artifacts and log, move logic to services +- **Use kebab-case** for module IDs and folder names +- **Handle errors in lifecycle hooks** — use try/catch in `onInstall`/`onUninstall` +- **Keep files organized** — one artifact per file, use subdirectories +- **Declare only needed intents** — minimize privileged intent usage -## Next steps +## Next Steps -- [Commands](./commands) — Add slash commands to your module +- [Commands](./commands) — Add slash commands with options and autocomplete - [Listeners](./listeners) — React to Discord events -- [Interactions](./interactions) — Buttons, modals, and select menus -- [Configuration](./configuration) — Declare a configuration schema +- [Interactions](./interactions) — Buttons, select menus, and modals +- [Configuration](./configuration) — Declare a typed configuration schema - [Services](./services) — Organize business logic -- [Database](./database) — Use Prisma with your module +- [Database](./database) — Persist data with Prisma diff --git a/docs/site/en/guide/database.md b/docs/site/en/guide/database.md index 4c5c229..8380354 100644 --- a/docs/site/en/guide/database.md +++ b/docs/site/en/guide/database.md @@ -1,28 +1,27 @@ # Database -OmniBot uses Prisma as its ORM with a multi-file architecture that distributes database models across different modules. +OmniBot uses [Prisma](https://www.prisma.io/) as its ORM with a multi-file schema architecture. Database models are distributed across modules and consolidated at build time. ## Architecture -### Prisma file structure +### File Layout ``` src/ ├── prisma/ -│ ├── dbinfo.prisma # Generator and datasource configuration -│ └── schema.prisma # Consolidated schema (generated) +│ ├── dbinfo.prisma # Generator + datasource configuration +│ └── schema.prisma # Consolidated schema (auto-generated, do not edit) ├── core/ │ └── models/ -│ └── modules.prisma # Core module system models +│ ├── config.prisma # GuildConfiguration model +│ └── modules.prisma # ModuleActivation model └── modules/ - └── [module-name]/ + └── my-module/ └── models/ - └── *.prisma # Module-specific models + └── my-model.prisma # Module-specific models ``` -### Configuration - -The `dbinfo.prisma` file contains the base configuration: +### Base Configuration (`dbinfo.prisma`) ```prisma generator client { @@ -36,83 +35,126 @@ datasource db { } ``` -## Adding a model to your module +## Adding a Model to Your Module -1. Create a `.prisma` file in your module's `models/` directory: +### 1. Create the `.prisma` file ``` src/modules/my-module/models/my-model.prisma ``` -2. Define your model: +### 2. Define your model ```prisma model MyModel { id String @id @default(cuid()) - name String + userId String + username String createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt } ``` -3. Generate the Prisma client: +### 3. Generate the Prisma client ```bash pnpm prisma:generate ``` -4. Create a migration: +This runs the consolidation script then `prisma generate`. + +### 4. Create a migration ```bash pnpm prisma:migrate ``` -## Consolidation system +This runs consolidation then `prisma migrate dev`. It creates a new migration file in `src/prisma/migrations/`. -Instead of keeping all models in a single `schema.prisma` file, the project distributes models across `.prisma` files in different modules. A consolidation script (`scripts/consolidate-schema.ts`) automatically merges all these files with the `dbinfo.prisma` header into the main schema. +## Consolidation System -The script: +Instead of maintaining a single monolithic `schema.prisma`, the project distributes models across `*.prisma` files in each module. The consolidation script (`scripts/consolidate-schema.ts`) merges everything: -1. Recursively scans all directories in `src/` for `.prisma` files (except `src/prisma/`) -2. Combines the header and all models into a single `schema.prisma` -3. Adds comments identifying each model's source +1. Recursively scans `src/` for `*.prisma` files (excluding `src/prisma/` itself and `src/generated/`) +2. Extracts models from each file +3. Combines them with the `dbinfo.prisma` header into `src/prisma/schema.prisma` +4. Adds comments identifying each model's source file -### npm scripts +### Commands ```bash -pnpm prisma:consolidate # Consolidate models only +pnpm prisma:consolidate # Consolidate only (no client generation) pnpm prisma:generate # Consolidate + generate Prisma client -pnpm prisma:migrate # Consolidate + create/apply migration +pnpm prisma:migrate # Consolidate + create and apply migration pnpm prisma:studio # Consolidate + open Prisma Studio ``` -## Usage in code +> **Important**: Never edit `src/prisma/schema.prisma` directly. Always edit the module's own `*.prisma` file and run `pnpm prisma:consolidate` (or `pnpm prisma:generate`). + +## Current Models + +### ModuleActivation (`src/core/models/modules.prisma`) + +```prisma +model ModuleActivation { + moduleId String + guildId String + activated Boolean + activatedVersion String @default("") + @@id([moduleId, guildId]) +} +``` + +Tracks which modules are enabled on which guilds and at which version. Used by the version-gating system for command registration. + +### GuildConfiguration (`src/core/models/config.prisma`) + +```prisma +model GuildConfiguration { + guildId String @id + data Json +} +``` + +Stores all module configurations as a single JSON blob per guild. The `data` field maps `moduleId → { key: serializedValue }`. + +## Using Prisma in Code ```typescript import prisma from "#lib/database.js"; -// In a service or command -const users = await prisma.user.findMany(); -const newUser = await prisma.user.create({ - data: { userId: "123", username: "test" }, +// Query examples +const activations = await prisma.moduleActivation.findMany({ + where: { guildId: interaction.guildId }, }); -``` -## Best practices +const config = await prisma.guildConfiguration.findUnique({ + where: { guildId: "123456789" }, +}); -- **Core models**: `src/core/models/` -- **Module-specific models**: `src/modules/[module-name]/models/` -- **File naming**: `descriptive-name.prisma` -- **Model naming**: PascalCase (`ModuleActivation`) -- **Field naming**: camelCase (`moduleId`) -- Keep few models per file for easier maintenance +// Create +await prisma.moduleActivation.create({ + data: { + moduleId: "my-module", + guildId: "123456789", + activated: true, + activatedVersion: "1.0.0", + }, +}); +``` -## Environment variables +The Prisma client is a singleton exported from `#lib/database.js`. It's initialized once at startup. -```ini -DATABASE_URL="postgresql://username:password@localhost:5432/omnibot" -``` +## Best Practices + +- **Core models** go in `src/core/models/` +- **Module-specific models** go in `src/modules//models/` +- Use **PascalCase** for model names and **camelCase** for fields +- Keep models focused — one file per logical entity or small group +- Always run `pnpm prisma:generate` after changing any `.prisma` file +- Run `pnpm prisma:migrate` in development to create and apply migrations -## Useful resources +## Next Steps -- [Prisma Documentation](https://www.prisma.io/docs/) -- [Prisma Schema Reference](https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference) +- [Services](./services) for organizing database operations +- [Configuration](./configuration) for the generic config system diff --git a/docs/site/en/guide/getting-started.md b/docs/site/en/guide/getting-started.md index 5fc3e28..f3b954b 100644 --- a/docs/site/en/guide/getting-started.md +++ b/docs/site/en/guide/getting-started.md @@ -1,53 +1,145 @@ # Getting Started -Welcome to the OmniBot development guide! This documentation explains how to create modules, commands, and listeners to extend the Discord bot's functionality. +Welcome to OmniBot! This guide will help you set up the bot for development and understand the project. -OmniBot is a modular Discord bot where each feature is encapsulated in its own module. Modules are auto-discovered at startup — **no manual registration is needed** outside the module directory. +## What is OmniBot? -## Project structure +OmniBot is a **modular Discord bot** developed for the [Graven - Développement](https://discord.gg/graven) community. Each feature is a self-contained **module** that is auto-discovered at startup and can be installed or uninstalled **per Discord server** via `/modules`. Modules declare their own slash commands, event listeners, interaction handlers, and a typed configuration schema edited live with `/config `. + +The core system wires everything together — adding a feature means creating a module, never touching the bootstrap. + +### Tech Stack + +| Area | Choice | +| --------------- | ------------------------------------------------------------------------------------ | +| Language | TypeScript (ESM, NodeNext), Node.js 24 | +| Discord API | discord.js v14 | +| Database | PostgreSQL 17 via Prisma ORM | +| Package manager | pnpm (workspaces: bot + docs site) | +| Toolchain | mise (tool versions), oxlint + oxfmt (lint/format), lefthook (git hooks), commitlint | +| Tests | Vitest (unit + integration) | +| Logging | pino | +| Dev runner | pitchfork (one-command stack: DB + bot) | + +--- + +## Quick Start + +### Prerequisites + +- [mise](https://mise.jdx.dev/) — pins Node.js, pnpm, and pitchfork versions. Without mise, install **Node.js 24** and **pnpm** manually (check `.mise.toml` for exact versions). +- **Docker** — for the PostgreSQL 17 container. +- A **Discord bot token** — create an application in the [Discord Developer Portal](https://discord.com/developers/applications), then go to the Bot section and reset the token. Enable **Message Content Intent** if your module needs it. + +### Setup + +```bash +# 1. Clone the repository +git clone https://github.com/GravenDev/OmniBot.git +cd OmniBot + +# 2. Install toolchain (Node, pnpm, pitchfork) +mise install + +# 3. Install dependencies and git hooks +pnpm install + +# 4. Create environment file +cp .env.example .env + +# 5. Fill in the .env file (see Environment section below) +``` + +### Run + +**One-command stack (recommended):** + +```bash +pitchfork start bot # starts PostgreSQL, waits for it, then runs the bot +pitchfork logs bot # tail logs +pitchfork stop bot db # stop everything +``` + +Daemons are defined in `pitchfork.toml` — the `bot` daemon depends on `db` (Docker PostgreSQL). + +**Manual setup:** + +```bash +docker compose up -d # start PostgreSQL 17 +pnpm prisma:migrate # apply database migrations (first run only) +pnpm dev # run the bot with tsx +``` + +### Environment + +Copy `.env.example` to `.env` and fill in the values: + +| Variable | Description | +| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | +| `DISCORD_TOKEN` | Bot token from the Discord Developer Portal | +| `DATABASE_URL` | PostgreSQL connection string (default matches `compose.yaml`) | +| `DEV_GUILD_ID` | **Development only** — guild ID where slash commands register instantly (global commands take ~1h to propagate). Required for `pnpm dev`. | + +--- + +## Project Structure ``` src/ -├── modules/ # All bot modules -│ ├── my-module/ # Module -│ │ ├── commands/ # Module commands -│ │ ├── listeners/ # Module listeners -│ │ ├── models/ # Prisma models (optional) -│ │ ├── services/ # Module services -│ │ ├── interactions/ # Interaction handlers (buttons, etc.) -│ │ └── my-module.module.ts # Main module file -├── lib/ # Library exposed to modules -│ ├── module.ts # defineModule() -│ ├── command.ts # declareCommand() -│ ├── listener.ts # declareEventListener() -│ ├── interaction.ts # declareInteractionHandler() -│ ├── service.ts # declareService() -│ ├── config.ts # ConfigType, ConfigProvider -│ ├── database.ts # Prisma client -│ └── logger.ts # Structured logger -├── core/ # Bot core system -│ ├── commands/ # System commands (/modules, /config) -│ ├── loaders/ # Module/command loaders -│ └── services/ # Internal services -└── prisma/ # Prisma configuration - ├── dbinfo.prisma # Generator + datasource config - └── schema.prisma # Consolidated schema (auto-generated) +├── index.ts # Entry point — boot sequence +├── core/ # Framework (always loaded) +│ ├── commands/ # /modules and /config commands +│ ├── config/ # Config type handlers (modal, select, toggle) +│ ├── interactions/ # Core button handlers +│ ├── listeners/ # Global event listeners +│ ├── loaders/ # Module/command/listener discovery +│ ├── models/ # Core Prisma schemas +│ ├── services/ # ModuleService, ConfigService +│ └── utils/ # Permission guard, version parser, messages +├── modules/ # Feature modules (one folder per module) +│ ├── thread-creator/ # Example module: automatic thread creation +│ └── test-config/ # Dev-only: exercises every config type +├── lib/ # Shared contracts exposed to modules +│ ├── module.ts # defineModule() +│ ├── command.ts # declareCommand() +│ ├── listener.ts # declareEventListener() +│ ├── interaction.ts # declareInteractionHandler() +│ ├── config.ts # ConfigType, ConfigProvider, validators +│ ├── service.ts # declareService() +│ ├── database.ts # Prisma client singleton +│ ├── logger.ts # Pino logger +│ └── registry.ts # Registry: collects commands/listeners/handlers +├── prisma/ # Database +│ ├── dbinfo.prisma # Generator + datasource config +│ ├── schema.prisma # Consolidated schema (auto-generated) +│ └── migrations/ # SQL migration files +├── utils/ # Shared utilities +│ └── colors.ts # Embed color constants +└── generated/ # Prisma client (auto-generated) ``` -## Naming conventions +--- + +## Naming Conventions -The project uses suffixes to identify each file's role: +Files use suffixes to identify their role: -- `*.module.ts` — Module definition (e.g. `my-module.module.ts`) -- `*.command.ts` — Slash command (e.g. `test.command.ts`) -- `*.listener.ts` — Event listener (e.g. `message-create.listener.ts`) -- `*.button.ts` / `*.modal.ts` / `*.select.ts` — Interaction handlers -- `*.service.ts` — Business service (e.g. `user.service.ts`) -- `*.prisma` — Database model (e.g. `users.prisma`) +| Suffix | Role | +| --------------- | ---------------------------------------------------------- | +| `*.module.ts` | Module definition (e.g. `thread-creator.module.ts`) | +| `*.command.ts` | Slash command (e.g. `greet.command.ts`) | +| `*.listener.ts` | Event listener (e.g. `message-create.listener.ts`) | +| `*.button.ts` | Button interaction handler | +| `*.modal.ts` | Modal submit handler | +| `*.select.ts` | Select menu handler | +| `*.service.ts` | Business service (e.g. `thread-creation-queue.service.ts`) | +| `*.prisma` | Prisma model definition | -## Imports +--- -Cross-directory imports use **subpath imports** with `#lib/...`: +## Import Conventions + +Cross-directory imports use **subpath imports** with `#` prefix. Always use the `.js` extension (NodeNext resolution): ```typescript import { defineModule } from "#lib/module.js"; @@ -60,14 +152,38 @@ import logger from "#lib/logger.js"; import prisma from "#lib/database.js"; ``` -Same-directory imports stay relative (`./file.js`). The `.js` extension is always required (NodeNext). +Same-directory imports stay relative: + +```typescript +import myCommand from "./commands/my-command.command.js"; +``` + +--- + +## Common Commands + +```bash +pnpm dev # Run the bot with tsx (development) +pnpm build # prisma:generate + tsc → dist/ +pnpm test # Run full test suite (unit + integration) +pnpm test:unit # Run unit tests only +pnpm test:integration # Run integration tests only +pnpm format # oxfmt + oxlint --fix + +# Database +pnpm prisma:generate # Consolidate schemas + generate Prisma client +pnpm prisma:migrate # Create and apply a migration +pnpm prisma:studio # Open Prisma Studio (GUI) + +# Docs site (VitePress) +pnpm --filter omnibot-docs dev # Start docs dev server +pnpm --filter omnibot-docs build # Build static docs site +``` + +--- -## Next steps +## Next Steps -- [Creating a Module](./creating-a-module) — Module structure and lifecycle -- [Commands](./commands) — Creating slash commands -- [Listeners](./listeners) — Reacting to Discord events -- [Interactions](./interactions) — Buttons, select menus, and modals -- [Configuration](./configuration) — Typed configuration schema -- [Services](./services) — Business logic -- [Database](./database) — Prisma ORM +- [Architecture](./architecture) — Understand how the bot works internally +- [Creating a Module](./creating-a-module) — Build your first module +- [Contributing](./contributing) — Learn how to contribute to the project diff --git a/docs/site/en/guide/interactions.md b/docs/site/en/guide/interactions.md index 3b824e3..937672a 100644 --- a/docs/site/en/guide/interactions.md +++ b/docs/site/en/guide/interactions.md @@ -1,15 +1,13 @@ # Interactions -Discord interactions allow your module to react to user actions on UI components: buttons, select menus, and modals. +Discord interactions allow your module to react to user actions on UI components: buttons, select menus (string, user, role, channel), and modals. -## Supported interaction types - -OmniBot supports two types of interactions: +## Supported Types - **Message Component Interactions**: buttons, select menus - **Modal Submit Interactions**: form submissions -## File structure +## File Structure ``` src/modules/my-module/ @@ -17,39 +15,85 @@ src/modules/my-module/ ├── commands/ │ └── my-command.command.ts └── interactions/ - ├── my-button.button.ts - ├── my-menu.select.ts - └── my-modal.modal.ts + ├── confirm.button.ts + ├── role-select.select.ts + └── feedback.modal.ts ``` -## API +## InteractionHandler API ```typescript -interface InteractionHandler { - customId: string; // Unique identifier - requiresAdmin?: boolean; // Admin-only +interface InteractionHandler { + customId: string; // Prefix for custom ID matching + requiresAdmin?: boolean; // Restrict to administrators check: ( - interaction, - config // Type guard - ) => interaction is Interaction; + interaction: CompatibleInteraction, + config: ConfigProvider + ) => interaction is SpecificType; execute: ( - interaction, + interaction: SpecificType, args: string[], - config // Execution function + config: ConfigProvider ) => Promise; } ``` -The `config` parameter is a `ConfigProvider` giving access to the module's configuration (see [Configuration](./configuration)). +| Field | Required | Description | +| --------------- | -------- | ------------------------------------------------------------------------------ | +| `customId` | Yes | Unique prefix — matched against the start of the interaction's `customId` | +| `requiresAdmin` | No | If `true`, only users with `Administrator` permission can use this interaction | +| `check` | Yes | Type guard — narrows the interaction type for `execute` | +| `execute` | Yes | Called when the interaction is triggered | + +The `config` parameter is a `ConfigProvider` giving access to the module's configuration. + +## Custom ID with Arguments + +The system supports passing arguments through the `customId` using `:` as a separator: + +``` +customId:arg1:arg2:arg3 +``` -The `requiresAdmin` flag restricts the interaction to guild administrators. The check is enforced centrally by the system — no additional code needed. +The dispatcher splits the interaction's `customId` on `:`, uses the first segment for handler matching, and passes the remaining segments as the `args` array. -## Creating an interaction +### Example: Button with Args + +```typescript +// Command that creates the button +import { ButtonBuilder, ButtonStyle } from "discord.js"; + +const button = new ButtonBuilder() + .setCustomId(`confirm-action:delete:${userId}`) + .setLabel("Confirm") + .setStyle(ButtonStyle.Danger); +``` + +```typescript +// Interaction handler +import { ButtonInteraction, MessageFlags } from "discord.js"; +import { declareInteractionHandler } from "#lib/interaction.js"; + +export default declareInteractionHandler({ + customId: "confirm-action", + check: (interaction): interaction is ButtonInteraction => + interaction.isButton(), + + async execute(interaction, [action, targetId]) { + await interaction.reply({ + content: `Action "${action}" confirmed for user ${targetId}`, + flags: MessageFlags.Ephemeral, + }); + }, +}); +``` + +## Creating Interactions ### Button ```typescript -// src/modules/my-module/interactions/confirm.button.ts +// src/modules/greeter/interactions/confirm.button.ts import { MessageFlags } from "discord.js"; import { declareInteractionHandler } from "#lib/interaction.js"; @@ -67,10 +111,10 @@ export default declareInteractionHandler({ }); ``` -### Select menu +### Select Menu ```typescript -// src/modules/my-module/interactions/role-select.select.ts +// src/modules/greeter/interactions/role-select.select.ts import { declareInteractionHandler } from "#lib/interaction.js"; @@ -80,10 +124,9 @@ export default declareInteractionHandler({ async execute(interaction, [targetUserId]) { const selectedRoles = interaction.values; - await interaction.reply({ content: `Roles ${selectedRoles.join(", ")} assigned to <@${targetUserId}>`, - ephemeral: true, + flags: MessageFlags.Ephemeral, }); }, }); @@ -92,7 +135,7 @@ export default declareInteractionHandler({ ### Modal ```typescript -// src/modules/my-module/interactions/feedback.modal.ts +// src/modules/greeter/interactions/feedback.modal.ts import { declareInteractionHandler } from "#lib/interaction.js"; @@ -102,40 +145,37 @@ export default declareInteractionHandler({ async execute(interaction, [category]) { const feedback = interaction.fields.getTextInputValue("feedback-input"); - await interaction.reply({ content: "Thank you for your feedback!", - ephemeral: true, + flags: MessageFlags.Ephemeral, }); }, }); ``` -## Custom ID with arguments +## Admin Gating -The system supports passing arguments through the `customId` using `:` as separator: - -``` -customId:arg1:arg2:arg3 -``` - -### Usage in a command +Set `requiresAdmin: true` to restrict an interaction to server administrators: ```typescript -import { ButtonBuilder, ButtonStyle } from "discord.js"; +export default declareInteractionHandler({ + customId: "dangerous-action", + requiresAdmin: true, + check: (interaction) => interaction.isButton(), -const button = new ButtonBuilder() - .setCustomId(`confirm-action:delete:${userId}`) - .setLabel("Confirm") - .setStyle(ButtonStyle.Danger); + async execute(interaction, args) { + // Only admins can reach this point + await interaction.reply({ content: "Action performed!", ephemeral: true }); + }, +}); ``` -Arguments are automatically extracted and passed to the handler as an array (`args`). +The check is enforced **centrally** by the interaction dispatcher — no need for inline permission checks in your handler. -## Registering in the module +## Registration ```typescript -// src/modules/my-module/my-module.module.ts +// src/modules/greeter/greeter.module.ts import confirmButton from "./interactions/confirm.button.js"; import roleSelect from "./interactions/role-select.select.js"; @@ -147,18 +187,32 @@ export default defineModule({ }); ``` -## Automatic activation management +## Activation Management + +Interactions are automatically tied to module activation. When a module is disabled on a guild, its interaction handlers are no-op — the dispatcher skips them. No extra code needed. -The system handles interaction activation/deactivation automatically: +## Config Message Refresh -- Interactions only work when the module is enabled on the guild -- No extra code needed to check module state -- Automatic error messages if the module is disabled -- Interactions marked `requiresAdmin: true` are auto-protected +When editing configuration via ephemeral select menus, the source config message (public) needs to be updated. The system uses `refreshSourceConfigMessage()` which threads the source message ID through `customId` arguments and re-edits the public message after each change. -## Best practices +## Best Practices -### Argument validation +### Response Types + +```typescript +// Instant reply +await interaction.reply({ content: "Done!", ephemeral: true }); + +// Deferred reply (for operations > 3 seconds) +await interaction.deferReply({ ephemeral: true }); +await longOperation(); +await interaction.editReply({ content: "Finished!" }); + +// Update the source message (for message components) +await interaction.update({ content: "Updated", components: [] }); +``` + +### Argument Validation ```typescript async execute(interaction, [userId, action]) { @@ -169,10 +223,11 @@ async execute(interaction, [userId, action]) { }); return; } + // Proceed with validated args } ``` -### Error handling +### Error Handling ```typescript async execute(interaction, [targetId]) { @@ -189,22 +244,7 @@ async execute(interaction, [targetId]) { } ``` -### Response types - -```typescript -// Instant action -await interaction.reply({ content: "Done!", ephemeral: true }); - -// Long running action -await interaction.deferReply({ ephemeral: true }); -await longOperation(); -await interaction.editReply({ content: "Finished!" }); - -// Update message (buttons) -await interaction.update({ content: "Updated", components: [] }); -``` - -## Next steps +## Next Steps - [Configuration](./configuration) for managing module settings - [Commands](./commands) for creating slash commands diff --git a/docs/site/en/guide/listeners.md b/docs/site/en/guide/listeners.md index 7aabe7f..36d5d44 100644 --- a/docs/site/en/guide/listeners.md +++ b/docs/site/en/guide/listeners.md @@ -1,15 +1,11 @@ # Listeners -Event listeners allow your module to react to Discord events. The module system automatically activates/deactivates listeners based on the module's installation state. +Event listeners allow your module to react to Discord events in real time. The module system automatically activates or deactivates listeners based on whether the module is enabled on the relevant guild. -> **Important**: For events that can occur in DMs (direct messages), you must manually check if the module is active, since the system cannot automatically detect module state outside of a guild. - -> **Discord Intents**: Make sure to add the required intents in `defineModule()` and in your Discord Developer Portal configuration. - -## Declaring a listener +## Creating a Listener ```typescript -// src/modules/my-module/listeners/message-create.listener.ts +// src/modules/greeter/listeners/message-create.listener.ts import { declareEventListener } from "#lib/listener.js"; @@ -17,16 +13,28 @@ export default declareEventListener({ eventType: "messageCreate", async execute(message, config) { - // config is a ConfigProvider | undefined - // Your logic here + if (message.author.bot) return; + + await message.reply("Hello!"); }, }); ``` -## Registering in the module +### Listener Interface ```typescript -// src/modules/my-module/my-module.module.ts +interface EventListener { + eventType: keyof ClientEvents; // Discord.js event name + execute: (...args) => Promise; +} +``` + +The `execute` function receives the standard Discord.js event arguments plus a `ConfigProvider | undefined` as the last parameter. The config is available when the event originates from a guild context; it's `undefined` for DM events. + +## Registering in the Module + +```typescript +// src/modules/greeter/greeter.module.ts import messageListener from "./listeners/message-create.listener.js"; export default defineModule({ @@ -36,9 +44,57 @@ export default defineModule({ }); ``` -The system automatically enables or disables the listener based on whether the module is installed on the relevant guild. +## Auto-Activation System + +When you register a listener, the loader (`listener-loader.ts`) wraps it with automatic activation logic: + +1. The wrapper extracts `guildId` from the event arguments +2. It looks up the module's activation state in the database for that guild +3. If the module is **disabled**, the listener returns immediately (no-op) +4. If the module is **enabled**, it fetches the module's config and calls `execute()` with the config as the last argument + +This means you don't need to check module state or fetch config manually in most cases. + +### DM Edge Case + +For events that can occur outside a guild context (e.g., `messageCreate` in DMs), there is no `guildId` to look up. In this case, the wrapper cannot determine activation state, so the listener is executed with `config: undefined`. Your listener should handle this: + +```typescript +async execute(message, config) { + // In DMs, config is undefined — handle gracefully + if (!config) { + if (!message.guild) return; // Ignore DMs + // Or handle DM case separately + } +} +``` + +## Discord Intents + +Your module must declare the required gateway intents in `defineModule()`. These are aggregated at startup and passed to the Discord client. + +```typescript +export default defineModule({ + intents: [ + GatewayIntentBits.Guilds, + GatewayIntentBits.GuildMessages, + GatewayIntentBits.MessageContent, + ], +}); +``` + +Additionally, **privileged intents** (`GuildMembers`, `GuildPresences`, `MessageContent`) must be enabled in the Discord Developer Portal under Bot > Privileged Gateway Intents. + +## Best Practices + +- **Filter early** — check for bots, DMs, or irrelevant channels at the top of `execute()` +- **Use services** for complex event handling logic +- **Be mindful of performance** — high-volume events like `messageCreate` run on every message +- **Handle errors** — uncaught errors in listeners are caught and logged by the loader +- **Don't assume config is available** — handle the `undefined` case for DM events -## Next steps +## Next Steps - [Configuration](./configuration) for managing module settings -- [Database](./database) for persistent data +- [Database](./database) for persistent data storage +- [Services](./services) for organizing business logic diff --git a/docs/site/en/guide/services.md b/docs/site/en/guide/services.md index 3698cf8..c3e7b9a 100644 --- a/docs/site/en/guide/services.md +++ b/docs/site/en/guide/services.md @@ -1,11 +1,11 @@ # Services -Services allow you to encapsulate business logic and reuse it across your module. +Services allow you to encapsulate business logic and reuse it across your module's commands, listeners, and interactions. Unlike modules, services are **not auto-discovered** — you import them directly where needed. -## Creating a service +## Creating a Service ```typescript -// src/modules/my-module/services/user.service.ts +// src/modules/greeter/services/user.service.ts import { declareService, type Service } from "#lib/service.js"; import prisma from "#lib/database.js"; @@ -27,13 +27,11 @@ class UserService implements Service { export default declareService(new UserService()); ``` -## Service interface +The `Service` interface is currently empty, allowing maximum flexibility. `declareService()` tags the instance with `DeclarationType.Service` for consistency with the declared pattern. -`declareService` accepts any class instance. The `Service` interface is currently empty, allowing great flexibility in service structure. +## Using a Service -## Usage - -Services can be imported and used in commands, listeners, or other services: +Services are imported and used in commands, listeners, or other services: ```typescript // In a command @@ -45,9 +43,92 @@ async execute(interaction) { } ``` -## Best practices +## Real Example: Thread Creation Queue + +The `thread-creator` module demonstrates a practical service pattern. Here's how it works: + +### ThreadCreationQueue (`services/thread-creation-queue.ts`) + +A per-guild FIFO queue with rate limiting for thread creation: + +```typescript +class ThreadCreationQueue implements Service { + // Each guild has its own queue + private queues = new Map(); + private pumps = new Map(); + private timestamps = new Map(); + + enqueue(guildId: string, job: QueuedJob): void { + // Add job to guild's queue and start processing + } + + private pump(guildId: string): void { + // Process jobs at max 5 per 10 seconds per guild + // Uses setTimeout to schedule next batch when rate-limited + } +} +``` + +Key characteristics: + +- **Per-guild isolation** — rate limits are tracked separately per guild +- **FIFO ordering** — jobs are processed in arrival order +- **In-memory only** — queue is lost on restart (intentional trade-off) +- **Non-blocking** — errors on individual jobs are logged without stopping the queue + +### ThreadCreatorService (`services/thread-creator.service.ts`) + +Coordinates thread creation: + +```typescript +class ThreadCreatorService implements Service { + async scheduleThreadForMessage( + message: Message, + config: ConfigProvider + ): Promise { + // Check if message channel is in the configured channels + // Generate thread name from template + // Enqueue thread creation + } + + generateThreadName(template: string, message: Message): string { + // Replace variables: {messageAuthor}, {messageContent}, {timestamp} + // Cap at 100 characters (Discord limit) + } +} +``` + +The `ThreadCreatorService` is used by the `messageCreate` listener, which calls `scheduleThreadForMessage()` with the message and the module's config. The service handles channel checking, name generation, and queueing in one call. + +### Config Schema Integration + +```typescript +// thread-creator.config.ts +config: { + channels: { + name: "Channels", + description: "Channels to watch", + type: [ConfigType.CHANNEL], + }, + welcomeMessage: { + name: "Welcome message", + description: "Message posted in new threads", + type: ConfigType.STRING, + defaultValue: "💬 Use this thread to discuss this topic!", + }, +} +``` + +## Best Practices + +- **Single responsibility** — each service should focus on one concern +- **Testable** — export the class so unit tests can import it directly before `declareService`; alternatively keep it in a separate file and import the plain class for testing without Discord API calls +- **No global state** — use class instances with proper encapsulation +- **Import directly** — import the declared instance where needed +- **Use with Prisma** — services are the natural place for database operations + +## Next Steps -- Use services to encapsulate complex business logic -- Keep services focused on a single responsibility -- Use services for database interactions -- Always export an instance declared with `declareService` +- [Database](./database) for persistent data +- [Configuration](./configuration) for managing module settings +- [Commands](./commands) for creating slash commands diff --git a/docs/site/en/legal/privacy.md b/docs/site/en/legal/privacy.md new file mode 100644 index 0000000..0074420 --- /dev/null +++ b/docs/site/en/legal/privacy.md @@ -0,0 +1,69 @@ +# Privacy Policy + +Last updated: June 2026 + +## Data Controller + +OmniBot is an open-source project developed by the Graven - Développement community. The bot is self-hosted by server administrators who deploy it. Each instance operator is the data controller for their instance. + +The source code is available at [github.com/GravenDev/OmniBot](https://github.com/GravenDev/OmniBot) under the GNU GPL v3 license. + +## Data Collected + +OmniBot collects only the data necessary for its operation: + +### Discord Identifiers + +- **User IDs** — to associate configurations, actions, and permissions with users +- **Role IDs** — to store role references in module configurations +- **Channel IDs** — to store channel references in module configurations +- **Category IDs** — to store category references in module configurations +- **Guild (Server) IDs** — to store per-guild configurations and module activation states + +### Configuration Data + +- Module configuration values (strings, numbers, boolean toggles, enum selections) as defined by each module's schema +- Configuration is stored as JSON in the database + +### Module Activation State + +- Which modules are enabled or disabled on each guild +- The version at which each module was last installed + +## Data Storage + +Data is stored in a PostgreSQL database managed by the bot instance operator. The database is not shared with any third party. The operator is responsible for securing their database instance. + +## Data Retention + +Configuration and activation data is retained for as long as the bot is active on a guild. When a module is uninstalled, its configuration data may be retained in the guild's configuration blob (configurable by instance operators). To request data deletion, contact the instance operator. + +## Data Sharing + +OmniBot does not share collected data with third parties. The only external service used is the Discord API, which receives data as part of normal bot operation (sending messages, responding to interactions, registering commands). + +## Open Source + +As an open-source project, the complete source code is publicly available for audit. Users can: + +- Review exactly what data is collected and how it is processed +- Self-host their own instance with full control over their data +- Modify the code to change data handling practices + +## Your Rights + +Depending on your jurisdiction, you may have the right to: + +- Access your personal data +- Request deletion of your personal data +- Restrict or object to processing + +To exercise these rights, contact the operator of the bot instance you interact with. + +## Changes to This Policy + +This policy may be updated to reflect changes in data handling practices. Continued use of the bot after changes constitutes acceptance of the updated policy. + +## Contact + +For questions about this privacy policy, open an issue on the [GitHub repository](https://github.com/GravenDev/OmniBot/issues) or join the [Graven - Développement](https://discord.gg/graven) Discord server. diff --git a/docs/site/en/legal/terms.md b/docs/site/en/legal/terms.md new file mode 100644 index 0000000..185dc5e --- /dev/null +++ b/docs/site/en/legal/terms.md @@ -0,0 +1,74 @@ +# Terms of Service + +Last updated: June 2026 + +## 1. Acceptance + +By inviting OmniBot to your Discord server or using the bot, you agree to these Terms of Service. If you do not agree, do not invite or use the bot. + +## 2. Description of Service + +OmniBot is a modular Discord bot that provides various features through installable modules. The specific features available depend on which modules are enabled on your server. The bot is provided as open-source software under the GNU GPL v3 license. + +## 3. User Obligations + +### 3.1 Compliance with Discord Terms + +You must comply with Discord's [Terms of Service](https://discord.com/terms) and [Community Guidelines](https://discord.com/guidelines) at all times. + +### 3.2 Compliance with Applicable Law + +You must comply with all applicable laws and regulations when using the bot. + +### 3.3 Prohibited Uses + +You may not: + +- Use the bot for any illegal or unauthorized purpose +- Attempt to disrupt, damage, or impair the bot's operation +- Exploit the bot for automated abuse (spam, raids, etc.) +- Use the bot to harass, threaten, or harm others +- Reverse engineer, decompile, or extract the bot's source code beyond what the open-source license permits + +## 4. Availability and Maintenance + +The bot is provided "as is" without warranty of any kind. The maintainers do not guarantee: + +- Uninterrupted or error-free operation +- That all features will work in all Discord server configurations +- That the bot will be compatible with future Discord API changes + +Instance operators are responsible for their own maintenance and updates. + +## 5. Limitation of Liability + +To the maximum extent permitted by applicable law, the maintainers and contributors shall not be liable for any indirect, incidental, special, consequential, or punitive damages arising from the use of or inability to use the bot. + +## 6. Open Source License + +OmniBot is licensed under the GNU GPL v3. You may: + +- Use the bot for any purpose +- Study how the bot works +- Modify the bot +- Share the bot with others + +You must: + +- Disclose your source code when distributing the bot +- Keep the same license +- Document changes made + +See the [LICENSE](https://github.com/GravenDev/OmniBot/blob/master/LICENSE) file for the full terms. + +## 7. Changes to Terms + +These terms may be updated at any time. Changes will be posted on the project's GitHub repository. Continued use of the bot after changes constitutes acceptance of the new terms. + +## 8. Governing Law + +These terms are governed by French law. Any disputes shall be subject to the jurisdiction of the French courts. + +## 9. Contact + +For questions about these terms, open an issue on the [GitHub repository](https://github.com/GravenDev/OmniBot/issues) or join the [Graven - Développement](https://discord.gg/graven) Discord server. diff --git a/docs/site/fr/guide/architecture.md b/docs/site/fr/guide/architecture.md new file mode 100644 index 0000000..f90cf37 --- /dev/null +++ b/docs/site/fr/guide/architecture.md @@ -0,0 +1,146 @@ +# Architecture + +Cette page explique le fonctionnement interne d'OmniBot. Comprendre cela vous aidera à créer de meilleurs modules et à résoudre les problèmes. + +## Séquence de démarrage + +Au démarrage du bot (`src/index.ts`), la séquence suivante est exécutée : + +``` +1. Vérification de la base de données + └─ prisma.$queryRaw`SELECT 1` + └─ sort avec une erreur si la DB est indisponible + +2. Découverte des modules + └─ loadModules("./modules") + └─ parcourt chaque sous-dossier de src/modules/ + └─ importe le fichier *.module.ts + └─ ignore les modules devOnly en production + +3. Agrégation des intentions + └─ collecte les GatewayIntentBits de tous les modules + └─ crée le client Discord avec l'union de toutes les intentions + +4. Gestionnaire ClientReady (asynchrone) + ├─ Pour chaque module : + │ ├─ module.onLoad(client, registry) + │ └─ loadModuleEvents(client, module) + ├─ coreModule.onLoad(client, coreModule.registry) + ├─ syncCommands(client, modules) + │ ├─ Mode dev : PUT groupé sur DEV_GUILD_ID + │ └─ Mode prod : commandes guild versionnées + commandes globales du cœur + └─ loadGlobalEvents(client) + +5. Connexion + └─ client.login(token) + +6. Gestionnaires d'arrêt + └─ SIGTERM / SIGINT → client.destroy() + prisma.$disconnect() +``` + +## Auto-découverte des modules + +Les modules ne sont **pas enregistrés manuellement**. Le chargeur (`src/core/loaders/module-loader.ts`) les découvre automatiquement : + +1. Liste tous les sous-dossiers de `src/modules/` +2. Pour chaque dossier, trouve un fichier `*.module.ts` (ou `*.module.js`) +3. Importe dynamiquement le module +4. Valide qu'il a `type: DeclarationType.Module` +5. Retourne un objet `Module` avec son `Registry` + +Cela signifie qu'ajouter un nouveau module est aussi simple que de créer un dossier avec un fichier `*.module.ts`. Aucun fichier de configuration à éditer, aucun import à ajouter. + +## Registry et motif Declared + +Chaque module possède sa propre instance de **Registry** (`src/lib/registry.ts`) qui collecte trois types d'artefacts : + +- `commands: Declared[]` +- `listeners: Declared[]` +- `interactionHandlers: Declared[]` + +La méthode `register()` utilise une union discriminée — elle vérifie `handler.type` (de l'énumération `DeclarationType`) et pousse dans le tableau approprié. + +Chaque fichier importé dynamiquement (commande, écouteur, interaction) est enveloppé avec une fonction `declare*()` qui l'étiquette avec son `DeclarationType`. Cela permet aux chargeurs d'identifier le type d'artefact sans conventions de nommage ni configuration. + +```typescript +// Le motif Declared +export enum DeclarationType { + Module = "module", + Command = "command", + Listener = "listener", + Interaction = "interaction", + Service = "service", +} +``` + +## Dispatch centralisé des interactions + +Un seul écouteur `interactionCreate` (`src/core/listeners/interaction-create.listener.ts`) gère toutes les interactions utilisateur : + +``` +interactionCreate +├─ isChatInputCommand() +│ ├─ trouver la commande par nom dans tous les modules +│ ├─ vérifier l'état d'activation du module (BDD) +│ ├─ charger la configuration (ConfigProvider) +│ └─ execute(interaction, config) +├─ isAutocomplete() +│ ├─ trouver la commande +│ ├─ vérifier l'activation +│ └─ complete(interaction, config) +├─ isMessageComponent() ou isModalSubmit() +│ ├─ diviser customId sur ":" → préfixe + args +│ ├─ trouver le gestionnaire par préfixe +│ ├─ vérifier l'activation du module +│ ├─ si requiresAdmin → vérifier la permission Administrateur +│ ├─ exécuter la garde de type (check()) +│ └─ execute(interaction, args, config) +``` + +Cette approche centralisée signifie que : + +- Les **permissions** sont vérifiées à un seul endroit (le flag `requiresAdmin` sur `InteractionHandler`) +- L'**activation du module** est vérifiée automatiquement +- L'**injection de configuration** se fait de manière transparente + +## Propagation des commandes + +### Mode développement + +Quand `NODE_ENV=development`, toutes les commandes (cœur + modules activés) sont enregistrées dans un **PUT unique** sur `DEV_GUILD_ID`. C'est **instantané** — aucun délai de propagation. Chaque redémarrage du bot resynchronise toutes les commandes. + +### Mode production + +- **Commandes du cœur** (`/modules`, `/config`) : enregistrées **globalement** (~1 heure de propagation). +- **Commandes des modules** : enregistrées **par serveur**. Pour éviter de ré-enregistrer à chaque démarrage, le système utilise le **versionnage** : il compare la version déclarée du module avec `activatedVersion` stockée en base. Les commandes ne sont ré-enregistrées que sur les serveurs où la version diffère. + +## Aperçu du système de configuration + +Le système de configuration comporte plusieurs couches : + +1. **Déclaration du schéma** — Les modules déclarent des schémas typés dans `defineModule({ config: {...} })` +2. **Stockage** — Toutes les configurations sont stockées dans un blob JSON par serveur dans la table `GuildConfiguration` +3. **Cache** — `ConfigService` maintient un cache en mémoire (`configCache`) pour éviter les lectures base de données à chaque interaction +4. **Désérialisation** — Les IDs d'entités (utilisateur, rôle, salon) sont stockés sous forme de chaînes et résolus en objets Discord à la lecture +5. **Interface admin** — `/config ` affiche un panneau interactif avec des contrôles d'édition par champ + +## Couche service + +Contrairement aux modules, **les services ne sont pas auto-découverts**. Ce sont de simples classes ou objets TypeScript étiquetés avec `declareService()`. Vous les importez directement là où vous en avez besoin : + +```typescript +import fileAttente from "../services/file-attente-creation.js"; +``` + +Cela maintient la couche service simple et sans dépendances. + +## Auto-activation des écouteurs + +Quand un écouteur de module est enregistré, le chargeur l'enveloppe avec une logique qui : + +1. Extrait le `guildId` des arguments de l'événement +2. Recherche l'état d'activation du module dans la base de données +3. Si désactivé → retour silencieux (no-op) +4. Si activé → récupère la configuration et appelle `execute()` avec la config comme dernier argument + +Pour les événements qui peuvent se produire en dehors d'un serveur (ex. messages privés), l'écouteur doit gérer les vérifications d'activation manuellement — il n'y a pas de contexte de serveur pour effectuer la recherche. diff --git a/docs/site/fr/guide/commands.md b/docs/site/fr/guide/commands.md index 0a448e0..f3124bf 100644 --- a/docs/site/fr/guide/commands.md +++ b/docs/site/fr/guide/commands.md @@ -1,66 +1,180 @@ # Commandes -Ce guide explique comment créer des commandes slash Discord dans vos modules. Les commandes ne sont disponibles que si le module est activé sur le serveur. +Ce guide explique comment créer des commandes slash Discord dans vos modules. Les commandes ne sont disponibles que si le module est activé sur le serveur où elles sont utilisées. -## Création d'une commande +## Créer une commande ```typescript -// src/modules/mon-module/commands/test.command.ts +// 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("test") - .setDescription("Une commande de test"), + .setName("bonjour") + .setDescription("Dit bonjour !"), async execute(interaction) { - await interaction.reply("Test !"); + await interaction.reply(`Bonjour, ${interaction.user.username} !`); }, }); ``` -## Interface Command +### Interface Command ```typescript interface Command { - data: SlashCommandBuilder; // Configuration de la commande + data: + | SlashCommandBuilder + | SlashCommandSubcommandBuilder + | SlashCommandSubcommandGroupBuilder + | SlashCommandSubcommandsOnlyBuilder + | SlashCommandOptionsOnlyBuilder; execute: ( - // Fonction d'exécution (obligatoire) - interaction, - config + interaction: ChatInputCommandInteraction, + config: ConfigProvider ) => Promise; complete?: ( - // Autocomplétion (optionnelle) - interaction, - config + interaction: AutocompleteInteraction, + config: ConfigProvider ) => Promise; } ``` -Le paramètre `config` est un `ConfigProvider` qui donne accès à la configuration du module (voir [Configuration](./configuration)). +| Champ | Requis | Description | +| ---------- | ------ | ----------------------------------------------------------------- | +| `data` | Oui | Définition de la commande slash (nom, description, options) | +| `execute` | Oui | Appelé quand un utilisateur exécute la commande | +| `complete` | Non | Appelé quand un utilisateur tape dans une option à autocomplétion | -## Enregistrement dans le module +### Accès à la configuration + +Le paramètre `config` est un `ConfigProvider` qui donne accès à la configuration du module (voir [Configuration](./configuration)). Il est toujours injecté — même pour les modules sans schéma de configuration. + +> [!NOTE] +> Dans les handlers `complete()` (autocomplétion), le `config` injecté provient actuellement du module **Cœur** plutôt que du module de la commande. Les valeurs de configuration spécifiques au module ne sont donc pas disponibles pendant l'autocomplétion — seule la configuration du module Cœur est accessible. + +```typescript +async execute(interaction, config) { + const salon = config.get("canalLog"); + const max = config.get("avertissementsMax"); + // ... +} +``` + +## Options et sous-commandes + +### Options de base + +```typescript +data: new SlashCommandBuilder() + .setName("saluer") + .setDescription("Saluer quelqu'un") + .addUserOption((option) => + option.setName("cible").setDescription("Qui saluer").setRequired(true) + ) + .addStringOption((option) => + option + .setName("message") + .setDescription("Message personnalisé") + .setMaxLength(200) + ); +``` + +### Autocomplétion + +```typescript +export default declareCommand({ + data: new SlashCommandBuilder() + .setName("couleur") + .setDescription("Choisir une couleur") + .addStringOption((option) => + option + .setName("couleur") + .setDescription("Choisissez une couleur") + .setAutocomplete(true) + .setRequired(true) + ), + + async execute(interaction, config) { + const couleur = interaction.options.getString("couleur", true); + await interaction.reply(`Vous avez choisi ${couleur} !`); + }, + + async complete(interaction, config) { + const couleurs = ["rouge", "vert", "bleu", "jaune", "violet"]; + const saisie = interaction.options.getFocused().toLowerCase(); + const filtrees = couleurs.filter((c) => c.startsWith(saisie)); + await interaction.respond(filtrees.map((c) => ({ name: c, value: c }))); + }, +}); +``` + +### Sous-commandes ```typescript -// src/modules/mon-module/mon-module.module.ts -import testCommand from "./commands/test.command.js"; +const builder = new SlashCommandBuilder() + .setName("config") + .setDescription("Commandes de configuration") + .addSubcommand((sub) => + sub.setName("voir").setDescription("Voir la configuration") + ) + .addSubcommand((sub) => + sub + .setName("definir") + .setDescription("Définir une valeur") + .addStringOption((opt) => + opt.setName("cle").setDescription("Clé de config").setRequired(true) + ) + .addStringOption((opt) => + opt + .setName("valeur") + .setDescription("Valeur de config") + .setRequired(true) + ) + ); +``` + +## Enregistrement et propagation + +### Dans le module + +```typescript +// src/modules/salut/salut.module.ts +import bonjourCommand from "./commands/bonjour.command.js"; export default defineModule({ onLoad(_client, registry) { - registry.register(testCommand); + registry.register(bonjourCommand); }, }); ``` -## Enregistrement et propagation +### Développement vs Production -| Commandes | Production | Développement (`NODE_ENV=development`) | -| -------------------------------- | ---------------------------------------------------------------- | ----------------------------------------------------------------------------------- | -| **core** (`/config`, `/modules`) | Globales (propagation ~1 h) | Enregistrées sur `DEV_GUILD_ID` — **instantané** | -| **module** | Par serveur, (ré)installées au changement de `version` du module | Re-synchronisées **à chaque démarrage** sur `DEV_GUILD_ID` pour les modules activés | +| Aspect | Développement (`NODE_ENV=development`) | Production | +| ------------------------- | ------------------------------------------------------------------------------ | -------------------------------------------------------------------- | +| **Commandes du cœur** | Enregistrées sur `DEV_GUILD_ID` (instantané) | Globales (~1h de propagation) | +| **Commandes des modules** | Resynchronisées à chaque démarrage sur `DEV_GUILD_ID` pour les modules activés | Enregistrées par serveur, uniquement en cas de changement de version | +| **Propagation** | Instantanée (PUT unique) | Différée (globale) ou à la demande (par serveur) | -En production, le versionnage évite de re-pousser les commandes d'un module sur toutes les guildes à chaque démarrage. En développement, les commandes sont enregistrées en un seul PUT sur la guild de développement, ce qui est instantané. +Le système de versionnage en production utilise le champ `activatedVersion` dans l'enregistrement `ModuleActivation` en base de données. Les commandes sont ré-enregistrées sur un serveur uniquement lorsque la version déclarée du module diffère de la version stockée. Cela évite des appels API inutiles à chaque redémarrage. > `DEV_GUILD_ID` est **requis** en mode développement. Voir `.env.example`. + +## Commandes du cœur + +OmniBot fournit deux commandes intégrées dans le module **Cœur** (toujours actif, non désinstallable) : + +| Commande | Permission | Description | +| ------------------ | -------------- | -------------------------------------------------------------- | +| `/modules` | Administrateur | Liste tous les modules avec boutons d'activation/désactivation | +| `/config ` | Administrateur | Ouvre le panneau de configuration interactif d'un module | + +## Bonnes pratiques + +- **Utilisez reply, deferReply ou editReply** de manière appropriée — différée pour les opérations de plus de 3 secondes +- **Utilisez les réponses éphémères** pour les réponses propres à un utilisateur (`flags: MessageFlags.Ephemeral`) +- **Gérez les erreurs** — enveloppez les opérations risquées dans try/catch et répondez avec un message convivial +- **Validez les valeurs des options** — utilisez la validation intégrée du builder (min/max length, min/max value, etc.) diff --git a/docs/site/fr/guide/configuration.md b/docs/site/fr/guide/configuration.md index 02b1a29..8c6b081 100644 --- a/docs/site/fr/guide/configuration.md +++ b/docs/site/fr/guide/configuration.md @@ -1,8 +1,8 @@ # Configuration -OmniBot fournit un système de configuration typée pour les modules. Chaque module peut déclarer un schéma de configuration qui est automatiquement exposé via une interface interactive Discord accessible aux administrateurs. +OmniBot fournit un système de configuration typé pour les modules. Chaque module peut déclarer un schéma de configuration qui est automatiquement exposé via une interface Discord interactive, accessible aux administrateurs via `/config `. -## Déclarer un schéma de configuration +## Déclarer un schéma Un module déclare son schéma dans `defineModule()` via la propriété `config` : @@ -19,18 +19,18 @@ export default defineModule({ version: "1.0.0", config: { - logChannel: { + canalLog: { name: "Salon de logs", description: "Salon où poster les logs", type: ConfigType.CHANNEL, }, - maxWarnings: { + avertissementsMax: { name: "Avertissements max", description: "Nombre d'avertissements avant sanction", type: ConfigType.NUMBER, defaultValue: 3, }, - logLevel: { + niveauLog: { name: "Niveau de log", description: "Verbosité des logs", type: ConfigType.ENUM, @@ -45,43 +45,50 @@ export default defineModule({ }); ``` -## Types de champs supportés +## Types de champs -| Type | Saisie | Stockage (JSON) | Lecture | -| ---------- | ------------------------ | ---------------- | ----------------- | -| `STRING` | Modal (texte) | `string` | `string` | -| `NUMBER` | Modal (texte → nombre) | `number` | `number` | -| `BOOLEAN` | Bouton toggle | `boolean` | `boolean` | -| `USER` | Select menu utilisateur | `string` (id) | `User` | -| `ROLE` | Select menu rôle | `string` (id) | `Role` | -| `CHANNEL` | Select menu salon | `string` (id) | `Channel` | -| `CATEGORY` | Select menu catégorie | `string` (id) | `CategoryChannel` | -| `ENUM` | Select menu (choix fixe) | `string` (choix) | Union littérale | +### Types supportés -## Listes +| Type | Saisie | Stockage (JSON) | Lecture (désérialisé) | +| ---------- | ------------------------------- | ---------------- | --------------------- | +| `STRING` | Modale (saisie texte) | `string` | `string` | +| `NUMBER` | Modale (texte → nombre) | `number` | `number` | +| `BOOLEAN` | Bouton toggle | `boolean` | `boolean` | +| `USER` | Menu de sélection utilisateur | `string` (id) | `User` | +| `ROLE` | Menu de sélection rôle | `string` (id) | `Role` | +| `CHANNEL` | Menu de sélection salon | `string` (id) | `Channel` | +| `CATEGORY` | Menu de sélection catégorie | `string` (id) | `CategoryChannel` | +| `ENUM` | Menu de sélection (choix fixes) | `string` (choix) | Union littérale | + +### Listes N'importe quel type peut être déclaré en **liste** via `type: [ConfigType.X]` : ```typescript config: { - channels: { + salons: { name: "Salons surveillés", - description: "Salons à surveiller (un ou plusieurs)", + description: "Salons à surveiller", type: [ConfigType.CHANNEL], // Liste de salons }, - warnings: { - name: "Avertissements", - description: "Liste d'avertissements", + scores: { + name: "Scores", + description: "Liste des meilleurs scores", type: [ConfigType.NUMBER], // Liste de nombres }, } ``` -Le type retourné par `config.get("channels")` est alors `Channel[]`. +L'édition des listes dépend du type d'élément : + +- **Entités** (`USER`, `ROLE`, `CHANNEL`, `CATEGORY`) : menus multi-sélection natifs +- **ENUM** : menu multi-sélection avec les options déclarées +- **Scalaires** (`STRING`, `NUMBER`) : éditeur dédié ajouter/supprimer +- **`BOOLEAN`** : éditeur ajouter/supprimer avec boutons toggle -## Type ENUM (choix fixe) +### Type ENUM (choix fixes) -Le type `ENUM` permet de restreindre un champ à un ensemble fixe de valeurs : +Le type `ENUM` restreint un champ à un ensemble fixe de valeurs : ```typescript config: { @@ -92,62 +99,94 @@ config: { options: ["clair", "sombre", "auto"] as const, defaultValue: "auto", }, - features: { + fonctionnalites: { name: "Fonctionnalités", description: "Fonctionnalités activées", - type: [ConfigType.ENUM], // Liste multi-choix - options: ["welcome", "logs", "automod"] as const, + type: [ConfigType.ENUM], + options: ["accueil", "logs", "automod"] as const, }, } ``` -- `options` est **obligatoire** sur une entrée `ENUM` -- L'utilisation de `as const` donne un typage précis : `config.get("theme")` retourne `"clair" | "sombre" | "auto"` +- `options` est **requis** sur les entrées `ENUM` (imposé par le système de types) +- `as const` permet un typage précis : `config.get("theme")` retourne `"clair" | "sombre" | "auto"` - Sans `as const`, le type retourné est `string` ## Valeurs par défaut -- Un champ **avec** `defaultValue` est toujours présent (`T`) -- Un champ **sans** `defaultValue` peut être `undefined` (`T | undefined`) -- Les entités Discord sont automatiquement désérialisées (du stockage par id vers l'objet Discord) +- Un champ **avec** `defaultValue` est toujours présent — le type est `T` (jamais `undefined`) +- Un champ **sans** `defaultValue` peut être `T | undefined` (jamais défini par l'administrateur) +- Les champs d'entités sans valeur par défaut retournent `undefined` (pas une valeur factice) -## Accès à la configuration +C'est une distinction importante — vérifiez toujours `undefined` pour les champs sans valeur par défaut. -La configuration est accessible via le paramètre `config` dans les commandes, écouteurs et interactions : +## Accéder à la configuration + +Le paramètre `config` est passé automatiquement aux commandes, écouteurs et interactions : ```typescript -// Dans une commande -async execute(interaction, config) { - const channel = config.get("logChannel"); // Channel | undefined - const max = config.get("maxWarnings"); // number (a un defaultValue) - const level = config.get("logLevel"); // "debug" | "info" | "warn" | "error" +async function handler(interaction, config) { + const salon = config.get("canalLog"); // Channel | undefined + const max = config.get("avertissementsMax"); // number (toujours présent) + const niveau = config.get("niveauLog"); // "debug" | "info" | "warn" | "error" + + // Utilisation avec valeur par défaut + const securise = config.get("canalLog") ?? salonParDefaut; } ``` ## Interface administrateur -La commande `/config ` (réservée aux administrateurs) affiche un panneau de configuration interactif avec : +### Commande `/config ` -- La liste de tous les champs configurables, leur type et valeur courante -- Des boutons d'édition adaptés à chaque type (modal, toggle, select menu) -- Une pagination automatique si le module a plus de 10 champs configurables -- Barre de navigation si plusieurs pages +- **Admin uniquement** (permission `Administrateur` requise) +- **Réponse publique** — la configuration est visible par tous (transparence) +- **Autocomplétion** sur le nom du module (cœur + modules activés) -Les modifications sont persistées et prennent effet immédiatement. +Le panneau affiche : -## Bonnes pratiques +- Tous les champs configurables avec leur type, description et valeur courante +- Des boutons d'édition adaptés à chaque type (toggle, modale, menu de sélection) +- Une pagination quand il y a plus de 10 champs (limite des 40 composants Discord) + +### Flux d'édition + +| Type de champ | Type d'éditeur | Persistance | Mise à jour de l'interface | +| ------------------------------------- | ------------------------------------ | --------------- | ----------------------------------- | +| `STRING` | Modale (saisie texte) | À la soumission | Message public mis à jour sur place | +| `NUMBER` | Modale (saisie nombre) | À la soumission | Message public mis à jour sur place | +| `BOOLEAN` | Bouton toggle | Au clic | Message public mis à jour sur place | +| `USER`, `ROLE`, `CHANNEL`, `CATEGORY` | Menu select entité (éphémère) | À la sélection | Message public rafraîchi | +| `ENUM` | Menu select chaîne (éphémère) | À la sélection | Message public rafraîchi | +| Liste de scalaires | Éditeur ajouter/supprimer (éphémère) | À l'action | Message public rafraîchi | -### Nommage des champs +**Éditeurs éphémères** (menus de sélection, éditeurs de listes) : ils rafraîchissent le message de configuration public après chaque modification en utilisant `refreshSourceConfigMessage()`. L'ID du message source est transmis via le `customId` des composants éphémères. -- **Clé** : camelCase (`logChannel`, `maxWarnings`) -- **name** : lisible, affiché dans l'interface (`Salon de logs`) -- **description** : explique le rôle du champ +## Persistance -### Validation +La configuration est stockée sous forme d'un blob JSON par serveur dans la table `GuildConfiguration` : + +```prisma +model GuildConfiguration { + guildId String @id + data Json // Record +} +``` + +- Les IDs d'entités (utilisateur, rôle, salon) sont stockées sous forme de chaînes et **désérialisées** en objets Discord à la lecture +- Un cache en mémoire (`configCache`) évite les lectures base de données à chaque interaction +- Le cache est invalidé à chaque écriture + +## Bonnes pratiques -La validation est automatique selon le type déclaré. Pour `ENUM`, seules les valeurs listées dans `options` sont acceptées. +- **Utilisez le camelCase pour les clés**, un `name` lisible et une `description` claire +- **Fournissez `defaultValue`** pour les champs qui doivent toujours avoir une valeur +- **Gardez les schémas ciblés** — trop de champs rend le panneau d'administration difficile à utiliser +- **Utilisez `as const`** sur les options ENUM pour des unions littérales type-safe +- **Vérifiez `undefined`** pour les champs sans valeur par défaut ## Prochaines étapes - [Services](./services) pour organiser la logique métier -- [Base de données](./database) pour des données persistantes avancées +- [Base de données](./database) pour les données persistantes avancées +- [Commandes](./commands) pour créer des commandes slash diff --git a/docs/site/fr/guide/contributing.md b/docs/site/fr/guide/contributing.md new file mode 100644 index 0000000..630d32c --- /dev/null +++ b/docs/site/fr/guide/contributing.md @@ -0,0 +1,139 @@ +# Contribuer + +Les contributions sont les bienvenues de tous — membres de la communauté Graven - Développement et staff. Ce projet est développé pour et par la communauté Discord [Graven - Développement](https://discord.gg/graven). + +## Pour commencer + +1. Forkez le dépôt sur GitHub +2. Clonez votre fork +3. Configurez l'environnement de développement (voir [Pour commencer](./getting-started)) +4. Créez une branche pour votre travail + +```bash +git checkout -b feat/ma-fonctionnalite +``` + +## Conventional Commits + +Les messages de commit doivent suivre le format [Conventional Commits](https://www.conventionalcommits.org/). Ceci est imposé par **commitlint** via un hook lefthook commit-msg. + +``` +type(scope): description + +feat(thread-creator): ajouter file d'attente de création +fix(config): gérer les valeurs undefined dans le handler number +docs(commands): expliquer le flux d'autocomplétion +chore(deps): mettre à jour pnpm vers v11 +``` + +### Types autorisés + +| Type | Utilisation | +| ---------- | --------------------------------------------------------- | +| `feat` | Une nouvelle fonctionnalité | +| `fix` | Une correction de bug | +| `chore` | Maintenance, dépendances, outillage | +| `docs` | Changements de documentation | +| `refactor` | Restructuration du code sans changement de fonctionnalité | +| `test` | Ajout ou mise à jour de tests | +| `ci` | Changements de configuration CI/CD | + +## Avant d'ouvrir une PR + +Exécutez ces vérifications localement — les mêmes vérifications sont effectuées dans la CI : + +```bash +pnpm build # Doit réussir +pnpm test # Tous les tests doivent passer (unitaire + intégration) +pnpm exec oxlint --deny-warnings # Aucun avertissement de lint +pnpm exec oxfmt --check # Le formatage du code doit être correct +``` + +Pour corriger automatiquement le formatage et les problèmes de lint : + +```bash +pnpm format # oxfmt --write + oxlint --fix --fix-suggestions +``` + +## Workflow de PR + +1. **Branchez depuis `master`** — gardez votre branche à jour +2. **Faites des commits ciblés** — chaque commit doit représenter un changement logique +3. **Gardez les PR petites** — plus facile à reviewer, plus rapide à merger +4. **Ouvrez une PR tôt en draft** — obtenez des retours pendant que vous travaillez +5. **Assurez-vous que la CI est verte** — corrigez les échecs +6. **Demandez une review** — au moins une approbation requise + +### Convention de titre de PR + +Suivez le même format Conventional Commits pour les titres de PR : + +``` +feat(module): ajouter la fonctionnalité de journalisation des messages +fix(core): résoudre la condition de course dans la vérification des permissions +``` + +## Style de code + +### Formatage + +- **oxfmt** gère tout le formatage (remplace Prettier) +- Exécutez `pnpm format` avant de commiter +- Le hook `pre-commit` lefthook exécute oxfmt + oxlint sur les fichiers stagés + +### Linting + +- **oxlint** applique les règles de qualité de code +- Exécutez `pnpm exec oxlint --deny-warnings` pour vérifier +- Certains avertissements peuvent être corrigés automatiquement avec `pnpm format` + +### Imports + +- Entre répertoires : utilisez les subpath imports `#` (`#lib/config.js`, `#core/...`) +- Même répertoire : utilisez les imports relatifs (`./fichier.js`) +- Incluez toujours l'extension `.js` (NodeNext) + +### Nommage + +- Les fichiers utilisent des suffixes de rôle : `*.command.ts`, `*.listener.ts`, `*.button.ts`, `*.service.ts` +- Les IDs de module sont en kebab-case : `thread-creator`, `mon-module` +- Les types et interfaces sont en PascalCase + +## Tests + +Nous utilisons **Vitest** pour les tests. Les tests sont co-localisés avec les fichiers source : + +``` +src/lib/config.ts # Source +src/lib/config.test.ts # Tests +src/core/services/module.service.ts +src/core/services/module.service.test.ts +``` + +### Exécuter les tests + +```bash +pnpm test # Tous les tests +pnpm test:unit # Tests unitaires uniquement +pnpm test:integration # Tests d'intégration uniquement +pnpm test -- --watch # Mode watch +``` + +### Écrire des tests + +- **Tests unitaires** : testent la logique isolée (parseurs, utilitaires, algorithmes) +- **Tests d'intégration** : testent les interactions avec l'API Discord (mockée) et la base de données +- Les tests utilisent `vi.mock()` pour isoler les dépendances +- Consultez les tests existants pour les modèles et conventions + +## Trouver des tâches + +- **[AUDIT.md](https://github.com/GravenDev/OmniBot/blob/master/AUDIT.md)** — suit la dette technique, les problèmes connus et les suivis +- **[GitHub Issues](https://github.com/GravenDev/OmniBot/issues)** — demandes de fonctionnalités et rapports de bugs +- **[Documents de review](https://github.com/GravenDev/OmniBot/tree/master/docs/review)** — résultats de revue de code avec éléments actionnables + +## Besoin d'aide ? + +- Rejoignez le serveur Discord [Graven - Développement](https://discord.gg/graven) +- Ouvrez une [Discussion GitHub](https://github.com/GravenDev/OmniBot/discussions) +- Consultez la [documentation](/) pour les guides et références diff --git a/docs/site/fr/guide/creating-a-module.md b/docs/site/fr/guide/creating-a-module.md index f8b8acf..1c09dd4 100644 --- a/docs/site/fr/guide/creating-a-module.md +++ b/docs/site/fr/guide/creating-a-module.md @@ -1,148 +1,160 @@ # 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 interactions et ses propres données. +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. ## Structure d'un module ``` src/modules/mon-module/ -├── mon-module.module.ts # Définition du module principal -├── commands/ # Commandes du module -│ └── ma-commande.command.ts -├── listeners/ # Écouteurs d'événements -│ └── mon-listener.listener.ts -├── interactions/ # Handlers d'interactions (boutons, etc.) -│ └── mon-bouton.button.ts -├── services/ # Services métier +├── mon-module.module.ts # Définition principale du module +├── 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/ # Modèles Prisma (optionnel) +└── models/ # Schémas Prisma (optionnel) └── mon-modele.prisma ``` -## Création d'un module +## Guide pas à pas : Créer un module « Salut » -### 1. Définition du module principal - -Créez le fichier principal de votre module : +### 1. Créer le dossier et le fichier principal ```typescript -// src/modules/mon-module/mon-module.module.ts +// src/modules/salut/salut.module.ts -import { GatewayIntentBits } from "discord.js"; import { defineModule } from "#lib/module.js"; import logger from "#lib/logger.js"; export default defineModule({ - id: "mon-module", - name: "Mon Module", - description: "Description de mon module personnalisé.", + id: "salut", + name: "Salut", + description: "Accueille les nouveaux membres et dit bonjour.", version: "1.0.0", - author: "Votre Nom", // Optionnel - - // Intents Discord requis par le module - intents: [ - GatewayIntentBits.Guilds, - GatewayIntentBits.GuildMessages, - GatewayIntentBits.MessageContent, - ], + author: "Vous", - // Appelé au démarrage du bot onLoad(_client, registry) { - // Enregistrer les commandes, listeners et interactions - // registry.register(maCommande); - // registry.register(monListener); - // registry.register(monInteraction); - - logger.info("Mon module chargé avec succès"); + logger.info("Module Salut chargé"); }, - // Appelé quand le module est installé sur un serveur onInstall(_client, guild) { - logger.info(`Mon module installé sur ${guild.name}`); + logger.info(`Salut installé sur ${guild.name}`); }, - // Appelé quand le module est désinstallé d'un serveur onUninstall(_client, guild) { - logger.info(`Mon module désinstallé de ${guild.name}`); + logger.info(`Salut désinstallé de ${guild.name}`); }, }); ``` -### 2. Cycles de vie +### 2. Ajouter une commande slash -Un module expose trois hooks : +```typescript +// src/modules/salut/commands/bonjour.command.ts -| Hook | Quand | Usage | -| ------------- | ---------------------------- | -------------------------------------------------- | -| `onLoad` | Au démarrage du bot | Enregistrer commandes, listeners, interactions | -| `onInstall` | Installation sur un serveur | Créer des rôles, initialiser des données | -| `onUninstall` | Désinstallation d'un serveur | Nettoyer des données, supprimer des configurations | +import { SlashCommandBuilder } from "discord.js"; +import { declareCommand } from "#lib/command.js"; -### 3. Interface ModuleDeclaration +export default declareCommand({ + data: new SlashCommandBuilder() + .setName("bonjour") + .setDescription("Dit bonjour !"), -```typescript -interface ModuleDeclaration { - id: string; // Identifiant unique (kebab-case) - name: string; // Nom d'affichage - description: string; // Description - version: string; // Version semver (ex: "1.0.0") - author?: string; // Auteur (optionnel) - intents?: GatewayIntentBits[]; // Intents Discord requis - devOnly?: boolean; // Chargé seulement en mode développement - config?: ConfigSchema; // Schéma de configuration (voir Configuration) - - onLoad: (client, registry) => void; - onInstall: (client, guild, registry) => void; - onUninstall: (client, guild, registry) => void; -} + async execute(interaction) { + await interaction.reply(`Bonjour, ${interaction.user.username} !`); + }, +}); ``` -## Bonnes pratiques +### 3. Enregistrer la commande dans le module -### Conventions de nommage +```typescript +// src/modules/salut/salut.module.ts +import bonjourCommand from "./commands/bonjour.command.js"; + +export default defineModule({ + // ... id, nom, etc. -- **ID du module** : kebab-case (`mon-super-module`) -- **Nom du fichier** : `{id}.module.ts` -- **Dossier** : même nom que l'ID du module + onLoad(_client, registry) { + registry.register(bonjourCommand); + logger.info("Module Salut chargé"); + }, + // ... +}); +``` -### Gestion des intents +### 4. Lancer et tester -Déclarez uniquement les intents nécessaires à votre module. Consultez la [documentation Discord](https://discord.com/developers/docs/events/gateway#list-of-intents) pour savoir quels intents sont requis. +Démarrez le bot (voir [Pour commencer](./getting-started)), installez le module Salut via `/modules`, puis exécutez `/bonjour`. -### Gestion des erreurs +## API ModuleDeclaration ```typescript -onInstall(_client, guild) { - try { - // Logique d'installation - } catch (error) { - logger.error(`Erreur lors de l'installation : ${error.message}`); - } +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 + 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; } ``` -### Organisation des fichiers +### Cycles de vie -- Gardez le fichier principal du module léger -- Séparez la logique complexe dans des services -- Utilisez des sous-dossiers pour organiser commandes, listeners et interactions +| 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 | -## Découverte automatique +### Gestion des intentions -Les modules sont chargés automatiquement depuis `src/modules/` au démarrage. Le chargeur (`module-loader.ts`) : +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. -1. Parcourt chaque sous-dossier de `src/modules/` -2. Cherche un fichier `*.module.ts` (ou `*.module.js`) -3. Importe le module et vérifie qu'il expose un `default` de type `Module` -4. Ignore les modules marqués `devOnly: true` en production +```typescript +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 : + +1. Liste chaque sous-dossier +2. Trouve un fichier `*.module.ts` à l'intérieur +3. L'importe dynamiquement +4. Vérifie que l'export a `type: DeclarationType.Module` +5. 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. + +## Bonnes pratiques -Aucun enregistrement manuel n'est nécessaire. +- **Gardez `onLoad` lé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](./commands) — Ajouter des commandes slash à votre module +- [Commandes](./commands) — Ajouter des commandes slash avec options et autocomplétion - [Écouteurs](./listeners) — Réagir aux événements Discord -- [Interactions](./interactions) — Boutons, modales et menus déroulants -- [Configuration](./configuration) — Déclarer un schéma de configuration +- [Interactions](./interactions) — Boutons, menus de sélection et modales +- [Configuration](./configuration) — Déclarer un schéma de configuration typé - [Services](./services) — Organiser la logique métier -- [Base de données](./database) — Utiliser Prisma avec votre module +- [Base de données](./database) — Persister les données avec Prisma diff --git a/docs/site/fr/guide/database.md b/docs/site/fr/guide/database.md index f2300a8..10579a4 100644 --- a/docs/site/fr/guide/database.md +++ b/docs/site/fr/guide/database.md @@ -1,28 +1,27 @@ # Base de données -OmniBot utilise Prisma comme ORM avec une architecture multi-fichiers qui permet de distribuer les modèles de base de données à travers les différents modules. +OmniBot utilise [Prisma](https://www.prisma.io/) comme ORM avec une architecture de schéma multi-fichiers. Les modèles de base de données sont répartis entre les modules et consolidés au moment de la construction. ## Architecture -### Structure des fichiers Prisma +### Disposition des fichiers ``` src/ ├── prisma/ -│ ├── dbinfo.prisma # Configuration du générateur et datasource -│ └── schema.prisma # Schéma consolidé (généré) +│ ├── dbinfo.prisma # Configuration du générateur + datasource +│ └── schema.prisma # Schéma consolidé (auto-généré, ne pas éditer) ├── core/ │ └── models/ -│ └── modules.prisma # Modèles du système de modules +│ ├── config.prisma # Modèle GuildConfiguration +│ └── modules.prisma # Modèle ModuleActivation └── modules/ - └── [module-name]/ + └── mon-module/ └── models/ - └── *.prisma # Modèles spécifiques au module + └── mon-modele.prisma # Modèles spécifiques au module ``` -### Configuration - -Le fichier `dbinfo.prisma` contient la configuration de base : +### Configuration de base (`dbinfo.prisma`) ```prisma generator client { @@ -38,81 +37,124 @@ datasource db { ## Ajouter un modèle à votre module -1. Créez un fichier `.prisma` dans le dossier `models/` de votre module : +### 1. Créer le fichier `.prisma` ``` src/modules/mon-module/models/mon-modele.prisma ``` -2. Définissez votre modèle : +### 2. Définir votre modèle ```prisma model MonModele { id String @id @default(cuid()) + userId String nom String createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt } ``` -3. Générez le client Prisma : +### 3. Générer le client Prisma ```bash pnpm prisma:generate ``` -4. Créez une migration : +Ceci exécute le script de consolidation puis `prisma generate`. + +### 4. Créer une migration ```bash pnpm prisma:migrate ``` -## Système de consolidation +Ceci exécute la consolidation puis `prisma migrate dev`. Cela crée un nouveau fichier de migration dans `src/prisma/migrations/`. -Au lieu d'avoir tous les modèles dans un seul fichier `schema.prisma`, le projet permet de distribuer les modèles dans différents fichiers `.prisma` à travers les modules. Un script de consolidation (`scripts/consolidate-schema.ts`) rassemble automatiquement tous ces fichiers avec le header `dbinfo.prisma` dans le schéma principal. +## Système de consolidation -Le script : +Au lieu de maintenir un seul fichier `schema.prisma` monolithique, le projet répartit les modèles dans des fichiers `*.prisma` dans chaque module. Le script de consolidation (`scripts/consolidate-schema.ts`) fusionne le tout : -1. Parcourt tous les dossiers dans `src/` pour trouver les fichiers `.prisma` (sauf dans `src/prisma/`) -2. Combine le header et tous les modèles dans un seul `schema.prisma` -3. Ajoute des commentaires pour identifier la source de chaque modèle +1. Parcourt récursivement `src/` pour les fichiers `*.prisma` (sauf `src/prisma/` lui-même et `src/generated/`) +2. Extrait les modèles de chaque fichier +3. Les combine avec l'en-tête `dbinfo.prisma` dans `src/prisma/schema.prisma` +4. Ajoute des commentaires identifiant le fichier source de chaque modèle -### Scripts npm +### Commandes ```bash -pnpm prisma:consolidate # Consolide uniquement les modèles -pnpm prisma:generate # Consolide + génère le client Prisma -pnpm prisma:migrate # Consolide + crée/applique une migration -pnpm prisma:studio # Consolide + ouvre Prisma Studio +pnpm prisma:consolidate # Consolidation uniquement (pas de génération client) +pnpm prisma:generate # Consolidation + génération du client Prisma +pnpm prisma:migrate # Consolidation + création et application de migration +pnpm prisma:studio # Consolidation + ouverture de Prisma Studio +``` + +> **Important** : Ne modifiez jamais `src/prisma/schema.prisma` directement. Éditez toujours le fichier `*.prisma` du module concerné et exécutez `pnpm prisma:consolidate` (ou `pnpm prisma:generate`). + +## Modèles actuels + +### ModuleActivation (`src/core/models/modules.prisma`) + +```prisma +model ModuleActivation { + moduleId String + guildId String + activated Boolean + activatedVersion String @default("") + @@id([moduleId, guildId]) +} +``` + +Suit quels modules sont activés sur quels serveurs et à quelle version. Utilisé par le système de versionnage pour l'enregistrement des commandes. + +### GuildConfiguration (`src/core/models/config.prisma`) + +```prisma +model GuildConfiguration { + guildId String @id + data Json +} ``` -## Utilisation dans le code +Stocke toutes les configurations des modules sous forme d'un blob JSON par serveur. Le champ `data` contient la correspondance `moduleId → { clé: valeurSérialisée }`. + +## Utiliser Prisma dans le code ```typescript import prisma from "#lib/database.js"; -// Dans un service ou une commande -const users = await prisma.user.findMany(); -const newUser = await prisma.user.create({ - data: { userId: "123", username: "test" }, +// Exemples de requêtes +const activations = await prisma.moduleActivation.findMany({ + where: { guildId: interaction.guildId }, }); -``` -## Bonnes pratiques +const config = await prisma.guildConfiguration.findUnique({ + where: { guildId: "123456789" }, +}); + +// Création +await prisma.moduleActivation.create({ + data: { + moduleId: "mon-module", + guildId: "123456789", + activated: true, + activatedVersion: "1.0.0", + }, +}); +``` -- **Modèles du core** : `src/core/models/` -- **Modèles spécifiques aux modules** : `src/modules/[module-name]/models/` -- **Nommage des fichiers** : `nom-descriptif.prisma` -- **Nommage des modèles** : PascalCase (`ModuleActivation`) -- **Nommage des champs** : camelCase (`moduleId`) -- Réduisez le nombre de modèles par fichier pour faciliter la maintenance +Le client Prisma est un singleton exporté depuis `#lib/database.js`. Il est initialisé une fois au démarrage. -## Variables d'environnement +## Bonnes pratiques -```ini -DATABASE_URL="postgresql://username:password@localhost:5432/omnibot" -``` +- **Modèles du cœur** dans `src/core/models/` +- **Modèles spécifiques aux modules** dans `src/modules//models/` +- Utilisez le **PascalCase** pour les noms de modèles et le **camelCase** pour les champs +- Gardez les modèles ciblés — un fichier par entité logique ou petit groupe +- Exécutez toujours `pnpm prisma:generate` après avoir modifié un fichier `.prisma` +- Exécutez `pnpm prisma:migrate` en développement pour créer et appliquer les migrations -## Ressources utiles +## Prochaines étapes -- [Documentation Prisma](https://www.prisma.io/docs/) -- [Prisma Schema Reference](https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference) +- [Services](./services) pour organiser les opérations base de données +- [Configuration](./configuration) pour le système de configuration générique diff --git a/docs/site/fr/guide/getting-started.md b/docs/site/fr/guide/getting-started.md index 21b93b3..6584877 100644 --- a/docs/site/fr/guide/getting-started.md +++ b/docs/site/fr/guide/getting-started.md @@ -1,53 +1,145 @@ # Pour commencer -Bienvenue dans le guide de développement d'OmniBot ! Cette documentation vous explique comment créer des modules, commandes et écouteurs pour étendre les fonctionnalités du bot Discord. +Bienvenue sur OmniBot ! Ce guide vous aidera à configurer le bot pour le développement et à comprendre le projet. -OmniBot est un bot Discord modulaire où chaque fonctionnalité est encapsulée dans son propre module. Les modules sont auto-découverts au démarrage — **aucun enregistrement manuel n'est nécessaire** en dehors du dossier du module. +## Qu'est-ce qu'OmniBot ? -## Architecture du projet +OmniBot est un **bot Discord modulaire** développé pour la communauté [Graven - Développement](https://discord.gg/graven). Chaque fonctionnalité est un **module** autonome qui est auto-découvert au démarrage et peut être installé ou désinstallé **par serveur Discord** via `/modules`. Les modules déclarent leurs propres commandes slash, écouteurs d'événements, gestionnaires d'interactions et un schéma de configuration typé éditable en direct avec `/config `. + +Le système central relie le tout — ajouter une fonctionnalité signifie créer un module, sans jamais toucher au bootstrap. + +### Technologies utilisées + +| Domaine | Choix | +| ----------------------- | ---------------------------------------------------------------------------------------- | +| Langage | TypeScript (ESM, NodeNext), Node.js 24 | +| API Discord | discord.js v14 | +| Base de données | PostgreSQL 17 via Prisma ORM | +| Gestionnaire de paquets | pnpm (workspaces : bot + site de doc) | +| Outillage | mise (versions d'outils), oxlint + oxfmt (lint/format), lefthook (hooks git), commitlint | +| Tests | Vitest (unitaires + intégration) | +| Journalisation | pino | +| Lanceur de Dev | pitchfork (stack complète en une commande : DB + bot) | + +--- + +## Démarrage rapide + +### Prérequis + +- [mise](https://mise.jdx.dev/) — fixe les versions de Node.js, pnpm et pitchfork. Sans mise, installez **Node.js 24** et **pnpm** manuellement (vérifiez `.mise.toml` pour les versions exactes). +- **Docker** — pour le conteneur PostgreSQL 17. +- Un **token de bot Discord** — créez une application dans le [Discord Developer Portal](https://discord.com/developers/applications), puis allez dans la section Bot et réinitialisez le token. Activez **Message Content Intent** si votre module en a besoin. + +### Configuration + +```bash +# 1. Cloner le dépôt +git clone https://github.com/GravenDev/OmniBot.git +cd OmniBot + +# 2. Installer la chaîne d'outils (Node, pnpm, pitchfork) +mise install + +# 3. Installer les dépendances et les hooks git +pnpm install + +# 4. Créer le fichier d'environnement +cp .env.example .env + +# 5. Remplir le fichier .env (voir section Environnement ci-dessous) +``` + +### Lancement + +**Stack complète en une commande (recommandé) :** + +```bash +pitchfork start bot # démarre PostgreSQL, attend qu'il soit prêt, puis lance le bot +pitchfork logs bot # consulter les logs +pitchfork stop bot db # tout arrêter +``` + +Les daemons sont définis dans `pitchfork.toml` — le daemon `bot` dépend de `db` (Docker PostgreSQL). + +**Lancement manuel :** + +```bash +docker compose up -d # démarrer PostgreSQL 17 +pnpm prisma:migrate # appliquer les migrations (première exécution uniquement) +pnpm dev # lancer le bot avec tsx +``` + +### Environnement + +Copiez `.env.example` vers `.env` et remplissez les valeurs : + +| Variable | Description | +| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `DISCORD_TOKEN` | Token du bot depuis le Discord Developer Portal | +| `DATABASE_URL` | Chaîne de connexion PostgreSQL (la valeur par défaut correspond à `compose.yaml`) | +| `DEV_GUILD_ID` | **Développement uniquement** — ID du serveur où les commandes slash sont enregistrées instantanément (les commandes globales mettent ~1h à se propager). Requis pour `pnpm dev`. | + +--- + +## Structure du projet ``` src/ -├── modules/ # Dossier contenant tous les modules du bot -│ ├── mon-module/ # Module -│ │ ├── commands/ # Commandes du module -│ │ ├── listeners/ # Écouteurs du module -│ │ ├── models/ # Modèles Prisma (optionnel) -│ │ ├── services/ # Services du module -│ │ ├── interactions/ # Handlers d'interactions (boutons, etc.) -│ │ └── mon-module.module.ts # Fichier principal du module -├── lib/ # Bibliothèque exposée aux modules -│ ├── module.ts # defineModule() -│ ├── command.ts # declareCommand() -│ ├── listener.ts # declareEventListener() -│ ├── interaction.ts # declareInteractionHandler() -│ ├── service.ts # declareService() -│ ├── config.ts # ConfigType, ConfigProvider -│ ├── database.ts # Client Prisma -│ └── logger.ts # Logger structuré -├── core/ # Système central du bot -│ ├── commands/ # Commandes système (/modules, /config) -│ ├── loaders/ # Chargeurs de modules/commandes -│ └── services/ # Services internes -└── prisma/ # Configuration Prisma - ├── dbinfo.prisma # Configuration générateur + datasource - └── schema.prisma # Schéma consolidé (généré automatiquement) +├── index.ts # Point d'entrée — séquence de démarrage +├── core/ # Framework (toujours chargé) +│ ├── commands/ # Commandes /modules et /config +│ ├── config/ # Gestionnaires de types de config (modale, select, toggle) +│ ├── interactions/ # Gestionnaires de boutons du cœur +│ ├── listeners/ # Écouteurs d'événements globaux +│ ├── loaders/ # Découverte des modules/commandes/écouteurs +│ ├── models/ # Schémas Prisma du cœur +│ ├── services/ # ModuleService, ConfigService +│ └── utils/ # Garde de permission, parseur de version, messages +├── modules/ # Modules fonctionnels (un dossier par module) +│ ├── thread-creator/ # Module exemple : création automatique de fils +│ └── test-config/ # Dev uniquement : teste tous les types de config +├── lib/ # Contrats partagés exposés aux modules +│ ├── module.ts # defineModule() +│ ├── command.ts # declareCommand() +│ ├── listener.ts # declareEventListener() +│ ├── interaction.ts # declareInteractionHandler() +│ ├── config.ts # ConfigType, ConfigProvider, validateurs +│ ├── service.ts # declareService() +│ ├── database.ts # Singleton Prisma client +│ ├── logger.ts # Logger Pino +│ └── registry.ts # Registry : collecte commandes/écouteurs/gestionnaires +├── prisma/ # Base de données +│ ├── dbinfo.prisma # Configuration du générateur + datasource +│ ├── schema.prisma # Schéma consolidé (auto-généré) +│ └── migrations/ # Fichiers de migration SQL +├── utils/ # Utilitaires partagés +│ └── colors.ts # Constantes de couleurs pour les embeds +└── generated/ # Client Prisma (auto-généré) ``` +--- + ## Conventions de nommage -Le projet utilise un système de suffixes pour identifier le rôle de chaque fichier : +Les fichiers utilisent des suffixes pour identifier leur rôle : -- `*.module.ts` — Définition du module (ex: `mon-module.module.ts`) -- `*.command.ts` — Commande slash (ex: `test.command.ts`) -- `*.listener.ts` — Écouteur d'événement (ex: `message-create.listener.ts`) -- `*.button.ts` / `*.modal.ts` / `*.select.ts` — Handlers d'interactions -- `*.service.ts` — Service métier (ex: `user.service.ts`) -- `*.prisma` — Modèle de base de données (ex: `users.prisma`) +| Suffixe | Rôle | +| --------------- | ------------------------------------------------------- | +| `*.module.ts` | Définition du module (ex. `thread-creator.module.ts`) | +| `*.command.ts` | Commande slash (ex. `saluer.command.ts`) | +| `*.listener.ts` | Écouteur d'événement (ex. `message-create.listener.ts`) | +| `*.button.ts` | Gestionnaire d'interaction bouton | +| `*.modal.ts` | Gestionnaire de soumission de modale | +| `*.select.ts` | Gestionnaire de menu de sélection | +| `*.service.ts` | Service métier (ex. `file-attente.service.ts`) | +| `*.prisma` | Définition de modèle Prisma | -## Imports +--- -Les imports entre modules se font via des **subpath imports** avec `#lib/...` : +## Conventions d'import + +Les imports entre répertoires utilisent des **subpath imports** avec le préfixe `#`. L'extension `.js` est toujours présente (résolution NodeNext) : ```typescript import { defineModule } from "#lib/module.js"; @@ -60,14 +152,38 @@ import logger from "#lib/logger.js"; import prisma from "#lib/database.js"; ``` -Les imports dans un même répertoire restent relatifs (`./fichier.js`). L'extension `.js` est toujours présente (NodeNext). +Les imports dans le même répertoire restent relatifs : + +```typescript +import maCommande from "./commands/ma-commande.command.js"; +``` + +--- + +## Commandes courantes + +```bash +pnpm dev # Lancer le bot avec tsx (développement) +pnpm build # prisma:generate + tsc → dist/ +pnpm test # Lancer toute la suite de tests (unitaire + intégration) +pnpm test:unit # Tests unitaires uniquement +pnpm test:integration # Tests d'intégration uniquement +pnpm format # oxfmt + oxlint --fix + +# Base de données +pnpm prisma:generate # Consolider les schémas + générer le client Prisma +pnpm prisma:migrate # Créer et appliquer une migration +pnpm prisma:studio # Ouvrir Prisma Studio (interface graphique) + +# Site de documentation (VitePress) +pnpm --filter omnibot-docs dev # Démarrer le serveur de doc en dev +pnpm --filter omnibot-docs build # Construire le site statique de doc +``` + +--- ## Prochaines étapes -- [Créer un module](./creating-a-module) — Structure et cycle de vie d'un module -- [Commandes](./commands) — Créer des commandes slash -- [Écouteurs](./listeners) — Réagir aux événements Discord -- [Interactions](./interactions) — Boutons, menus déroulants et modales -- [Configuration](./configuration) — Schéma de configuration typé -- [Services](./services) — Logique métier -- [Base de données](./database) — Prisma ORM +- [Architecture](./architecture) — Comprendre le fonctionnement interne du bot +- [Créer un module](./creating-a-module) — Construire votre premier module +- [Contribuer](./contributing) — Apprendre à contribuer au projet diff --git a/docs/site/fr/guide/interactions.md b/docs/site/fr/guide/interactions.md index 1d395aa..52813ef 100644 --- a/docs/site/fr/guide/interactions.md +++ b/docs/site/fr/guide/interactions.md @@ -1,13 +1,11 @@ # Interactions -Les interactions Discord permettent à votre module de réagir aux actions des utilisateurs sur des composants d'interface : boutons, menus déroulants et modales. +Les interactions Discord permettent à votre module de réagir aux actions des utilisateurs sur les composants d'interface : boutons, menus de sélection (chaîne, utilisateur, rôle, salon) et modales. -## Types d'interactions supportées +## Types pris en charge -OmniBot supporte deux types d'interactions : - -- **Message Component Interactions** : boutons, menus déroulants (select menus) -- **Modal Submit Interactions** : soumissions de formulaires modaux +- **Message Component Interactions** : boutons, menus de sélection +- **Modal Submit Interactions** : soumissions de formulaires ## Structure des fichiers @@ -17,45 +15,91 @@ src/modules/mon-module/ ├── commands/ │ └── ma-commande.command.ts └── interactions/ - ├── mon-bouton.button.ts - ├── mon-menu.select.ts - └── ma-modale.modal.ts + ├── confirmer.button.ts + ├── select-role.select.ts + └── retour.modal.ts ``` -## API +## API InteractionHandler ```typescript -interface InteractionHandler { - customId: string; // Identifiant unique - requiresAdmin?: boolean; // Réservé aux admins +interface InteractionHandler { + customId: string; // Préfixe pour la correspondance custom ID + requiresAdmin?: boolean; // Restreindre aux administrateurs check: ( - interaction, - config // Garde de type - ) => interaction is Interaction; + interaction: CompatibleInteraction, + config: ConfigProvider + ) => interaction is TypeSpecifique; execute: ( - interaction, + interaction: TypeSpecifique, args: string[], - config // Fonction d'exécution + config: ConfigProvider ) => Promise; } ``` -Le paramètre `config` est un `ConfigProvider` qui donne accès à la configuration du module (voir [Configuration](./configuration)). +| Champ | Requis | Description | +| --------------- | ------ | -------------------------------------------------------------------------------------------------------- | +| `customId` | Oui | Préfixe unique — comparé au début du `customId` de l'interaction | +| `requiresAdmin` | Non | Si `true`, seuls les utilisateurs avec la permission `Administrateur` peuvent utiliser cette interaction | +| `check` | Oui | Garde de type — affine le type d'interaction pour `execute` | +| `execute` | Oui | Appelé quand l'interaction est déclenchée | + +Le paramètre `config` est un `ConfigProvider` donnant accès à la configuration du module. -Le paramètre `requiresAdmin` permet de réserver l'interaction aux administrateurs du serveur. La vérification est faite centralement par le système — aucun code supplémentaire n'est nécessaire. +## Custom ID avec arguments -## Création d'une interaction +Le système supporte le passage d'arguments via le `customId` en utilisant `:` comme séparateur : + +``` +customId:arg1:arg2:arg3 +``` + +Le dispatcher divise le `customId` de l'interaction sur `:`, utilise le premier segment pour la correspondance du gestionnaire, et passe les segments restants comme tableau `args`. + +### Exemple : Bouton avec arguments + +```typescript +// Commande qui crée le bouton +import { ButtonBuilder, ButtonStyle } from "discord.js"; + +const button = new ButtonBuilder() + .setCustomId(`confirmer-action:supprimer:${userId}`) + .setLabel("Confirmer") + .setStyle(ButtonStyle.Danger); +``` + +```typescript +// Gestionnaire d'interaction +import { ButtonInteraction, MessageFlags } from "discord.js"; +import { declareInteractionHandler } from "#lib/interaction.js"; + +export default declareInteractionHandler({ + customId: "confirmer-action", + check: (interaction): interaction is ButtonInteraction => + interaction.isButton(), + + async execute(interaction, [action, cibleId]) { + await interaction.reply({ + content: `Action "${action}" confirmée pour l'utilisateur ${cibleId}`, + flags: MessageFlags.Ephemeral, + }); + }, +}); +``` + +## Créer des interactions ### Bouton ```typescript -// src/modules/mon-module/interactions/confirm.button.ts +// src/modules/salut/interactions/confirmer.button.ts import { MessageFlags } from "discord.js"; import { declareInteractionHandler } from "#lib/interaction.js"; export default declareInteractionHandler({ - customId: "confirm-action", + customId: "confirmer-action", check: (interaction) => interaction.isButton(), async execute(interaction, [actionId, userId]) { @@ -67,97 +111,107 @@ export default declareInteractionHandler({ }); ``` -### Menu déroulant +### Menu de sélection ```typescript -// src/modules/mon-module/interactions/role-select.select.ts +// src/modules/salut/interactions/select-role.select.ts import { declareInteractionHandler } from "#lib/interaction.js"; export default declareInteractionHandler({ - customId: "role-select", + customId: "select-role", check: (interaction) => interaction.isStringSelectMenu(), - async execute(interaction, [targetUserId]) { - const selectedRoles = interaction.values; - + async execute(interaction, [userIdCible]) { + const rolesSelectionnes = interaction.values; await interaction.reply({ - content: `Rôles ${selectedRoles.join(", ")} attribués à <@${targetUserId}>`, - ephemeral: true, + content: `Rôles ${rolesSelectionnes.join(", ")} attribués à <@${userIdCible}>`, + flags: MessageFlags.Ephemeral, }); }, }); ``` -### Modal +### Modale ```typescript -// src/modules/mon-module/interactions/feedback.modal.ts +// src/modules/salut/interactions/retour.modal.ts import { declareInteractionHandler } from "#lib/interaction.js"; export default declareInteractionHandler({ - customId: "feedback-modal", + customId: "retour-modal", check: (interaction) => interaction.isModalSubmit(), - async execute(interaction, [category]) { - const feedback = interaction.fields.getTextInputValue("feedback-input"); - + async execute(interaction, [categorie]) { + const retour = interaction.fields.getTextInputValue("retour-input"); await interaction.reply({ content: "Merci pour votre retour !", - ephemeral: true, + flags: MessageFlags.Ephemeral, }); }, }); ``` -## Système de Custom ID avec arguments - -Le système permet de passer des arguments via le `customId` en utilisant le séparateur `:` : - -``` -customId:arg1:arg2:arg3 -``` +## Restriction administrateur -### Utilisation dans une commande +Définissez `requiresAdmin: true` pour restreindre une interaction aux administrateurs du serveur : ```typescript -import { ButtonBuilder, ButtonStyle } from "discord.js"; +export default declareInteractionHandler({ + customId: "action-dangereuse", + requiresAdmin: true, + check: (interaction) => interaction.isButton(), -const button = new ButtonBuilder() - .setCustomId(`confirm-action:delete:${userId}`) - .setLabel("Confirmer") - .setStyle(ButtonStyle.Danger); + async execute(interaction, args) { + // Seuls les administrateurs peuvent arriver ici + await interaction.reply({ content: "Action effectuée !", ephemeral: true }); + }, +}); ``` -Les arguments sont automatiquement extraits et passés au handler sous forme de tableau (`args`). +La vérification est effectuée **centralement** par le dispatcher d'interactions — pas besoin de vérifications de permission en ligne dans votre gestionnaire. -## Enregistrement dans le module +## Enregistrement ```typescript -// src/modules/mon-module/mon-module.module.ts -import confirmButton from "./interactions/confirm.button.js"; -import roleSelect from "./interactions/role-select.select.js"; +// src/modules/salut/salut.module.ts +import confirmerBouton from "./interactions/confirmer.button.js"; +import selectRole from "./interactions/select-role.select.js"; export default defineModule({ onLoad(_client, registry) { - registry.register(confirmButton); - registry.register(roleSelect); + registry.register(confirmerBouton); + registry.register(selectRole); }, }); ``` -## Gestion automatique de l'activation +## Gestion de l'activation + +Les interactions sont automatiquement liées à l'activation du module. Quand un module est désactivé sur un serveur, ses gestionnaires d'interactions ne font rien — le dispatcher les ignore. Aucun code supplémentaire nécessaire. -Le système gère automatiquement l'activation et la désactivation des interactions : +## Rafraîchissement du message de configuration -- Les interactions ne fonctionnent que si le module est activé sur le serveur -- Aucun code supplémentaire nécessaire pour vérifier l'état du module -- Messages d'erreur automatiques si le module est désactivé -- Les interactions marquées `requiresAdmin: true` sont protégées automatiquement +Lors de l'édition de la configuration via des menus de sélection éphémères, le message de configuration source (public) doit être mis à jour. Le système utilise `refreshSourceConfigMessage()` qui transmet l'ID du message source dans les arguments `customId` et réédite le message public après chaque modification. ## Bonnes pratiques +### Types de réponse + +```typescript +// Réponse instantanée +await interaction.reply({ content: "Terminé !", ephemeral: true }); + +// Réponse différée (pour les opérations > 3 secondes) +await interaction.deferReply({ ephemeral: true }); +await operationLongue(); +await interaction.editReply({ content: "Fini !" }); + +// Mettre à jour le message source (pour les composants de message) +await interaction.update({ content: "Mis à jour", components: [] }); +``` + ### Validation des arguments ```typescript @@ -169,41 +223,27 @@ async execute(interaction, [userId, action]) { }); return; } + // Continuer avec les arguments validés } ``` ### Gestion des erreurs ```typescript -async execute(interaction, [targetId]) { +async execute(interaction, [cibleId]) { try { - await performRiskyOperation(targetId); - await interaction.reply({ content: "Opération réussie !", ephemeral: true }); + await effectuerOperationRisquee(cibleId); + await interaction.reply({ content: "Succès !", ephemeral: true }); } catch (error) { - logger.error("Erreur:", error); + logger.error("Erreur :", error); await interaction.reply({ - content: "Une erreur s'est produite.", + content: "Une erreur est survenue. Veuillez réessayer.", ephemeral: true, }); } } ``` -### Types de réponses - -```typescript -// Action instantanée -await interaction.reply({ content: "Fait !", ephemeral: true }); - -// Action longue -await interaction.deferReply({ ephemeral: true }); -await longOperation(); -await interaction.editReply({ content: "Terminé !" }); - -// Mise à jour du message (boutons) -await interaction.update({ content: "Mis à jour", components: [] }); -``` - ## Prochaines étapes - [Configuration](./configuration) pour gérer les paramètres du module diff --git a/docs/site/fr/guide/listeners.md b/docs/site/fr/guide/listeners.md index 5d5dfa1..7e9b9c3 100644 --- a/docs/site/fr/guide/listeners.md +++ b/docs/site/fr/guide/listeners.md @@ -1,15 +1,11 @@ # Écouteurs -Les écouteurs d'événements (listeners) permettent à votre module de réagir aux événements Discord. Le système de modules gère automatiquement l'activation/désactivation des écouteurs selon l'état du module. +Les écouteurs d'événements (listeners) permettent à votre module de réagir aux événements Discord en temps réel. Le système de modules active ou désactive automatiquement les écouteurs selon que le module est activé sur le serveur concerné. -> **Important** : Pour les événements pouvant se produire en messages privés, vous devez gérer manuellement la vérification d'activation du module, car le système ne peut pas détecter automatiquement si le module est actif en dehors d'un serveur. - -> **Intents Discord** : Assurez-vous d'ajouter les intents nécessaires dans `defineModule()` et dans votre configuration Discord Developer Portal. - -## Déclaration d'un écouteur +## Créer un écouteur ```typescript -// src/modules/mon-module/listeners/message-create.listener.ts +// src/modules/salut/listeners/message-create.listener.ts import { declareEventListener } from "#lib/listener.js"; @@ -17,16 +13,28 @@ export default declareEventListener({ eventType: "messageCreate", async execute(message, config) { - // config est un ConfigProvider | undefined - // Votre logique ici + if (message.author.bot) return; + + await message.reply("Bonjour !"); }, }); ``` +### Interface EventListener + +```typescript +interface EventListener { + eventType: keyof ClientEvents; // Nom de l'événement Discord.js + execute: (...args) => Promise; +} +``` + +La fonction `execute` reçoit les arguments standards de l'événement Discord.js plus un `ConfigProvider | undefined` comme dernier paramètre. La configuration est disponible quand l'événement provient d'un contexte de serveur ; elle est `undefined` pour les événements en messages privés. + ## Enregistrement dans le module ```typescript -// src/modules/mon-module/mon-module.module.ts +// src/modules/salut/salut.module.ts import messageListener from "./listeners/message-create.listener.js"; export default defineModule({ @@ -36,9 +44,57 @@ export default defineModule({ }); ``` -Le système active ou désactive automatiquement l'écouteur selon que le module est installé sur le serveur concerné. +## Système d'auto-activation + +Quand vous enregistrez un écouteur, le chargeur (`listener-loader.ts`) l'enveloppe avec une logique d'activation automatique : + +1. L'enveloppe extrait le `guildId` des arguments de l'événement +2. Elle recherche l'état d'activation du module dans la base de données pour ce serveur +3. Si le module est **désactivé**, l'écouteur retourne immédiatement (no-op) +4. Si le module est **activé**, elle récupère la configuration du module et appelle `execute()` avec la config comme dernier argument + +Cela signifie que vous n'avez pas besoin de vérifier l'état du module ni de récupérer la configuration manuellement dans la plupart des cas. + +### Cas particulier des messages privés + +Pour les événements qui peuvent se produire en dehors d'un contexte de serveur (ex. `messageCreate` en MP), il n'y a pas de `guildId` à rechercher. Dans ce cas, l'enveloppe ne peut pas déterminer l'état d'activation, donc l'écouteur est exécuté avec `config: undefined`. Votre écouteur doit gérer ce cas : + +```typescript +async execute(message, config) { + // En MP, config est undefined — gérez-le gracieusement + if (!config) { + if (!message.guild) return; // Ignorer les MPs + // Ou gérer le cas MP séparément + } +} +``` + +## Intentions Discord + +Votre module doit déclarer les intentions Gateway requises dans `defineModule()`. Elles sont agrégées au démarrage et transmises au client Discord. + +```typescript +export default defineModule({ + intents: [ + GatewayIntentBits.Guilds, + GatewayIntentBits.GuildMessages, + GatewayIntentBits.MessageContent, + ], +}); +``` + +De plus, les **intentions privilégiées** (`GuildMembers`, `GuildPresences`, `MessageContent`) doivent être activées dans le Discord Developer Portal sous Bot > Privileged Gateway Intents. + +## Bonnes pratiques + +- **Filtrez tôt** — vérifiez les bots, MPs ou salons non pertinents au début de `execute()` +- **Utilisez des services** pour la logique complexe de traitement d'événements +- **Soyez attentif aux performances** — les événements à volume élevé comme `messageCreate` s'exécutent pour chaque message +- **Gérez les erreurs** — les erreurs non capturées dans les écouteurs sont attrapées et journalisées par le chargeur +- **Ne supposez pas que la configuration est disponible** — gérez le cas `undefined` pour les événements en MP ## Prochaines étapes - [Configuration](./configuration) pour gérer les paramètres du module -- [Base de données](./database) pour les données persistantes +- [Base de données](./database) pour le stockage persistant des données +- [Services](./services) pour organiser la logique métier diff --git a/docs/site/fr/guide/services.md b/docs/site/fr/guide/services.md index ed61176..c17119b 100644 --- a/docs/site/fr/guide/services.md +++ b/docs/site/fr/guide/services.md @@ -1,53 +1,134 @@ # Services -Les services permettent d'encapsuler la logique métier et de la réutiliser à travers votre module. +Les services vous permettent d'encapsuler la logique métier et de la réutiliser dans les commandes, écouteurs et interactions de votre module. Contrairement aux modules, les services ne sont **pas auto-découverts** — vous les importez directement là où vous en avez besoin. -## Création d'un service +## Créer un service ```typescript -// src/modules/mon-module/services/user.service.ts +// src/modules/salut/services/utilisateur.service.ts import { declareService, type Service } from "#lib/service.js"; import prisma from "#lib/database.js"; -class UserService implements Service { - async createUser(userId: string, username: string) { +class UtilisateurService implements Service { + async creerUtilisateur(userId: string, nom: string) { return await prisma.user.create({ - data: { userId, username }, + data: { userId, nom }, }); } - async getUser(userId: string) { + async getUtilisateur(userId: string) { return await prisma.user.findUnique({ where: { userId }, }); } } -export default declareService(new UserService()); +export default declareService(new UtilisateurService()); ``` -## Interface Service +L'interface `Service` est actuellement vide, ce qui offre une flexibilité maximale. `declareService()` étiquette l'instance avec `DeclarationType.Service` pour la cohérence avec le motif Declared. -La fonction `declareService` accepte n'importe quelle instance de classe. L'interface `Service` est pour l'instant vide, ce qui permet une grande flexibilité dans la structure de vos services. +## Utiliser un service -## Utilisation - -Les services s'importent et s'utilisent dans vos commandes, écouteurs ou autres services : +Les services sont importés et utilisés dans les commandes, écouteurs ou autres services : ```typescript // Dans une commande -import userService from "../services/user.service.js"; +import utilisateurService from "../services/utilisateur.service.js"; async execute(interaction) { - const user = await userService.getUser(interaction.user.id); + const utilisateur = await utilisateurService.getUtilisateur(interaction.user.id); // ... } ``` +## Exemple concret : File d'attente de création de fils + +Le module `thread-creator` illustre un modèle de service pratique. Voici son fonctionnement : + +### ThreadCreationQueue (`services/thread-creation-queue.ts`) + +Une file d'attente FIFO par serveur avec limitation de débit pour la création de fils : + +```typescript +class ThreadCreationQueue implements Service { + // Chaque serveur a sa propre file + private files = new Map(); + private pompes = new Map(); + private horodatages = new Map(); + + enqueue(guildId: string, job: JobEnAttente): void { + // Ajouter le job à la file du serveur et démarrer le traitement + } + + private pump(guildId: string): void { + // Traiter les jobs à max 5 par 10 secondes par serveur + // Utilise setTimeout pour planifier le prochain lot quand le taux est atteint + } +} +``` + +Caractéristiques clés : + +- **Isolation par serveur** — les limites de débit sont suivies séparément par serveur +- **Ordre FIFO** — les jobs sont traités dans l'ordre d'arrivée +- **En mémoire uniquement** — la file est perdue au redémarrage (compromis intentionnel) +- **Non-bloquant** — les erreurs sur des jobs individuels sont journalisées sans arrêter la file + +### ThreadCreatorService (`services/thread-creator.service.ts`) + +Coordonne la création de fils : + +```typescript +class ThreadCreatorService implements Service { + async planifierCreationFil( + message: Message, + config: ConfigProvider + ): Promise { + // Vérifier si le salon du message est dans les salons configurés + // Générer le nom du fil à partir du template + // Mettre en file d'attente la création du fil + } + + genererNomFil(template: string, message: Message): string { + // Remplacer les variables : {messageAuthor}, {messageContent}, {timestamp} + // Limiter à 100 caractères (limite Discord) + } +} +``` + +Le `ThreadCreatorService` est utilisé par l'écouteur `messageCreate`, qui appelle `planifierCreationFil()` avec le message et la configuration du module. Le service gère la vérification du salon, la génération du nom et la mise en file d'attente en un seul appel. + +### Intégration avec le schéma de configuration + +```typescript +// thread-creator.config.ts +config: { + salons: { + name: "Salons", + description: "Salons à surveiller", + type: [ConfigType.CHANNEL], + }, + messageBienvenue: { + name: "Message de bienvenue", + description: "Message posté dans les nouveaux fils", + type: ConfigType.STRING, + defaultValue: "💬 Utilisez ce fil pour discuter de ce sujet !", + }, +} +``` + ## Bonnes pratiques -- Utilisez les services pour encapsuler la logique métier complexe -- Gardez les services focalisés sur une responsabilité spécifique -- Utilisez les services pour les interactions avec la base de données -- Exportez toujours une instance déclarée avec `declareService` +- **Responsabilité unique** — chaque service doit se concentrer sur une préoccupation +- **Testable** — exportez la classe pour que les tests unitaires puissent l'importer directement avant `declareService` ; ou gardez-la dans un fichier séparé et importez la classe brute pour les tests sans appels API Discord +- **Pas d'état global** — utilisez des instances de classe avec un encapsullement approprié +- **Import direct** — importez l'instance déclarée là où vous en avez besoin +- **Utilisation avec Prisma** — les services sont l'endroit naturel pour les opérations base de données + +## Prochaines étapes + +- [Base de données](./database) pour les données persistantes +- [Configuration](./configuration) pour gérer les paramètres du module +- [Commandes](./commands) pour créer des commandes slash diff --git a/docs/site/fr/legal/privacy.md b/docs/site/fr/legal/privacy.md new file mode 100644 index 0000000..a08d372 --- /dev/null +++ b/docs/site/fr/legal/privacy.md @@ -0,0 +1,69 @@ +# Politique de confidentialité + +Dernière mise à jour : juin 2026 + +## Responsable du traitement + +OmniBot est un projet open-source développé par la communauté Graven - Développement. Le bot est auto-hébergé par les administrateurs de serveur qui le déploient. Chaque opérateur d'instance est le responsable du traitement pour son instance. + +Le code source est disponible sur [github.com/GravenDev/OmniBot](https://github.com/GravenDev/OmniBot) sous licence GNU GPL v3. + +## Données collectées + +OmniBot collecte uniquement les données nécessaires à son fonctionnement : + +### Identifiants Discord + +- **IDs utilisateur** — pour associer configurations, actions et permissions aux utilisateurs +- **IDs de rôles** — pour stocker des références de rôles dans les configurations des modules +- **IDs de salons** — pour stocker des références de salons dans les configurations des modules +- **IDs de catégories** — pour stocker des références de catégories dans les configurations des modules +- **IDs de serveurs (guildes)** — pour stocker les configurations par serveur et les états d'activation des modules + +### Données de configuration + +- Valeurs de configuration des modules (chaînes, nombres, toggles booléens, sélections d'énumérations) telles que définies par le schéma de chaque module +- La configuration est stockée au format JSON dans la base de données + +### État d'activation des modules + +- Quels modules sont activés ou désactivés sur chaque serveur +- La version à laquelle chaque module a été installé pour la dernière fois + +## Stockage des données + +Les données sont stockées dans une base de données PostgreSQL gérée par l'opérateur de l'instance du bot. La base de données n'est partagée avec aucun tiers. L'opérateur est responsable de la sécurisation de son instance de base de données. + +## Conservation des données + +Les données de configuration et d'activation sont conservées tant que le bot est actif sur un serveur. Quand un module est désinstallé, ses données de configuration peuvent être conservées dans le blob de configuration du serveur (configurable par les opérateurs d'instance). Pour demander la suppression des données, contactez l'opérateur de l'instance. + +## Partage des données + +OmniBot ne partage pas les données collectées avec des tiers. Le seul service externe utilisé est l'API Discord, qui reçoit des données dans le cadre du fonctionnement normal du bot (envoi de messages, réponse aux interactions, enregistrement des commandes). + +## Open Source + +En tant que projet open-source, le code source complet est publiquement disponible pour audit. Les utilisateurs peuvent : + +- Voir exactement quelles données sont collectées et comment elles sont traitées +- Auto-héberger leur propre instance avec un contrôle total sur leurs données +- Modifier le code pour changer les pratiques de traitement des données + +## Vos droits + +Selon votre juridiction, vous pouvez avoir le droit de : + +- Accéder à vos données personnelles +- Demander la suppression de vos données personnelles +- Restreindre ou vous opposer au traitement + +Pour exercer ces droits, contactez l'opérateur de l'instance du bot avec laquelle vous interagissez. + +## Modifications de cette politique + +Cette politique peut être mise à jour pour refléter les changements dans les pratiques de traitement des données. L'utilisation continue du bot après les modifications constitue l'acceptation de la politique mise à jour. + +## Contact + +Pour toute question concernant cette politique de confidentialité, ouvrez une issue sur le [dépôt GitHub](https://github.com/GravenDev/OmniBot/issues) ou rejoignez le serveur Discord [Graven - Développement](https://discord.gg/graven). diff --git a/docs/site/fr/legal/terms.md b/docs/site/fr/legal/terms.md new file mode 100644 index 0000000..5920b3d --- /dev/null +++ b/docs/site/fr/legal/terms.md @@ -0,0 +1,74 @@ +# Conditions générales d'utilisation + +Dernière mise à jour : juin 2026 + +## 1. Acceptation + +En invitant OmniBot sur votre serveur Discord ou en utilisant le bot, vous acceptez les présentes Conditions générales d'utilisation. Si vous n'acceptez pas ces conditions, n'invitez pas et n'utilisez pas le bot. + +## 2. Description du service + +OmniBot est un bot Discord modulaire qui fournit diverses fonctionnalités via des modules installables. Les fonctionnalités spécifiques disponibles dépendent des modules activés sur votre serveur. Le bot est fourni en tant que logiciel open-source sous licence GNU GPL v3. + +## 3. Obligations de l'utilisateur + +### 3.1 Conformité avec les conditions de Discord + +Vous devez respecter les [Conditions d'utilisation](https://discord.com/terms) et les [Règles de la communauté](https://discord.com/guidelines) de Discord en tout temps. + +### 3.2 Conformité avec la législation applicable + +Vous devez respecter toutes les lois et réglementations applicables lors de l'utilisation du bot. + +### 3.3 Utilisations interdites + +Vous ne pouvez pas : + +- Utiliser le bot à des fins illégales ou non autorisées +- Tenter de perturber, endommager ou altérer le fonctionnement du bot +- Exploiter le bot pour des abus automatisés (spam, raids, etc.) +- Utiliser le bot pour harceler, menacer ou nuire à autrui +- Effectuer du reverse engineering, décompiler ou extraire le code source du bot au-delà de ce que la licence open-source permet + +## 4. Disponibilité et maintenance + +Le bot est fourni « tel quel », sans garantie d'aucune sorte. Les mainteneurs ne garantissent pas : + +- Un fonctionnement ininterrompu ou sans erreur +- Que toutes les fonctionnalités fonctionneront dans toutes les configurations de serveur Discord +- Que le bot sera compatible avec les futurs changements de l'API Discord + +Les opérateurs d'instance sont responsables de leur propre maintenance et de leurs mises à jour. + +## 5. Limitation de responsabilité + +Dans toute la mesure permise par la loi applicable, les mainteneurs et contributeurs ne peuvent être tenus responsables des dommages indirects, accessoires, spéciaux, consécutifs ou punitifs découlant de l'utilisation ou de l'impossibilité d'utiliser le bot. + +## 6. Licence open-source + +OmniBot est sous licence GNU GPL v3. Vous pouvez : + +- Utiliser le bot à n'importe quelle fin +- Étudier le fonctionnement du bot +- Modifier le bot +- Partager le bot avec d'autres + +Vous devez : + +- Divulguer votre code source lors de la distribution du bot +- Conserver la même licence +- Documenter les modifications effectuées + +Voir le fichier [LICENSE](https://github.com/GravenDev/OmniBot/blob/master/LICENSE) pour les termes complets. + +## 7. Modifications des conditions + +Ces conditions peuvent être mises à jour à tout moment. Les modifications seront publiées sur le dépôt GitHub du projet. L'utilisation continue du bot après les modifications constitue l'acceptation des nouvelles conditions. + +## 8. Droit applicable + +Les présentes conditions sont régies par le droit français. Tout litige sera soumis à la compétence des tribunaux français. + +## 9. Contact + +Pour toute question concernant ces conditions, ouvrez une issue sur le [dépôt GitHub](https://github.com/GravenDev/OmniBot/issues) ou rejoignez le serveur Discord [Graven - Développement](https://discord.gg/graven).