Gryt
Build on Gryt

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.

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 the log prefix, config section and storage namespace all hang off.
namestring
versionstring
mainstringEntry point, relative to the plugin's own folder.
description?string
author?string
homepage?stringMembers are shown this as a link, so http or https and nothing else: a javascript: URL here is an injection into every client.
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

Throws at subscribe rather than failing silently at delivery, so the stack trace at startup names the line.

moderation

moderation: PluginModeration

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

What 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: 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"A plugin logging departures wants a different line for each, and one deciding whether to act wants to know a human already did.
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>

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

Hear what the client half of this plugin sends.

send()

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

False when refused, with the reason in the log. Not a promise: there is no delivery to wait for.

What a handler is given

FieldTypeWhat it is
topicstringThe sender picked it, within the limits below.
dataunknownThe member's own bytes — check it. Not validated beyond its size: a member's own bytes, and the plugin's to check.
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