Files
Toju/docs-site/docs/plugin-development/examples.md
2026-06-05 17:12:26 +02:00

6.7 KiB

sidebar_position
sidebar_position
5

Examples

Toolbar Message Plugin

toju-plugin.json

{
  "schemaVersion": 1,
  "id": "example.toolbar-message",
  "title": "Toolbar Message",
  "description": "Adds a View plugins menu 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

export function activate(context) {
  const { api } = context;

  context.subscriptions.push(
    api.ui.registerToolbarAction('standup-message', {
      icon: 'ST',
      label: 'Standup',
      run: (actionContext) => api.messages.send('Standup: yesterday, today, blocked', actionContext.textChannel?.id)
    })
  );
}

The action appears as a tile in the server side panel's View plugins menu and runs with source: 'toolbarAction'.

Slash Command Plugin

toju-plugin.json

{
  "schemaVersion": 1,
  "id": "example.slash-commands",
  "title": "Slash Commands",
  "description": "Registers / commands available from the chat composer.",
  "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.commands"]
}

main.js

export function activate(context) {
  const { api } = context;

  // Global: works in chat servers and direct messages.
  context.subscriptions.push(
    api.commands.register('shrug', {
      name: 'shrug',
      description: 'Append the shrug emoticon',
      scope: 'global',
      run: () => api.messages.send('¯\\_(ツ)_/¯')
    })
  );

  // Server-scoped: only offered while a chat server is active.
  context.subscriptions.push(
    api.commands.register('announce', {
      name: 'announce',
      description: 'Post an announcement to the current channel',
      icon: '📢',
      scope: 'server',
      options: [{ name: 'message', type: 'rest', required: true }],
      run: (slash) => api.messages.send(`📢 ${slash.args.message}`, slash.textChannel?.id)
    })
  );
}

Typing / in the composer opens the autocomplete menu. /shrug runs immediately; /announce <message> fills the composer so the user can type the announcement before sending. See the Slash Commands API for option parsing and the command context.

Settings Page Plugin

{
  "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 }
    }
  }
}
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.

{
  "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"
  }
}
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

{
  "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"]
}
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.

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.