# Server plugin API

Source: https://docs.gryt.chat/docs/server/plugin-api

Every capability, event and call a server plugin gets, generated from the source

{/* Generated by .github/scripts/generate-api-reference.mjs in the monorepo.
    Do not edit this file. Change the source it reads, then run the script. */}

This is the reference. [Server plugins](https://docs.gryt.chat/docs/server/plugins) is the page that
teaches the shape, and [plugin pairs](https://docs.gryt.chat/docs/guide/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.

```js
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`

```ts
id: string
```

This plugin's own id, so it does not have to hard-code the folder name.

### `capabilities`

```ts
capabilities: readonly PluginCapability[]
```

What the manifest declared, normalised. Readable so a plugin can degrade.

### `on()`

```ts
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`

```ts
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`

```ts
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`

```ts
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`.

```ts
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`.

```ts
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`.

```ts
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()`

```ts
kick(serverUserId: string, options?: { reason?: string }): Promise<ModerationOutcome>
```

Remove somebody from the server. They can come back.

### `ban()`

```ts
ban( serverUserId: string, options?: { reason?: string; durationMs?: number }, ): Promise<ModerationOutcome>
```

Remove somebody and stop them returning.

### `deleteMessage()`

```ts
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()`

```ts
on(topic: string, handler: PluginMessageHandler): void
```

Hear what the client half of this plugin sends.

### `send()`

```ts
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

| 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.
