Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ See [`docs/site/`](docs/site/README.md) for the full developer guide:
| Event listeners | [docs/site/en/guide/listeners.md](docs/site/en/guide/listeners.md) |
| Buttons / modals / selects | [docs/site/en/guide/interactions.md](docs/site/en/guide/interactions.md) |
| Services | [docs/site/en/guide/services.md](docs/site/en/guide/services.md) |
| Scheduled tasks | [docs/site/en/guide/tasks.md](docs/site/en/guide/tasks.md) |
| Generating images | [docs/site/en/guide/images.md](docs/site/en/guide/images.md) |
| Prisma / database | [docs/site/en/guide/database.md](docs/site/en/guide/database.md) |
| Functional behavior | [docs/functional.md](docs/functional.md) |

Expand Down
52 changes: 52 additions & 0 deletions docs/functional.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,58 @@ Fixe le score d'un membre, ou le supprime. Fixer un score à 0 revient à le sup

---

## Module RNGdle

Repris du bot DJ4H. Suit les tirages quotidiens des membres sur [rngdle.com](https://www.rngdle.com) : un administrateur lie chaque membre à son pseudo RNGdle, puis le bot télécharge ses tirages et en tire des classements, des profils et des statistiques. Toutes les données sont propres à chaque serveur : un même joueur peut être lié sur plusieurs serveurs sans que ses tirages se mélangent.

### Paliers

Chaque tirage est classé selon le percentile de son score parmi tous les tirages de rngdle.com : TRASH (moins de 1 %), COMMON (moins de 50 %), UNCOMMON (moins de 75 %), RARE (moins de 90 %), EPIC (moins de 95 %), ANOMALY (moins de 99 %) et MYTHIC. La table des percentiles est extraite du site au démarrage du bot puis chaque semaine ; si elle change, tous les tirages sont retéléchargés. Une table extraite qui ne ressemble pas à une table de percentiles est ignorée.

### Synchronisation

Les tirages sont téléchargés à 06:00 et 18:00 UTC, avant le classement quotidien, et au plus toutes les 5 minutes par serveur quand un joueur lance une commande `/rngdle`. Si rngdle.com est lent, la commande n'attend pas plus de 10 secondes et affiche les données déjà enregistrées. Un compte qui a déjà un tirage enregistré pour le jour en cours n'est pas re-téléchargé. Un pseudo qui n'existe plus sur rngdle.com est signalé dans les journaux et ignoré.

### `/rngdle leaderboard`

Image du classement des tirages du jour (UTC) : rang, avatar, pseudo, numéro tiré coloré selon son palier, score et placement en percentile. Limité aux 25 premiers.

### `/rngdle profile [member] [username]`

Image du profil d'un joueur : meilleur et pire tirage avec leur date, nombre de tirages, score moyen, record de badges, score total, rang au classement général du serveur et répartition par palier. Sans option, affiche son propre profil. L'option `username` propose les pseudos liés sur le serveur ; les deux options ne se combinent pas.

### `/rngdle server-stats`

Image des statistiques du serveur : meilleur et pire tirage de tous les temps, nombre de tirages, score moyen, score total et répartition par palier avec les trois joueurs les plus représentés dans chacun.

### `/rngdle leaderboard-all [page]`

Image du classement général (somme des scores), 10 joueurs par page, avec des boutons Précédent et Suivant. Si le membre qui consulte n'est pas sur la page, sa ligne est ajoutée en bas. Cliquer sur les boutons d'un message public ouvre une copie visible de soi seul, qu'on peut ensuite parcourir.

### Classement quotidien

Chaque jour à 01:00 UTC, le bot publie le classement de la veille dans le salon configuré, en mentionnant le ou les meilleurs joueurs.

### `/rngdle-admin`

**Permission requise :** Administrateur. Réponses visibles de l'administrateur seul.

| Sous-commande | Effet |
| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `register <member> <username>` | Lie un membre à un pseudo RNGdle, après avoir vérifié qu'il existe, et importe tous ses tirages. Un pseudo ne peut être lié qu'à un seul membre par serveur. Changer de pseudo supprime les tirages de l'ancien. |
| `delete <member>` | Délie le membre et supprime ses tirages sur ce serveur. |
| `show` | Liste les comptes liés sur le serveur. |
| `refresh [full]` | Télécharge tout de suite les nouveaux tirages du serveur ; avec `full`, retélécharge tout l'historique (au plus une fois tous les quarts d'heure). |
| `clear` | Supprime les tirages enregistrés sur ce serveur ; ils sont retéléchargés à la synchronisation suivante. |

### Configuration — `/config rngdle`

