Server plugin API
Every capability, event and call a server plugin gets, generated from the source
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 everything else hangs off: the log prefix, the config section, the storage namespace. |
name | string | |
version | string | |
main | string | Entry point, relative to the plugin's own folder. |
description? | string | |
author? | string | |
homepage? | string | Where to go and read what this plugin is (GRYT-941). Members are told this, so it is checked rather than trusted: http or https and nothing else. A javascript: or data: URL rendered as a link in somebody's client is the reason this is not just a string. |
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>): voidSubscribe to an event. Throws if the manifest did not ask for the capability behind it.
Throwing rather than returning false, and throwing at subscribe rather than failing silently at delivery: a plugin that never receives an event it thought it had asked for is a bad afternoon, and the stack trace at startup names the line.
moderation
moderation: PluginModerationKick and ban. Throws on access if the manifest did not declare
moderation, rather than handing back an object whose every call refuses
— a plugin should find out it has not been given this when it reaches for
it, not on the first member it tries to act on.
The calls themselves return an outcome rather than throwing. A refusal there is an ordinary answer — the member is a moderator, or already gone — and a plugin should be able to log it and carry on.
messaging
messaging: PluginMessagingThe pipe to the client half of this plugin. Throws on access if the
manifest did not declare messaging.
What arrives on it was written by a member's client. Check it. The transport caps the size and the rate and nothing else — the shape is the plugin's to establish, and assuming its own client half is on the other end is the mistake this note exists for.
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" | Why they are gone. A plugin logging arrivals and departures wants to write a different line for each, and one deciding whether to act wants to know it was not already handled by a human. |
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>Take a message down. Usually the more proportionate answer — most spam wants the post gone rather than the person.
Channels only. A direct message is between two people and a plugin the
operator installed has no business in it, 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): booleanSend to the client halves of this plugin. Returns false when the message was refused — too big, or a topic that is not one — and says why in the log.
Not a promise: this hands the message to socket.io and returns. There is no delivery to wait for and nothing useful to do about a client that is not listening.
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. Whatever the client plugin sent. Not validated beyond its size — this is the member's own bytes and a server plugin has to check it. |
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.