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.
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