| Champ | Type | Description |
| -------------------- | ----- | ------------------------------------------------------------------------------ |
| `leaderboardChannel` | Salon | Salon du classement quotidien. Tant qu'il n'est pas défini, rien n'est publié. |

---

## Module Thread Creator

Crée automatiquement un fil de discussion sous chaque nouveau message dans un salon configuré. Remplace le bot Needle.
Expand Down
4 changes: 4 additions & 0 deletions docs/site/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ export default defineConfig({
{ text: "Configuration", link: "/fr/guide/configuration" },
{ text: "Localisation", link: "/fr/guide/localisation" },
{ text: "Services", link: "/fr/guide/services" },
{ text: "Tâches planifiées", link: "/fr/guide/tasks" },
{ text: "Images", link: "/fr/guide/images" },
{ text: "Base de données", link: "/fr/guide/database" },
{ text: "Contribuer", link: "/fr/guide/contributing" },
],
Expand Down Expand Up @@ -106,6 +108,8 @@ export default defineConfig({
{ text: "Configuration", link: "/en/guide/configuration" },
{ text: "Localization", link: "/en/guide/localization" },
{ text: "Services", link: "/en/guide/services" },
{ text: "Scheduled Tasks", link: "/en/guide/tasks" },
{ text: "Images", link: "/en/guide/images" },
{ text: "Database", link: "/en/guide/database" },
{ text: "Contributing", link: "/en/guide/contributing" },
],
Expand Down
2 changes: 1 addition & 1 deletion docs/site/en/guide/creating-a-module.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ pnpm new-module my-module "My Module"

The generated module compiles and its test passes unmodified. Fill the `TODO` description, then enable it via `/modules` on your dev guild. No registration step exists: modules are auto-discovered from `src/modules/` at startup.

For tests, use the shared helpers from `#lib/testing.js` (`makeTestConfig`, `fakeGuild`, `fakeMessage`, `initTestI18n`) instead of hand-rolled mocks. Note that `vi.mock("#index.js")` and `vi.mock("#lib/database.js")` must still be declared per test file — Vitest hoists them, so no helper can hide them.
For tests, use the shared helpers from `#lib/testing.js` (`makeTestConfig`, `fakeGuild`, `fakeMessage`, `initTestI18n`) instead of hand-rolled mocks. Note that `vi.mock("#core/runtime.js")` and `vi.mock("#lib/database.js")` must still be declared per test file — Vitest hoists them, so no helper can hide them.

## Module Structure

Expand Down
91 changes: 91 additions & 0 deletions docs/site/en/guide/images.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Generating Images

Leaderboards and stat cards are rendered as PNG images with [`@napi-rs/canvas`](https://github.com/Brooooooklyn/canvas) (prebuilt binaries, nothing to install). Two ready-made renderers cover most needs; lower-level helpers are there for anything else.

| Module | Provides |
| ------------------------ | -------------------------------------------------------------------------------------------------------- |
| `#lib/leaderboard-table` | `renderLeaderboardTable`: ranked table with medals, avatars, names and your own columns |
| `#lib/stat-card` | `renderStatCard`: card with a portrait, a title, an optional rank, a grid of stat boxes and a side panel |
| `#lib/imaging` | Shared palette and font, avatar download, circular images, fitted text, assets |

The Outfit font and the gold, silver and bronze medals ship in `src/lib/assets/` and are loaded for you.

## A Leaderboard

Rows extend `LeaderboardEntry` (`rank`, `name`, `avatar`); the rank, medal, avatar and name columns are drawn for you. Describe the other columns:

```typescript
import { fetchAvatar } from "#lib/imaging.js";
import {
renderLeaderboardTable,
type LeaderboardEntry,
} from "#lib/leaderboard-table.js";

interface Row extends LeaderboardEntry {
points: number;
}

const rows: Row[] = await Promise.all(
players.map(async (player, index) => ({
rank: index + 1,
name: player.user.username,
avatar: await fetchAvatar(player.user),
points: player.points,
}))
);

const png = await renderLeaderboardTable({
headers: { rank: t("header.rank"), player: t("header.player") },
rows,
columns: [
{
header: t("header.points"),
x: 650,
maxWidth: 140,
cell: (row) => ({ text: String(row.points) }),
},
],
});
```

A column can be right-aligned (`align: "right"`, `x` is then its right edge), and each cell can set its own `color`, font `family` or a small `icon` drawn before the text. Pass `caller` to add the viewer's own row under a separator when they are not on the page, and `width` for a table wider than the default 800 px.

## A Stat Card

```typescript
import { renderStatCard } from "#lib/stat-card.js";

const png = await renderStatCard({
image: await fetchAvatar(user),
title: user.username,
subtitle: t("stats.memberSince", { date }),
rank: { position: 3, total: 42 },
rows: [
[
{ title: t("stats.messages"), value: "1 204" },
{ title: t("stats.streak"), value: "12 days", color: [94, 233, 181] },
],
[
{
title: t("stats.joined"),
value: "2024-03-01",
subtext: t("stats.hint"),
},
],
],
});
```

The card is 1000 px wide, landscape, so Discord displays it large enough to read.

- **The header** shows the round `image`, the `title`, an optional `subtitle` and an optional `rank`: a medal for the podium, `#N / total` otherwise, hidden when `position` is 0.
- **Rows** hold one to three boxes, which share the row's width.
- **A box** has a `title` and a `value`, and optionally a `color` (value and outline; `outline: false` keeps the color without the frame), a `subtext` and a small corner `avatar`. Text that is too long shrinks to fit the box.
- **An aside** (`{ width, draw(ctx, x, y, width, height) }`) adds a custom panel on the right of the boxes, as tall as them. `drawPanel`, `drawPanelTitle`, `drawText`, `drawFittedText` and `measureText` from `#lib/stat-card` keep it consistent with the rest of the card; the RNGdle tier breakdown is built this way (`src/modules/rngdle/rendering/cards.ts`).

## Good Practices

- **Translate every text** you pass in (headers, titles, units): renderers draw what they receive.
- **Defer the reply** (`interaction.deferReply()`) before rendering: downloading avatars and drawing takes longer than Discord's 3-second limit allows.
- **Cache the PNG** when the same image is requested often, with `RevisionCache` from `#lib/revision-cache` keyed on the data that drives it.
- **Module-specific assets** (an extra font, icons) go in the module's own `assets/` folder; the Dockerfile copies every `assets/` folder next to the compiled code.
42 changes: 42 additions & 0 deletions docs/site/en/guide/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Scheduled Tasks

A module can run code on a schedule — a nightly report, a periodic sync with an external API — by declaring **tasks**. The core schedules them with [croner](https://github.com/Hexagon/croner) once the Discord client is ready, and stops them on shutdown.

## Declaring a Task

```typescript
// src/modules/my-module/tasks/cleanup.task.ts

import { declareTask } from "#lib/task.js";

export default declareTask({
id: "cleanup",
schedule: "0 3 * * *", // every day at 03:00 UTC
runOnStart: false,
async run(client) {
// client is the ready Discord client
},
});
```

| Field | Description |
| ------------ | ---------------------------------------------------------------------------------- |
| `id` | Identifier, unique within the module. The scheduler names the job `<module>:<id>`. |
| `schedule` | Cron expression, always evaluated in **UTC**. A 6-field expression adds seconds. |
| `runOnStart` | Also run once as soon as the bot starts. Optional, `false` by default. |
| `run` | The work to do. Receives the ready Discord client. |

Register the task in the module's `onLoad`, like commands and listeners:

```typescript
onLoad(_client, registry) {
registry.register(cleanupTask);
},
```

## Behavior

- **Global, not per guild.** A task runs once for the whole bot, whatever the number of guilds. To act on the guilds where the module is enabled, list them with `moduleService.getActivatedGuildIds(module.id)` and load each guild's configuration with `configService.getConfigForModuleIn(module, guildId)`.
- **No overlap.** If a run is still in progress when the next one is due, the next one is skipped.
- **Failures are contained.** An error thrown by `run` is logged with the task name; the task keeps its schedule and the bot keeps running.
- **Every run is logged** with its duration.
2 changes: 1 addition & 1 deletion docs/site/fr/guide/creating-a-module.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ pnpm new-module mon-module "Mon Module"

Le module généré compile et son test passe sans retouche. Remplissez la description `TODO`, puis activez-le via `/modules` sur votre serveur dev. Aucune étape d'enregistrement : les modules sont auto-découverts depuis `src/modules/` au démarrage.

Pour les tests, utilisez les helpers partagés de `#lib/testing.js` (`makeTestConfig`, `fakeGuild`, `fakeMessage`, `initTestI18n`) plutôt que des mocks maison. Notez que `vi.mock("#index.js")` et `vi.mock("#lib/database.js")` doivent toujours être déclarés par fichier de test — Vitest les hisse, aucun helper ne peut les masquer.
Pour les tests, utilisez les helpers partagés de `#lib/testing.js` (`makeTestConfig`, `fakeGuild`, `fakeMessage`, `initTestI18n`) plutôt que des mocks maison. Notez que `vi.mock("#core/runtime.js")` et `vi.mock("#lib/database.js")` doivent toujours être déclarés par fichier de test — Vitest les hisse, aucun helper ne peut les masquer.

```
src/modules/mon-module/
Expand Down
Loading
Loading