# Discord

> Chat both ways, accounts verified from a code typed in game, roles and bans held in step, and the console mirrored where you can read it.

Source: https://stygion.eu/docs/keystone/discord
Updated: 2026-10-10

Everything here is `discord.toml`, or the Settings screen in the panel.

## Getting it talking

1. Make a bot at `discord.com/developers`, and give it the **Message Content**
   and **Server Members** intents — without the first one the bot sees empty
   messages and nothing comes back to the game.
2. `/keystone set discord.token <token>` and
   `/keystone set discord.server <server id>`.
3. `/keystone set discord.chat-channel <channel id>` — the room the game talks
   to.
4. Invite it with permission to read, send and manage webhooks in that channel.

The token is a secret and Keystone masks the command that sets it out of logs
and consoles. It is still written into `discord.toml`, so that file is not
something to publish.

## Chat

| Setting | What it does |
|---|---|
| `game-to-discord` | What is said in game appears on Discord. |
| `discord-to-game` | And the other way. |
| `speak-as-players` | Each player speaks under their own name and face, through a webhook, rather than everything arriving as the bot. |
| `avatar` | Where a face comes from. Defaults to a public head renderer, keyed by name. |
| `joins-and-leaves` | Arrivals and departures too. |
| `topic` / `topic-every` | The channel topic carries who is on, refreshed on a timer. |
| `queue` | How many messages to hold when Discord is unreachable, so an outage does not lose the conversation. |

**With [chat channels](/docs/keystone/chat-channels) on** (0.2.1), each game channel may have its own Discord room (`discord=<channel id>`, or `discord=main` for `chat-channel`): a line goes only to its channel's room, and a message from a room only into its channel, to the players who may read it. A channel without a room never leaves the game. Invite the bot to every room you name, with permission to read, send and manage webhooks.

## Linking an account

In game:

```
/discord link      # hands you a code
/discord status    # whether you are linked, and to whom
/discord unlink    # undo it
```

Then **`/link <code>`** on Discord — as a slash command, so the code never
becomes a message anybody has to delete, and the reply is visible only to them.
No website, no email.

**Or a button, in a room nobody can write in (0.2.1).** `/discord panel
<channel-id>` (`keystone.discord.panel`, staff) posts a panel into a Discord
channel with a **Link account** button. The button opens a form that only the
person who pressed it sees; the code typed into it is spent exactly like
`/link` — the same checks, the same answer, the same limit of accounts. Make
that channel read-only for everybody: Discord takes slash commands away with
Send Messages, and a code pasted as a plain message can be read by anybody until
somebody deletes it. The button's id is fixed (`keystone:link`), so a panel
posted by other tooling under the same bot works too. `/link` stays for rooms
people can write in.

- `link.required-to-join` turns it into a gate: an unlinked player is shown the
  `screens.not-linked` message with their code, their invite link and how long
  the code lasts, and is not let in until they have done it.
- `link.accounts-per-user` is how many game accounts one Discord user may hold.
- `link.rename-nickname` renames them on Discord to the name they play under.
- `link.code-lasts` is how long a code is worth typing.
- `link.invite` is the invite that screen shows.

**And what it is worth outside the game:** on a server that is not in online
mode, a link is the only proof of identity there is — it was made from inside
the game under that name and confirmed by an account the game does not hand out.
So it can be asked about over the API: `GET /v1/discord/links?discord=<id>` with
the `players.read` scope says which game accounts that person holds. That is how
a shop learns who to deliver a purchase to, rather than believing a name typed
into a form.

## Roles and bans, held in step

- `roles.follow` pairs a Discord role with a Keystone group, in both directions:
  give somebody the role and they get the group.
- `ban.game-to-discord` and `ban.discord-to-game` carry a ban across. Turned off
  by default, because it is a decision, not a convenience — and `ban.who-may`
  says whose bans on Discord are allowed to reach the game. See
  [moderation](/docs/keystone/moderation).

## The console, where you can read it

`console.channel` mirrors the server's log into a Discord room, batched every
`console.every` so a busy server is readable rather than a firehose.
`console.levels` picks what is worth mirroring and `console.ignoring` drops the
lines you are tired of. `console.most-lines` caps a single batch.

**Anybody who can read that room can read your server's log.** Put it somewhere
only your team can see.

## Alerts

`alerts.channel` is where Keystone says something is wrong — TPS under
`operations.alerts.tps-below`, memory over `operations.alerts.memory-above-percent`,
sustained for `must-last` and then quiet for `quiet-for` so one bad minute does
not become forty messages. `alerts.ping-role` is who gets woken up.
`alerts.runs-channel` is where finished load tests land.

**The server's state.** `status-channel` says that the server is up, is going
down, or will restart in so many minutes (once per restart, not at every
warning). By default (`"chat"`) it goes to the chat channel, where the players
are; a channel id sends it elsewhere and blank nowhere. The lines are language
keys (`discord.news.up`, `.down` and `.restart`) a server can reword.
`news-channel` is no longer read — a news channel is for news.

The list of channels and roles is read at most once per half minute
(`discord.names-every`), so changing ten channels does not spend Discord's
ten-a-minute budget shared with everything else the same bot does.

## The "is this you?" question

With `accounts.ask-on-discord` on, a linked player gets a direct message when
somebody joins under their name — who, from where, two buttons. In detail on the
[accounts page](/docs/keystone/accounts).

## Tickets on Discord

`tickets.category` and `tickets.roles` put private ticket channels in a category and give your staff roles access. The rest is on the [tickets page](/docs/keystone/tickets).
