Aller au contenu

03 / 04 En service · démon supervisord

Bot Légendes

Bot Discord de l'alliance Dofus « Faut Y Songer » : collection des Légendes des Songes, giveaways, candidatures d'entrée sur le serveur, organisation des runs. En service sur mon VPS, sous supervisord, déployé par Forge.

Coupe de ce qui survit à un arrêt du bot Le bot tourne comme démon supervisord, lancé au démarrage de la machine et relancé s'il s'arrête. Tout l'état utile est dans une base SQLite. Au démarrage, il replanifie les giveaways et tire ceux dont l'échéance est passée. Au tirage, il lit les réactions via l'API REST de Discord. La porte d'entrée du serveur ne dépend pas de lui : elle est verrouillée par les permissions Discord. événements GET réactions synchrone Discord passerelle (événements) · API REST Démon supervisord (Forge) autostart · autorestart 2 Au démarrage giveawaysreplanifiés ;échéance passéetirée aussitôt 2 Au tirage réactions luesen REST, parpages de 100 3 SQLite · WAL état durableaucun état en RAM 1 Porte d'entrée verrou dans lespermissions ;bot en panne :serveur fermé 4 Coupe · Bot Légendes discord.js · SQLite
Coupe de ce qui survit à un arrêt du bot. Les numéros renvoient aux passages du texte. Trait tireté : système externe ; hachures : contrainte.

Le problème

L'alliance compte quelques dizaines de membres, et le bot porte une part de sa vie quotidienne : qui possède quelles Légendes des Songes (trente ressources à collectionner), les giveaways, l'entrée des nouveaux membres sur le serveur, les inscriptions aux runs. La charge est faible. Ce qui compte, c'est ce qui se passe quand le bot s'arrête : plantage, mise à jour, redémarrage de la machine. Un tirage prévu pendant l'arrêt ne doit pas être perdu, un panneau publié la veille doit encore répondre, et une panne ne doit laisser entrer personne.

Ce qui rendait la solution évidente inutilisable

La façon évidente d'écrire un bot Discord garde l'état en mémoire : un setTimeout par giveaway, des participants comptés au fil des réactions reçues, l'avancement d'une candidature en plusieurs pages dans une variable. Tout cela disparaît au premier redémarrage. Les minuteurs n'existent plus, les réactions posées pendant l'arrêt n'ont jamais été vues, et les boutons des anciens messages ne mènent plus nulle part.

Ici, tout ce qui doit survivre est en base : SQLite via better-sqlite3, en mode WAL, synchrone. Il n'y a pas de serveur de base de données à faire tourner à côté, pas de pool, pas de await dans chaque commande, et le volume s'y prête : le code le dit, quelques dizaines de membres. Les panneaux ne gardent rien en mémoire : l'identifiant de chaque bouton porte de quoi retrouver son contexte en base. Un panneau publié avant un redémarrage répond comme avant.

Ce qui a été choisi, et ce que ça a coûté

En production, le bot tourne sous supervisord, déclaré comme démon dans Forge : node src/index.js, sous l'utilisateur forge. autostart le lance au démarrage de la machine, autorestart le relance s'il s'arrête. À l'arrêt, stopasgroup et killasgroup visent tout le groupe de processus : supervisor envoie le signal d'arrêt, attend 15 secondes (stopwaitsecs), puis tue le groupe entier, sans laisser de processus orphelin. La sortie standard, seul journal du bot, tourne sur trois fichiers de 5 Mo. Le conteneur podman du dépôt ne sert qu'au développement local.

Au démarrage, le bot replanifie tous les giveaways en cours ; un tirage dont l'échéance est passée pendant l'arrêt a lieu aussitôt. Il nettoie les salons vocaux temporaires et la file d'attente, et publie l'info du jour s'il l'a manquée, dans la limite de trois heures de retard. Les réactions posées sur des messages antérieurs au redémarrage sont reçues aussi, grâce aux partials de discord.js.

Les participants d'un giveaway ne sont pas suivis en direct. Au moment du tirage, le bot lit la liste des réactions 🎉 par l'API REST, par pages de 100, puis tire au sort (Fisher-Yates). Le résultat ne dépend d'aucun événement reçu pendant que le bot tournait : une réaction posée pendant une panne compte quand même. Le bouton « J'ai reçu ma légende » ne crédite le gagnant qu'une fois.

Écrit ainsi, le module des giveaways n'avait besoin d'aucun intent Discord. Le bot a depuis activé l'intent des réactions, qui n'est pas privilégié, pour son système d'expérience. Il ne demande toujours aucun intent privilégié par défaut : la liste des membres est une option désactivée d'office, et le contenu des messages n'est jamais lu.

La porte d'entrée n'est pas gardée par le bot. Le verrou est dans les permissions du serveur : @everyone ne voit que le salon des règles. Le bot se contente d'ouvrir, une fois la candidature acceptée par les administrateurs. S'il est en panne, le serveur reste fermé au lieu de s'ouvrir. Le coût est assumé : pendant une panne, un nouveau venu attend. Un refus vaut une exclusion, pas un bannissement, pour que la personne puisse retenter plus tard.

La machine vit en UTC. Pour publier l'info du jour à 11 h, heure de Paris, le bot ne calcule pas de décalage : il demande chaque minute l'heure qu'il est à Paris (Intl.DateTimeFormat, fuseau Europe/Paris). Un décalage calculé à la main se trompe deux fois par an, aux changements d'heure.

Le schéma de la base n'a pas de numéro de version. Au démarrage, le bot lit le schéma réel (PRAGMA table_info) et ajoute les colonnes absentes, avec reprise des données quand il le faut. Le prix : aucun retour arrière automatique. Les sauvegardes passent par VACUUM INTO, qui produit un instantané cohérent même pendant que le WAL est actif. L'archivage des candidatures suit la même logique prudente : la copie d'abord, l'original ensuite. Au pire un doublon, jamais un dossier perdu.

Le bot n'est pas seul à tourner : j'administre aussi des serveurs de jeu, Valheim moddé avec BepInEx et Enshrouded, sur un VPS Hostinger sous Debian 13, via CubeCoders AMP.

Vérifiable

Le dépôt est privé et le serveur Discord de l'alliance est réservé à ses membres : rien n'est consultable de l'extérieur.

Chiffres déclarés, dépôt privé :

Surface
71 commandes slash comptées le 2 octobre 2026 ; trois dépendances : discord.js, better-sqlite3, dotenv