feat: Add browser documentation
This commit is contained in:
301
docs-site/docs/plugin-development/api-reference.md
Normal file
301
docs-site/docs/plugin-development/api-reference.md
Normal file
@@ -0,0 +1,301 @@
|
||||
---
|
||||
sidebar_position: 4
|
||||
---
|
||||
|
||||
# Plugin API Reference
|
||||
|
||||
`TojuClientPluginApi` is the object passed to a plugin activation context. The runtime freezes the API object before passing it to plugin code.
|
||||
|
||||
## Activation Types
|
||||
|
||||
```ts
|
||||
interface TojuPluginDisposable {
|
||||
dispose: () => void;
|
||||
}
|
||||
|
||||
interface TojuPluginActivationContext {
|
||||
api: TojuClientPluginApi;
|
||||
manifest: TojuPluginManifest;
|
||||
pluginId: string;
|
||||
subscriptions: TojuPluginDisposable[];
|
||||
}
|
||||
|
||||
interface TojuClientPluginModule {
|
||||
activate?: (context: TojuPluginActivationContext) => Promise<void> | void;
|
||||
deactivate?: (context: TojuPluginActivationContext) => Promise<void> | void;
|
||||
onPluginDataChanged?: (context: TojuPluginActivationContext, event: unknown) => Promise<void> | void;
|
||||
onServerRequirementsChanged?: (context: TojuPluginActivationContext, snapshot: PluginRequirementsSnapshot) => Promise<void> | void;
|
||||
ready?: (context: TojuPluginActivationContext) => Promise<void> | void;
|
||||
}
|
||||
```
|
||||
|
||||
## Profiles
|
||||
|
||||
```ts
|
||||
interface PluginApiProfileUpdate {
|
||||
description?: string;
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
interface PluginApiAvatarUpdate {
|
||||
avatarHash: string;
|
||||
avatarMime: string;
|
||||
avatarUrl: string;
|
||||
}
|
||||
```
|
||||
|
||||
| Method | Capability | Description |
|
||||
| --- | --- | --- |
|
||||
| `profile.getCurrent()` | `profile.read` | Returns the current `User` or `null`. |
|
||||
| `profile.update(profile)` | `profile.write` | Updates display name and optional description. |
|
||||
| `profile.updateAvatar(avatar)` | `profile.write` | Updates avatar URL, MIME type, and hash metadata. |
|
||||
|
||||
## Users and Roles
|
||||
|
||||
| Method | Capability | Description |
|
||||
| --- | --- | --- |
|
||||
| `users.getCurrent()` | `users.read` | Returns current `User` or `null`. |
|
||||
| `users.list()` | `users.read` | Returns known users. |
|
||||
| `users.readMembers()` | `users.read` | Returns active room members. |
|
||||
| `users.setRole(userId, role)` | `roles.manage` | Updates a user's role. |
|
||||
| `users.kick(userId)` | `users.manage` | Kicks a user. |
|
||||
| `users.ban(userId, reason?)` | `users.manage` | Bans a user with optional reason. |
|
||||
| `roles.list()` | `roles.read` | Returns room roles. |
|
||||
| `roles.setAssignments(assignments)` | `roles.manage` | Replaces role assignments. |
|
||||
|
||||
## Server
|
||||
|
||||
```ts
|
||||
interface PluginApiServerSettingsUpdate {
|
||||
description?: string;
|
||||
isPrivate?: boolean;
|
||||
maxUsers?: number;
|
||||
name?: string;
|
||||
password?: string;
|
||||
topic?: string;
|
||||
}
|
||||
|
||||
interface PluginApiPluginUserRequest {
|
||||
avatarUrl?: string;
|
||||
displayName: string;
|
||||
id?: string;
|
||||
}
|
||||
```
|
||||
|
||||
| Method | Capability | Description |
|
||||
| --- | --- | --- |
|
||||
| `server.getCurrent()` | `server.read` | Returns the current `Room` or `null`. |
|
||||
| `server.registerPluginUser(request)` | `users.manage` | Adds a plugin-owned user and returns its id. |
|
||||
| `server.updatePermissions(permissions)` | `server.manage` | Updates partial room permissions. |
|
||||
| `server.updateSettings(settings)` | `server.manage` | Updates room settings. |
|
||||
|
||||
## Channels
|
||||
|
||||
```ts
|
||||
interface PluginApiChannelRequest {
|
||||
id?: string;
|
||||
name: string;
|
||||
position?: number;
|
||||
}
|
||||
```
|
||||
|
||||
| Method | Capability | Description |
|
||||
| --- | --- | --- |
|
||||
| `channels.list()` | `channels.read` | Returns current room channels. |
|
||||
| `channels.select(channelId)` | `channels.read` | Selects a channel. |
|
||||
| `channels.addAudioChannel(request)` | `channels.manage` | Adds a voice channel. |
|
||||
| `channels.addVideoChannel(request)` | `channels.manage` | Registers a video channel section. |
|
||||
| `channels.rename(channelId, name)` | `channels.manage` | Renames a channel. |
|
||||
| `channels.remove(channelId)` | `channels.manage` | Removes a channel. |
|
||||
|
||||
## Messages
|
||||
|
||||
```ts
|
||||
interface PluginApiMessageAsPluginUserRequest {
|
||||
channelId?: string;
|
||||
content: string;
|
||||
pluginUserId: string;
|
||||
}
|
||||
```
|
||||
|
||||
| Method | Capability | Description |
|
||||
| --- | --- | --- |
|
||||
| `messages.readCurrent()` | `messages.read` | Returns current visible messages. |
|
||||
| `messages.send(content, channelId?)` | `messages.send` | Sends a message and returns the created `Message`. |
|
||||
| `messages.sendAsPluginUser(request)` | `messages.send` | Emits a message from a registered plugin user. |
|
||||
| `messages.edit(messageId, content)` | `messages.editOwn` | Edits a plugin message. |
|
||||
| `messages.delete(messageId)` | `messages.deleteOwn` | Deletes a plugin message. |
|
||||
| `messages.moderateDelete(messageId)` | `messages.moderate` | Performs a moderation delete. |
|
||||
| `messages.sync(messages)` | `messages.sync` | Syncs an array of messages into state. |
|
||||
|
||||
## Events
|
||||
|
||||
```ts
|
||||
interface PluginApiEventSubscription {
|
||||
eventName: string;
|
||||
handler: (event: PluginEventEnvelope) => void;
|
||||
}
|
||||
|
||||
interface PluginEventEnvelope<TPayload = unknown> {
|
||||
emittedAt?: number;
|
||||
eventId?: string;
|
||||
eventName: string;
|
||||
payload: TPayload;
|
||||
pluginId: string;
|
||||
serverId: string;
|
||||
sourcePluginUserId?: string;
|
||||
sourceUserId?: string;
|
||||
type: 'plugin_event';
|
||||
}
|
||||
```
|
||||
|
||||
| Method | Capability | Description |
|
||||
| --- | --- | --- |
|
||||
| `events.publishServer(eventName, payload)` | `events.server.publish` | Sends a declared plugin event through the signaling server. |
|
||||
| `events.subscribeServer(subscription)` | `events.server.subscribe` | Subscribes to a declared server plugin event. |
|
||||
| `events.publishP2p(eventName, payload)` | `events.p2p.publish` | Sends a declared plugin event over peer paths. |
|
||||
| `events.subscribeP2p(subscription)` | `events.p2p.subscribe` | Registers a P2P event subscription. |
|
||||
|
||||
## Message Bus
|
||||
|
||||
```ts
|
||||
interface PluginApiMessageBusEnvelope {
|
||||
channelId?: string;
|
||||
eventId: string;
|
||||
messages?: Message[];
|
||||
payload?: unknown;
|
||||
pluginId: string;
|
||||
roomId: string;
|
||||
sentAt: number;
|
||||
sourcePeerId?: string;
|
||||
sourceUserId?: string;
|
||||
topic: string;
|
||||
}
|
||||
|
||||
interface PluginApiMessageBusLatestRequest {
|
||||
channelId?: string;
|
||||
includeDeleted?: boolean;
|
||||
limit?: number;
|
||||
sinceTimestamp?: number;
|
||||
targetPeerId?: string;
|
||||
topic?: string;
|
||||
}
|
||||
|
||||
interface PluginApiMessageBusPublishRequest extends PluginApiMessageBusLatestRequest {
|
||||
includeLatestMessages?: boolean;
|
||||
includeSelf?: boolean;
|
||||
payload?: unknown;
|
||||
topic: string;
|
||||
}
|
||||
|
||||
interface PluginApiMessageBusSubscription {
|
||||
channelId?: string;
|
||||
handler: (event: PluginApiMessageBusEnvelope) => void;
|
||||
latestMessageLimit?: number;
|
||||
replayLatest?: boolean;
|
||||
topic?: string;
|
||||
}
|
||||
```
|
||||
|
||||
| Method | Capability | Description |
|
||||
| --- | --- | --- |
|
||||
| `messageBus.publish(request)` | `events.p2p.publish`, optionally `messages.read` | Publishes a plugin-bus event, optionally including latest messages. |
|
||||
| `messageBus.sendLatestMessages(request?)` | `events.p2p.publish` and `messages.read` | Sends a latest-message snapshot. |
|
||||
| `messageBus.subscribe(subscription)` | `events.p2p.subscribe`, optionally `messages.read` | Subscribes to plugin-bus events, optionally replaying latest messages. |
|
||||
|
||||
## P2P and Media
|
||||
|
||||
```ts
|
||||
interface PluginApiAudioClipRequest {
|
||||
volume?: number;
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface PluginApiCustomStreamRequest {
|
||||
label?: string;
|
||||
stream: MediaStream;
|
||||
}
|
||||
```
|
||||
|
||||
| Method | Capability | Description |
|
||||
| --- | --- | --- |
|
||||
| `p2p.connectedPeers()` | `p2p.data` | Returns connected peer ids. |
|
||||
| `p2p.broadcastData(eventName, payload)` | `p2p.data` | Broadcasts plugin data. |
|
||||
| `p2p.sendData(peerId, eventName, payload)` | `p2p.data` | Sends plugin data targeted to a peer. |
|
||||
| `media.playAudioClip(request)` | `media.playAudio` | Plays an audio URL at optional volume. |
|
||||
| `media.addCustomAudioStream(request)` | `media.addAudioStream` | Contributes an audio `MediaStream`. |
|
||||
| `media.addCustomVideoStream(request)` | `media.addVideoStream` | Registers a video `MediaStream` contribution. |
|
||||
| `media.setInputVolume(volume)` | `audio.volume` | Sets local input volume. |
|
||||
| `media.setOutputVolume(volume)` | `audio.volume` | Sets local output volume. |
|
||||
|
||||
## Storage
|
||||
|
||||
| Method | Capability | Description |
|
||||
| --- | --- | --- |
|
||||
| `clientData.read(key)` | `storage.local` | Reads async plugin-local data. |
|
||||
| `clientData.write(key, value)` | `storage.local` | Writes async plugin-local data. |
|
||||
| `clientData.remove(key)` | `storage.local` | Removes async plugin-local data. |
|
||||
| `serverData.read(key)` | `storage.serverData.read` | Reads local per-user/per-server data. |
|
||||
| `serverData.write(key, value)` | `storage.serverData.write` | Writes local per-user/per-server data. |
|
||||
| `serverData.remove(key)` | `storage.serverData.write` | Removes local per-user/per-server data. |
|
||||
| `storage.get(key)` | `storage.local` | Legacy synchronous local read. |
|
||||
| `storage.set(key, value)` | `storage.local` | Legacy synchronous local write. |
|
||||
| `storage.remove(key)` | `storage.local` | Legacy synchronous local remove. |
|
||||
|
||||
## UI Contributions
|
||||
|
||||
```ts
|
||||
interface PluginApiActionContribution {
|
||||
icon?: string;
|
||||
label: string;
|
||||
run: () => Promise<void> | void;
|
||||
}
|
||||
|
||||
interface PluginApiPageContribution {
|
||||
label: string;
|
||||
path: string;
|
||||
render: () => HTMLElement | string;
|
||||
}
|
||||
|
||||
interface PluginApiPanelContribution {
|
||||
label: string;
|
||||
order?: number;
|
||||
render: () => HTMLElement | string;
|
||||
}
|
||||
|
||||
interface PluginApiSettingsPageContribution {
|
||||
label: string;
|
||||
order?: number;
|
||||
render: () => HTMLElement | string;
|
||||
settingsKey?: string;
|
||||
}
|
||||
|
||||
interface PluginApiChannelSectionContribution {
|
||||
label: string;
|
||||
order?: number;
|
||||
type?: 'audio' | 'custom' | 'video';
|
||||
}
|
||||
|
||||
interface PluginApiEmbedRendererContribution {
|
||||
embedType: string;
|
||||
render: (payload: unknown) => HTMLElement | string;
|
||||
}
|
||||
|
||||
interface PluginApiDomMountRequest {
|
||||
element: HTMLElement;
|
||||
position?: InsertPosition;
|
||||
target: Element | string;
|
||||
}
|
||||
```
|
||||
|
||||
| Method | Capability | Description |
|
||||
| --- | --- | --- |
|
||||
| `ui.registerAppPage(id, contribution)` | `ui.pages` | Adds a plugin app page. |
|
||||
| `ui.registerSettingsPage(id, contribution)` | `ui.settings` | Adds a plugin settings page. |
|
||||
| `ui.registerSidePanel(id, contribution)` | `ui.sidePanel` | Adds a side panel. |
|
||||
| `ui.registerChannelSection(id, contribution)` | `ui.channelsSection` | Adds a channel section. |
|
||||
| `ui.registerComposerAction(id, contribution)` | `ui.pages` | Adds a composer action. |
|
||||
| `ui.registerProfileAction(id, contribution)` | `ui.pages` | Adds a profile action. |
|
||||
| `ui.registerToolbarAction(id, contribution)` | `ui.pages` | Adds a toolbar action. |
|
||||
| `ui.registerEmbedRenderer(id, contribution)` | `ui.embeds` | Adds an embed renderer. |
|
||||
| `ui.mountElement(id, request)` | `ui.dom` | Mounts plugin-owned DOM into a target element or selector. |
|
||||
50
docs-site/docs/plugin-development/capabilities.md
Normal file
50
docs-site/docs/plugin-development/capabilities.md
Normal file
@@ -0,0 +1,50 @@
|
||||
---
|
||||
sidebar_position: 3
|
||||
---
|
||||
|
||||
# Capabilities
|
||||
|
||||
Capabilities protect privileged app surfaces. A plugin must declare a capability in its manifest and the user must grant it before the runtime allows the corresponding API call.
|
||||
|
||||
| Capability | API areas | Notes |
|
||||
| --- | --- | --- |
|
||||
| `profile.read` | `profile.getCurrent()` | Reads the current user. |
|
||||
| `profile.write` | `profile.update()`, `profile.updateAvatar()` | Updates local profile fields and avatar metadata. |
|
||||
| `users.read` | `users.getCurrent()`, `users.list()`, `users.readMembers()` | Reads users and server members. |
|
||||
| `users.manage` | `users.kick()`, `users.ban()`, `server.registerPluginUser()` | Can create plugin users and moderate members. |
|
||||
| `roles.read` | `roles.list()` | Reads server roles. |
|
||||
| `roles.manage` | `roles.setAssignments()`, `users.setRole()` | Changes role assignments or user roles. |
|
||||
| `messages.read` | `messages.readCurrent()`, message bus latest snapshots | Reads current channel messages. |
|
||||
| `messages.send` | `messages.send()`, `messages.sendAsPluginUser()` | Sends messages as the current user or registered plugin user. |
|
||||
| `messages.editOwn` | `messages.edit()` | Edits plugin-owned messages. |
|
||||
| `messages.deleteOwn` | `messages.delete()` | Deletes plugin-owned messages. |
|
||||
| `messages.moderate` | `messages.moderateDelete()` | Moderation delete path. |
|
||||
| `messages.sync` | `messages.sync()` | Syncs message arrays into client state. |
|
||||
| `channels.read` | `channels.list()`, `channels.select()` | Reads and selects channels. |
|
||||
| `channels.manage` | `channels.addAudioChannel()`, `channels.addVideoChannel()`, `channels.remove()`, `channels.rename()` | Mutates channel or channel-section state. |
|
||||
| `server.read` | `server.getCurrent()` | Reads active server. |
|
||||
| `server.manage` | `server.updatePermissions()`, `server.updateSettings()` | Updates server permissions or settings. |
|
||||
| `p2p.data` | `p2p.connectedPeers()`, `p2p.broadcastData()`, `p2p.sendData()` | Uses plugin peer data paths. |
|
||||
| `p2p.media` | Reserved peer media features. | Included for media-facing plugins. |
|
||||
| `media.playAudio` | `media.playAudioClip()` | Plays an audio URL locally. |
|
||||
| `media.addAudioStream` | `media.addCustomAudioStream()` | Adds a custom stream to voice handling. |
|
||||
| `media.addVideoStream` | `media.addCustomVideoStream()` | Registers custom video stream contribution. |
|
||||
| `audio.volume` | `media.setInputVolume()`, `media.setOutputVolume()` | Adjusts local voice volume. |
|
||||
| `audio.effects` | Reserved audio effect features. | Included for audio processing plugins. |
|
||||
| `ui.settings` | `ui.registerSettingsPage()` | Adds settings pages. |
|
||||
| `ui.pages` | `ui.registerAppPage()`, `ui.registerComposerAction()`, `ui.registerProfileAction()`, `ui.registerToolbarAction()` | Adds app pages and actions. |
|
||||
| `ui.sidePanel` | `ui.registerSidePanel()` | Adds side panels. |
|
||||
| `ui.channelsSection` | `ui.registerChannelSection()` | Adds channel sections. |
|
||||
| `ui.embeds` | `ui.registerEmbedRenderer()` | Renders custom embeds. |
|
||||
| `ui.dom` | `ui.mountElement()` | Mounts plugin-owned DOM into app targets. |
|
||||
| `storage.local` | `storage.*`, `clientData.*` | Reads and writes plugin-local data. |
|
||||
| `storage.serverData.read` | `serverData.read()` | Reads local per-user/per-server plugin data. |
|
||||
| `storage.serverData.write` | `serverData.write()`, `serverData.remove()` | Writes or removes local per-user/per-server plugin data. |
|
||||
| `events.server.publish` | `events.publishServer()` | Publishes declared server plugin events. |
|
||||
| `events.server.subscribe` | `events.subscribeServer()` | Subscribes to declared server plugin events. |
|
||||
| `events.p2p.publish` | `events.publishP2p()`, `messageBus.publish()`, `messageBus.sendLatestMessages()` | Publishes declared P2P/plugin bus events. |
|
||||
| `events.p2p.subscribe` | `events.subscribeP2p()`, `messageBus.subscribe()` | Subscribes to declared P2P/plugin bus events. |
|
||||
|
||||
## Recommended Practice
|
||||
|
||||
Request the fewest capabilities possible. Separate broad features into optional plugin modules when a single plugin would otherwise need many unrelated grants.
|
||||
106
docs-site/docs/plugin-development/create-a-plugin.md
Normal file
106
docs-site/docs/plugin-development/create-a-plugin.md
Normal file
@@ -0,0 +1,106 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# Create a Plugin
|
||||
|
||||
MetoYou plugins are browser-safe ES modules loaded by the Angular renderer. A plugin receives a frozen `TojuClientPluginApi`, declares every privileged capability in its manifest, and registers cleanup work through disposables.
|
||||
|
||||
## Folder Layout
|
||||
|
||||
A local desktop plugin is discovered from an immediate child folder under the app data `plugins` directory.
|
||||
|
||||
```text
|
||||
my-plugin/
|
||||
toju-plugin.json
|
||||
main.js
|
||||
README.md
|
||||
icon.svg
|
||||
```
|
||||
|
||||
The manifest file can be named `toju-plugin.json` or `plugin.json`. Entrypoints and readmes must stay inside the plugin folder.
|
||||
|
||||
## Minimal Manifest
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "example.hello-world",
|
||||
"title": "Hello World",
|
||||
"description": "Adds a toolbar action that sends a message.",
|
||||
"version": "1.0.0",
|
||||
"kind": "client",
|
||||
"scope": "client",
|
||||
"apiVersion": "1.0.0",
|
||||
"compatibility": {
|
||||
"minimumTojuVersion": "1.0.0"
|
||||
},
|
||||
"entrypoint": "./main.js",
|
||||
"capabilities": ["messages.send", "ui.pages"]
|
||||
}
|
||||
```
|
||||
|
||||
## Entrypoint
|
||||
|
||||
```js
|
||||
export function activate(context) {
|
||||
const { api } = context;
|
||||
|
||||
api.logger.info('Hello World activated');
|
||||
|
||||
const disposable = api.ui.registerToolbarAction('hello', {
|
||||
label: 'Hello',
|
||||
run: () => api.messages.send('Hello from my plugin')
|
||||
});
|
||||
|
||||
context.subscriptions.push(disposable);
|
||||
}
|
||||
|
||||
export function ready(context) {
|
||||
context.api.logger.info('All ready plugins have loaded');
|
||||
}
|
||||
|
||||
export function deactivate(context) {
|
||||
context.api.logger.info('Hello World deactivated');
|
||||
}
|
||||
```
|
||||
|
||||
## Lifecycle Hooks
|
||||
|
||||
| Hook | When it runs | Use it for |
|
||||
| --- | --- | --- |
|
||||
| `activate(context)` | During explicit plugin activation. | Register UI, subscribe to events, initialize state. |
|
||||
| `ready(context)` | After the load-order pass has activated ready plugins. | Cross-plugin coordination that needs other plugins loaded. |
|
||||
| `deactivate(context)` | During unload or reload. | Flush state and log shutdown. Disposables are also cleaned up by the host. |
|
||||
| `onPluginDataChanged(context, event)` | When plugin data changes are observed. | React to plugin-scoped persistence changes. |
|
||||
| `onServerRequirementsChanged(context, snapshot)` | When server plugin requirements change. | Adapt to required, optional, blocked, or incompatible server plugins. |
|
||||
|
||||
## Cleanup
|
||||
|
||||
Every API registration returns a disposable. Push it into `context.subscriptions`.
|
||||
|
||||
```js
|
||||
const subscription = api.messageBus.subscribe({
|
||||
topic: 'poll:votes',
|
||||
handler: (event) => api.logger.info('vote received', event.payload)
|
||||
});
|
||||
|
||||
context.subscriptions.push(subscription);
|
||||
```
|
||||
|
||||
The plugin host disposes subscriptions in reverse order when the plugin unloads.
|
||||
|
||||
## Capability Grants
|
||||
|
||||
A plugin can only call privileged APIs after the matching capability is declared in the manifest and granted by the user. Keep the manifest narrow. For example, a plugin that only adds a settings page does not need message or user management capabilities.
|
||||
|
||||
## Testing Locally
|
||||
|
||||
1. Create the plugin folder in the desktop plugins directory.
|
||||
2. Open the Plugin Manager.
|
||||
3. Register or refresh local plugins.
|
||||
4. Grant required capabilities.
|
||||
5. Activate the plugin.
|
||||
6. Inspect plugin logs in the manager.
|
||||
|
||||
For broad API examples, compare against the E2E fixture plugin under `toju-app/public/plugins/e2e-all-api/`.
|
||||
204
docs-site/docs/plugin-development/examples.md
Normal file
204
docs-site/docs/plugin-development/examples.md
Normal file
@@ -0,0 +1,204 @@
|
||||
---
|
||||
sidebar_position: 5
|
||||
---
|
||||
|
||||
# Examples
|
||||
|
||||
## Toolbar Message Plugin
|
||||
|
||||
`toju-plugin.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "example.toolbar-message",
|
||||
"title": "Toolbar Message",
|
||||
"description": "Adds a toolbar action that sends a reusable message.",
|
||||
"version": "1.0.0",
|
||||
"kind": "client",
|
||||
"scope": "client",
|
||||
"apiVersion": "1.0.0",
|
||||
"compatibility": {
|
||||
"minimumTojuVersion": "1.0.0",
|
||||
"verifiedTojuVersion": "1.0.0"
|
||||
},
|
||||
"entrypoint": "./main.js",
|
||||
"capabilities": ["messages.send", "ui.pages"]
|
||||
}
|
||||
```
|
||||
|
||||
`main.js`
|
||||
|
||||
```js
|
||||
export function activate(context) {
|
||||
const { api } = context;
|
||||
|
||||
context.subscriptions.push(api.ui.registerToolbarAction('standup-message', {
|
||||
label: 'Standup',
|
||||
run: () => api.messages.send('Standup: yesterday, today, blocked')
|
||||
}));
|
||||
}
|
||||
```
|
||||
|
||||
## Settings Page Plugin
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "example.settings-page",
|
||||
"title": "Settings Page Example",
|
||||
"description": "Adds a plugin settings page and stores a local preference.",
|
||||
"version": "1.0.0",
|
||||
"kind": "client",
|
||||
"apiVersion": "1.0.0",
|
||||
"compatibility": { "minimumTojuVersion": "1.0.0" },
|
||||
"entrypoint": "./main.js",
|
||||
"capabilities": ["ui.settings", "storage.local"],
|
||||
"settings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean", "default": true }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```js
|
||||
export function activate(context) {
|
||||
const { api } = context;
|
||||
|
||||
context.subscriptions.push(api.ui.registerSettingsPage('preferences', {
|
||||
label: 'Example Preferences',
|
||||
render: () => {
|
||||
const root = document.createElement('section');
|
||||
const button = document.createElement('button');
|
||||
|
||||
button.type = 'button';
|
||||
button.textContent = 'Remember preference';
|
||||
button.onclick = () => api.storage.set('enabled', true);
|
||||
root.append(button);
|
||||
return root;
|
||||
}
|
||||
}));
|
||||
}
|
||||
```
|
||||
|
||||
## Server-Scoped Soundboard
|
||||
|
||||
A server-scoped plugin can be installed as a server requirement and auto-installed for server members when marked required.
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "example.soundboard",
|
||||
"title": "Server Soundboard",
|
||||
"description": "Adds a soundboard side panel and announces played sounds.",
|
||||
"version": "1.0.0",
|
||||
"kind": "client",
|
||||
"scope": "server",
|
||||
"apiVersion": "1.0.0",
|
||||
"compatibility": { "minimumTojuVersion": "1.0.0" },
|
||||
"entrypoint": "./main.js",
|
||||
"capabilities": [
|
||||
"server.read",
|
||||
"users.manage",
|
||||
"ui.sidePanel",
|
||||
"media.playAudio",
|
||||
"messages.send"
|
||||
],
|
||||
"pluginUser": {
|
||||
"displayName": "Soundboard",
|
||||
"label": "Audio helper"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```js
|
||||
export function activate(context) {
|
||||
const { api } = context;
|
||||
const botId = api.server.registerPluginUser({
|
||||
id: 'soundboard-bot',
|
||||
displayName: 'Soundboard'
|
||||
});
|
||||
|
||||
context.subscriptions.push(api.ui.registerSidePanel('sounds', {
|
||||
label: 'Soundboard',
|
||||
render: () => {
|
||||
const panel = document.createElement('div');
|
||||
const button = document.createElement('button');
|
||||
|
||||
button.type = 'button';
|
||||
button.textContent = 'Play chime';
|
||||
button.onclick = async () => {
|
||||
await api.media.playAudioClip({ url: './chime.wav', volume: 0.7 });
|
||||
api.messages.sendAsPluginUser({ pluginUserId: botId, content: 'Played chime' });
|
||||
};
|
||||
|
||||
panel.append(button);
|
||||
return panel;
|
||||
}
|
||||
}));
|
||||
}
|
||||
```
|
||||
|
||||
## Message Bus Plugin
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "example.poll-bus",
|
||||
"title": "Poll Bus",
|
||||
"description": "Uses the plugin message bus for lightweight P2P poll votes.",
|
||||
"version": "1.0.0",
|
||||
"kind": "client",
|
||||
"apiVersion": "1.0.0",
|
||||
"compatibility": { "minimumTojuVersion": "1.0.0" },
|
||||
"entrypoint": "./main.js",
|
||||
"capabilities": ["events.p2p.publish", "events.p2p.subscribe", "messages.read"]
|
||||
}
|
||||
```
|
||||
|
||||
```js
|
||||
export function activate(context) {
|
||||
const { api } = context;
|
||||
|
||||
context.subscriptions.push(api.messageBus.subscribe({
|
||||
topic: 'poll:votes',
|
||||
replayLatest: true,
|
||||
latestMessageLimit: 20,
|
||||
handler: (event) => api.logger.info('Vote received', event.payload)
|
||||
}));
|
||||
|
||||
api.messageBus.publish({
|
||||
topic: 'poll:votes',
|
||||
payload: { option: 'A' },
|
||||
includeLatestMessages: true,
|
||||
includeSelf: true,
|
||||
latestMessageLimit: 20
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Custom DOM Mount
|
||||
|
||||
Use `ui.dom` sparingly and cleanly. The runtime tags mounted elements with plugin ownership metadata and removes remaining mounted elements when the plugin unloads.
|
||||
|
||||
```js
|
||||
export function activate(context) {
|
||||
const badge = document.createElement('div');
|
||||
|
||||
badge.textContent = 'Plugin active';
|
||||
badge.style.position = 'absolute';
|
||||
badge.style.right = '1rem';
|
||||
badge.style.bottom = '1rem';
|
||||
|
||||
context.subscriptions.push(context.api.ui.mountElement('active-badge', {
|
||||
target: 'body',
|
||||
element: badge
|
||||
}));
|
||||
}
|
||||
```
|
||||
|
||||
## All-API Fixture
|
||||
|
||||
The repo includes an E2E fixture at `toju-app/public/plugins/e2e-all-api/`. It intentionally calls every public plugin API surface so Playwright coverage can validate the runtime. Use it as a compatibility reference, not as the minimal style for production plugins.
|
||||
162
docs-site/docs/plugin-development/manifest.md
Normal file
162
docs-site/docs/plugin-development/manifest.md
Normal file
@@ -0,0 +1,162 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
---
|
||||
|
||||
# Manifest Model
|
||||
|
||||
The manifest is the source of truth for plugin identity, compatibility, runtime shape, capabilities, data, events, UI hints, and distribution metadata.
|
||||
|
||||
```ts
|
||||
type TojuPluginInstallScope = 'client' | 'server';
|
||||
type PluginEventDirection = 'clientToServer' | 'serverRelay' | 'p2pHint';
|
||||
type PluginEventScope = 'server' | 'channel' | 'user' | 'plugin';
|
||||
|
||||
type PluginCapabilityId =
|
||||
| 'profile.read'
|
||||
| 'profile.write'
|
||||
| 'users.read'
|
||||
| 'users.manage'
|
||||
| 'roles.read'
|
||||
| 'roles.manage'
|
||||
| 'messages.read'
|
||||
| 'messages.send'
|
||||
| 'messages.editOwn'
|
||||
| 'messages.deleteOwn'
|
||||
| 'messages.moderate'
|
||||
| 'messages.sync'
|
||||
| 'channels.read'
|
||||
| 'channels.manage'
|
||||
| 'server.read'
|
||||
| 'server.manage'
|
||||
| 'p2p.data'
|
||||
| 'p2p.media'
|
||||
| 'media.playAudio'
|
||||
| 'media.addAudioStream'
|
||||
| 'media.addVideoStream'
|
||||
| 'audio.volume'
|
||||
| 'audio.effects'
|
||||
| 'ui.settings'
|
||||
| 'ui.pages'
|
||||
| 'ui.sidePanel'
|
||||
| 'ui.channelsSection'
|
||||
| 'ui.embeds'
|
||||
| 'ui.dom'
|
||||
| 'storage.local'
|
||||
| 'storage.serverData.read'
|
||||
| 'storage.serverData.write'
|
||||
| 'events.server.publish'
|
||||
| 'events.server.subscribe'
|
||||
| 'events.p2p.publish'
|
||||
| 'events.p2p.subscribe';
|
||||
|
||||
interface TojuPluginManifest {
|
||||
schemaVersion: 1;
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
version: string;
|
||||
kind: 'client' | 'library';
|
||||
scope?: TojuPluginInstallScope;
|
||||
apiVersion: string;
|
||||
compatibility: {
|
||||
minimumTojuVersion: string;
|
||||
maximumTojuVersion?: string;
|
||||
verifiedTojuVersion?: string;
|
||||
};
|
||||
entrypoint?: string;
|
||||
bundle?: {
|
||||
url: string;
|
||||
entrypoint?: string;
|
||||
};
|
||||
readme?: string;
|
||||
homepage?: string;
|
||||
bugs?: string;
|
||||
changelog?: string;
|
||||
license?: string;
|
||||
authors?: {
|
||||
name: string;
|
||||
email?: string;
|
||||
url?: string;
|
||||
}[];
|
||||
capabilities?: PluginCapabilityId[];
|
||||
events?: {
|
||||
eventName: string;
|
||||
direction: PluginEventDirection;
|
||||
scope: PluginEventScope;
|
||||
maxPayloadBytes?: number;
|
||||
schema?: string;
|
||||
}[];
|
||||
data?: {
|
||||
key: string;
|
||||
schema?: string;
|
||||
scope: string;
|
||||
storage: 'local' | 'serverData';
|
||||
}[];
|
||||
relationships?: {
|
||||
after?: string[];
|
||||
before?: string[];
|
||||
conflicts?: string[];
|
||||
optional?: { id: string; versionRange?: string }[];
|
||||
requires?: { id: string; versionRange?: string }[];
|
||||
};
|
||||
load?: {
|
||||
priority?: 'bootstrap' | 'high' | 'default' | 'low';
|
||||
};
|
||||
pluginUser?: {
|
||||
avatar?: string;
|
||||
displayName: string;
|
||||
label?: string;
|
||||
};
|
||||
settings?: Record<string, unknown>;
|
||||
ui?: Record<string, unknown>;
|
||||
}
|
||||
```
|
||||
|
||||
## Required Fields
|
||||
|
||||
| Field | Meaning |
|
||||
| --- | --- |
|
||||
| `schemaVersion` | Manifest schema version. Currently `1`. |
|
||||
| `id` | Stable plugin id. Use a reverse-DNS or package-style id. |
|
||||
| `title` | Human-readable plugin name. |
|
||||
| `description` | Short explanation shown in plugin UI. |
|
||||
| `version` | Plugin version. |
|
||||
| `kind` | `client` for runtime plugins, `library` for shared dependency-style entries. |
|
||||
| `apiVersion` | Plugin API version expected by the plugin. |
|
||||
| `compatibility.minimumTojuVersion` | Oldest app version the plugin supports. |
|
||||
|
||||
## Scope
|
||||
|
||||
`scope: "client"` installs the plugin for the current client. Omit `scope` for the same behavior.
|
||||
|
||||
`scope: "server"` marks a plugin as server-scoped. Server-scoped store entries can be installed to a chat server as requirements. Required server plugins are auto-installed for members when that server opens; optional requirements stay listed but do not auto-install.
|
||||
|
||||
## Entrypoint and Bundle
|
||||
|
||||
Use `entrypoint` for a browser-resolvable module relative to the manifest. Use `bundle.url` when publishing a cached browser bundle through a plugin source manifest. Desktop installs cache bundle files into app data and load the cached manifest afterward.
|
||||
|
||||
## Events
|
||||
|
||||
Every server or P2P plugin event should be declared before it is published or subscribed to.
|
||||
|
||||
```json
|
||||
{
|
||||
"events": [
|
||||
{
|
||||
"eventName": "poll:vote",
|
||||
"direction": "p2pHint",
|
||||
"scope": "channel",
|
||||
"maxPayloadBytes": 2048
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Data Declarations
|
||||
|
||||
Use `data` to document plugin-owned data keys and intended storage.
|
||||
|
||||
- `local` maps to client-local plugin data.
|
||||
- `serverData` maps to local per-user/per-server plugin data.
|
||||
|
||||
Signal server HTTP persistence for arbitrary plugin data is disabled by design.
|
||||
Reference in New Issue
Block a user