Gryt

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.

CapabilityWhat the operator reads
messages:readRead every message sent in a channel
members:readSee who joins and leaves, and the invite code they used
moderationKick and ban members and delete their messages, but not a moderator's
messagingExchange its own messages with the copy of itself in people's clients

The manifest

manifest.json, beside your entry point.

FieldTypeWhat it is
idstringFolder name by convention, and the key everything else hangs off: the log prefix, the config section, the storage namespace.
namestring
versionstring
mainstringEntry point, relative to the plugin's own folder.
description?string
author?string
homepage?stringWhere 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.
capabilitiesPluginCapability[]Normalised: deduplicated, in catalogue order, unknown names dropped.

api

id

id: string

This 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>): void

Subscribe 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: PluginModeration

Kick 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: PluginMessaging

The 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: PluginLogger

Goes 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) => {})
FieldTypeWhat it is
messageIdstring
channelIdstring
userIdstringThe member's id on this server, not their identity key.
nicknamestring | null
textstring
attachmentCountnumber
atstring

member:joined

Needs members:read.

api.on("member:joined", (payload) => {})
FieldTypeWhat it is
userIdstring
nicknamestring | null
inviteCodestring | nullNull when they did not come in through an invite.
atstring

member:left

Needs members:read.

api.on("member:left", (payload) => {})
FieldTypeWhat it is
userIdstring
nicknamestring | 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.
atstring

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): void

Hear what the client half of this plugin sends.

send()

send(topic: string, data: unknown, target?: SendTarget): boolean

Send 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

FieldTypeWhat it is
topicstringThe sender picked it, within the limits below.
dataunknownThe 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.
userIdstringFrom the connection, so it cannot be faked. Who sent it, as their id on this server.
nicknamestring | nullFrom the connection, so it cannot be faked.

What Gryt drops before you see it

LimitValue
Topic length64 characters
Payload size8192 bytes
Nesting depth8
Values in a payload512

A __proto__, constructor or prototype key is refused as well, and so is anybody sending more than thirty messages in ten seconds.

On this page