Server plugin API
Every capability, event and call a server plugin gets, generated from the source
Generated from the source and regenerated by CI, so it cannot fall behind the code. Editing this page by hand gets the edit wiped on the next run — change what the generator reads instead.
This is the reference. Server plugins is the page that teaches the shape, and plugin pairs covers talking to a copy of yourself in somebody's client.
A plugin starts by exporting activate, or a default export, and is called with
the object below.
export function activate(api) {
api.log.info(`${api.id} is up`);
}Capabilities
What a manifest may ask for. A name this server has never heard of is dropped rather than refused, so a plugin written against a newer Gryt still loads.
| Capability | What the operator reads |
|---|---|
messages:read | Read every message sent in a channel |
members:read | See who joins and leaves, and the invite code they used |
moderation | Kick and ban members and delete their messages, but not a moderator's |
messaging | Exchange its own messages with the copy of itself in people's clients |
The manifest
manifest.json, beside your entry point.
| Field | Type | What it is |
|---|---|---|
id | string | Folder name by convention, and the key the log prefix, config section and storage namespace all hang off. |
name | string | |
version | string | |
main | string | Entry point, relative to the plugin's own folder. |
description? | string | |
author? | string | |
homepage? | string | Members are shown this as a link, so http or https and nothing else: a javascript: URL here is an injection into every client. |
capabilities | PluginCapability[] | Normalised: deduplicated, in catalogue order, unknown names dropped. |
api
id
id: stringThis plugin's own id, so it does not have to hard-code the folder name.
capabilities
capabilities: readonly PluginCapability[]What the manifest declared, normalised. Readable so a plugin can degrade.
on()
on<E extends PluginEventName>(event: E, handler: PluginEventHandler<E>): voidThrows at subscribe rather than failing silently at delivery, so the stack trace at startup names the line.
moderation
moderation: PluginModerationThrows on access without the moderation capability, so a plugin finds
out when it reaches for this, not on the first member it acts on.
messaging
messaging: PluginMessagingWhat arrives here was written by a member's client, and the transport caps only size and rate. Do not assume your own client half sent it.
log
log: PluginLoggerGoes to the server log, prefixed with the plugin id.
Events
api.on(name, handler). Subscribing throws if the manifest did not declare the
capability behind the event, rather than failing quietly at delivery.
message:created
Needs messages:read.
api.on("message:created", (payload) => {})| Field | Type | What it is |
|---|---|---|
messageId | string | |
channelId | string | |
userId | string | The member's id on this server, not their identity key. |
nickname | string | null | |
text | string | |
attachmentCount | number | |
at | string |
member:joined
Needs members:read.
api.on("member:joined", (payload) => {})| Field | Type | What it is |
|---|---|---|
userId | string | |
nickname | string | null | |
inviteCode | string | null | Null when they did not come in through an invite. |
at | string |
member:left
Needs members:read.
api.on("member:left", (payload) => {})| Field | Type | What it is |
|---|---|---|
userId | string | |
nickname | string | null | |
reason | "left" | "kicked" | "banned" | A plugin logging departures wants a different line for each, and one deciding whether to act wants to know a human already did. |
at | string |
api.moderation
Needs moderation. Reading the property throws without it, rather than handing
back an object whose every call refuses.
A call returns an outcome rather than throwing. A refusal is an ordinary answer — the member can moderate, or is already gone — and a plugin should log it and carry on.
kick()
kick(serverUserId: string, options?: { reason?: string }): Promise<ModerationOutcome>Remove somebody from the server. They can come back.
ban()
ban( serverUserId: string, options?: { reason?: string; durationMs?: number }, ): Promise<ModerationOutcome>Remove somebody and stop them returning.
deleteMessage()
deleteMessage(channelId: string, messageId: string): Promise<ModerationOutcome>Channels only, the same rule that keeps DMs out of message:created.
api.messaging
Needs messaging. The pipe to the client half of this plugin.
on()
on(topic: string, handler: PluginMessageHandler): voidHear what the client half of this plugin sends.
send()
send(topic: string, data: unknown, target?: SendTarget): booleanFalse when refused, with the reason in the log. Not a promise: there is no delivery to wait for.
What a handler is given
| Field | Type | What it is |
|---|---|---|
topic | string | The sender picked it, within the limits below. |
data | unknown | The member's own bytes — check it. Not validated beyond its size: a member's own bytes, and the plugin's to check. |
userId | string | From the connection, so it cannot be faked. Who sent it, as their id on this server. |
nickname | string | null | From the connection, so it cannot be faked. |
What Gryt drops before you see it
| Limit | Value |
|---|---|
| Topic length | 64 characters |
| Payload size | 8192 bytes |
| Nesting depth | 8 |
| Values in a payload | 512 |
A __proto__, constructor or prototype key is refused as well, and so is
anybody sending more than thirty messages in ten seconds.