/warn qui compte les avertissements a besoin de se souvenir. C’est un store : un fichier de données que le framework garde pour vous, par serveur, typé par ce que vous y mettez.
Le fichier
Créezsrc/moderation/warn.ts :
src/moderation/warn.ts
/warn deux fois sur quelqu’un, arrêtez le bot, relancez-le, utilisez-la encore une fois : trois avertissements. Ouvrez data/warnings.json à la racine de votre projet : ils sont là, écrits pour vous.
Ligne par ligne :
store<Warning[]>()déclare le store, et le type entre<>est ce qu’est chaque valeur : une liste d’avertissements. Votre éditeur sait alors qu’update()vous donne unWarning[] | undefinedet en veut un en retour.export const warningsle nomme. Comme un bouton, un store est nommé par son export : ce nom est le fichier dansdata/. Rien à enregistrer, aucun chemin à écrire.{ expires: '30d' }oublie une valeur trente jours après son écriture. Sans lui, les valeurs sont gardées pour toujours.warnings.update(key, change)lit la valeur àkey, la donne àchange(ouundefinedla première fois : d’oùlist = []), et garde ce quechangerenvoie. Un seul appel, quoi qu’il arrive entre-temps.
options.who.id est toute la clé.
Par serveur, sans le dire
Les bots Discord vivent dans beaucoup de serveurs, et un avertissement dans l’un n’a rien à voir avec les autres. Donc un store est par serveur : quand une fonction tourne dans un serveur (une commande, un bouton, un formulaire, un événement avec unguild), le framework sait lequel, et le store travaille sur les données de ce serveur. La même ligne, warnings.get(member.id), lit deux choses différentes dans deux serveurs.
Quand aucun serveur n’est connu, vous dites lequel. Une tâche tourne pour tout le bot, donc elle passe par in() :
src/moderation/digest.ts
in() dans une tâche, et le bot vous le dit, avec la ligne, au lieu de mélanger les données de tous les serveurs. Et pour quelque chose d’identique partout, un store prend scope: 'global' : il ne demande alors jamais de serveur.
Ce que fait un store
get, set, update, delete, entries, ttl : six appels, chacun renvoyant une promesse, donc écrivez await. Une clé est un texte, ou plusieurs ([channel.id, message.id]), et entries() peut lister par les premiers. Une valeur est du JSON : textes, nombres, booléens, listes, objets simples. Une Date reviendrait en texte, donc le store la refuse et vous dit de garder Date.now() à la place, ce que fait le fichier ci-dessus.
Ajoutez /warnings à côté de /warn, pour lire ce qui a été gardé :
src/moderation/warnings.ts
increment(key) ajoute 1 à un nombre, en partant de 0, et renvoie le nouveau nombre : un compteur en une ligne. getMany([a, b]) lit plusieurs clés d’un coup, setMany([[a, 1], [b, 2]]) garde plusieurs valeurs d’un coup, et deleteAll(prefix) oublie tout ce qui est sous un préfixe. Pourquoi pas update(key, n => n + 1) pour un compteur ? Ça marche aussi : increment() le dit seulement en un mot. Et quand deux personnes le déclenchent au même instant, aucune ne perd le changement de l’autre : update() lit et garde d’un trait, même si le bot tourne sur plusieurs processus.
Un classement
Un store garde des valeurs par clé, sans ordre. Pour savoir qui a écrit le plus de messages, il faudrait lire chaque clé et les trier vous-même, à chaque fois. Un classement (ranking) garde un score par membre, déjà dans l’ordre. Créez src/activity/talkers.ts :
src/activity/talkers.ts
add() part de 0, top(5) donne les cinq premiers avec leur rank, à partir de 1. Comme un store, le classement est par serveur sans le dire, gardé dans data/talkers.json, et nommé par son export. rankOf(id) dit où est un membre ; ranking({ order: 'lowest' }) met le score le plus bas en premier, pour un temps ou un nombre d’essais.
Points d’attention
- Un store est pour ce qui tient en mémoire. Le bot garde les données en mémoire et le fichier en est la copie. Réglages, scores, avertissements, qui a ouvert quoi : oui. Des millions de lignes : une base de données.
data/est à vous. Il est dans.gitignore, comme.env. Sauvegardez-le avec le reste de vos données, et sachez quechapterjs devetchapterjs startlancés dans le même dossier le partagent.- Deux stores ne peuvent pas avoir le même nom. Ils partageraient un fichier : le second est signalé au chargement des fichiers, et mis de côté.
- Enregistrer un fichier garde les données. Un store est lié à son nom : changez le code, les données restent. Supprimez
data/<nom>.jsonpour repartir de zéro. - Sur plusieurs processus, le processus que vous avez lancé garde les données pour les autres : rien à configurer, et rien à penser.
- Redis, quand vous le voulez.
store({ storage: 'redis' })ouranking({ storage: 'redis' }), etREDIS_URLdans.env: les données vont dans votre Redis au lieu dedata/, les appels restent les mêmes. Utile quand plusieurs machines partagent les données, ou quand votre hébergeur n’a pas de disque qui dure. Voir Stockage. - Un store et un classement ne peuvent pas partager un nom, pour la même raison que deux stores.
Récapitulatif
export const name = store<T>({ scope?, expires? }): par serveur par défaut, pour toujours par défaut.- Dans un serveur, la clé est à vous ; ailleurs,
name.in(guild). update()lit, change et garde d’un trait ;increment()compte ;entries()liste ;getMany(),setMany(),deleteAll()travaillent sur plusieurs valeurs ;expiressur le store ou sur une valeur.export const name = ranking({ order? }): un score par membre, dans l’ordre ;add(),top(),rankOf().- Un fichier JSON par store ou classement dans
data/, écrit pour vous, ou Redis avecstorage: 'redis'.
Organiser vos fichiers
Où vont les choses quand le bot grandit.
Guide : Stockage
La même chose, en court.