Skip to main content
Everything your bot knows so far, it knows from Discord: who used a command, what a message says. Stop it, start it again, and nothing of what happened is left. A /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

Create src/moderation/warn.ts:
src/moderation/warn.ts
Use /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 that update() gives you a Warning[] | undefined and wants one back.
  • export const warnings names it. Like a button, a store is named by its export: that name is the file in data/. 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 at key, gives it to change (or undefined the first time: hence list = []), and keeps what change returns. One call, whatever happens in between.
Notice what is not there: the server. 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 a guild), 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
Forget 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
Three more calls save you a loop. 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. Create src/activity/talkers.ts:
src/activity/talkers.ts
One file, two declarations: an event that gives a point for each message, and a command that shows the first five. 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 that chapterjs dev and chapterjs start run 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>.json to 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' }) or ranking({ storage: 'redis' }), and REDIS_URL in .env: the data goes to your Redis instead of data/, 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; expires on 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 with storage: 'redis'.

Organising your files

Where things go as the bot grows.

Guide: Storage

The same, in short.