/warn command that counts warnings needs to remember. That is a store: a file of data the framework keeps for you, per server, typed by what you put in it.
The file
Createsrc/moderation/warn.ts:
src/moderation/warn.ts
/warn twice on someone, stop the bot, start it again, use it once more: three warnings. Open data/warnings.json at the root of your project: they are there, written for you.
Line by line:
store<Warning[]>()declares the store, and the type between<>is what each value is: a list of warnings. Your editor then knows thatupdate()gives you aWarning[] | undefinedand wants one back.export const warningsnames it. Like a button, a store is named by its export: that name is the file indata/. Nothing to register, no path to write.{ expires: '30d' }forgets a value thirty days after it was written. Without it, values are kept forever.warnings.update(key, change)reads the value atkey, gives it tochange(orundefinedthe first time: hencelist = []), and keeps whatchangereturns. One call, whatever happens in between.
options.who.id is the whole key.
Per server, without saying so
Discord bots live in many servers, and a warning in one of them has nothing to do with the others. So a store is per server: when a function runs in a server (a command, a button, a form, an event with aguild), the framework knows which one, and the store works on the data of that server. The same line, warnings.get(member.id), reads two different things in two servers.
When no server is known, you say which one. A task runs for the whole bot, so it goes through in():
src/moderation/digest.ts
in() in a task, and the bot tells you, with the line, instead of mixing the data of every server. And for something that is the same everywhere, a store takes scope: 'global': it then never asks for a server.
What a store does
get, set, update, delete, entries, ttl: six calls, every one of them returning a promise, so write await. A key is a text, or several ([channel.id, message.id]), and entries() can list by the first ones. A value is JSON: texts, numbers, booleans, lists, plain objects. A Date would come back as a text, so the store refuses it and tells you to keep Date.now() instead, which is what the file above does.
Add /warnings next to /warn, to read what was kept:
src/moderation/warnings.ts
increment(key) adds 1 to a number, starting from 0, and gives the new number back: a counter in one line. getMany([a, b]) reads several keys at once, setMany([[a, 1], [b, 2]]) keeps several values at once, and deleteAll(prefix) forgets everything under a prefix. Why not update(key, n => n + 1) for a counter? It works too: increment() only says it in one word. And when two people trigger it at the same instant, neither loses the other’s change: update() reads and keeps in one go, even if the bot runs on several processes.
A ranking
A store keeps values by key, in no order. To know who wrote the most messages, you would read every key and sort them yourself, each time. A ranking keeps a score per member, already in order. Createsrc/activity/talkers.ts:
src/activity/talkers.ts
add() starts from 0, top(5) gives the first five with their rank, from 1. Like a store, the ranking is per server without saying so, kept in data/talkers.json, and named by its export. rankOf(id) says where one member is; ranking({ order: 'lowest' }) puts the lowest score first, for a time or a number of tries.
Points of attention
- A store is for what fits in memory. The bot keeps the data in memory and the file is its copy. Settings, scores, warnings, who opened what: yes. Millions of rows: a database.
data/is yours. It is in.gitignore, like.env. Back it up with the rest of your data, and know thatchapterjs devandchapterjs startrun in the same folder share it.- Two stores can’t have one name. They would share a file: the second one is reported when the files load, and left out.
- Saving a file keeps the data. A store is tied to its name: change the code, the data stays. Delete
data/<name>.jsonto start empty. - On several processes, the process you started keeps the data for the others: nothing to set up, and nothing to think about.
- Redis, when you want it.
store({ storage: 'redis' })orranking({ storage: 'redis' }), andREDIS_URLin.env: the data goes to your Redis instead ofdata/, the calls stay the same. Useful when several machines share the data, or when your host has no disk that lasts. See Storage. - A store and a ranking can’t share a name, for the same reason as two stores.
Recap
export const name = store<T>({ scope?, expires? }): per server by default, forever by default.- In a server, the key is yours; elsewhere,
name.in(guild). update()is read, change and keep in one go;increment()counts;entries()lists;getMany(),setMany(),deleteAll()work on several values;expireson the store or on one value.export const name = ranking({ order? }): a score per member, in order;add(),top(),rankOf().- One JSON file per store or ranking in
data/, written for you, or Redis withstorage: 'redis'.
Organising your files
Where things go as the bot grows.
Guide: Storage
The same, in short.