Tonight: dungeon practice.
'; - return root; - } - })); + context.subscriptions.push( + context.api.ui.registerAppPage('dashboard', { + label: 'Raid Dashboard', + path: '/plugins/example.raid-helper/dashboard', + render: () => { + const root = document.createElement('section'); + root.innerHTML = 'Tonight: dungeon practice.
'; + return root; + } + }) + ); } ``` @@ -46,22 +48,24 @@ The page is hosted by `/plugins/:pluginId/:pageId`. ```js export function activate(context) { - context.subscriptions.push(context.api.ui.registerSettingsPage('preferences', { - label: 'Raid Helper', - settingsKey: 'raid-helper', - order: 20, - render: () => { - const wrapper = document.createElement('section'); - const label = document.createElement('label'); - const checkbox = document.createElement('input'); + context.subscriptions.push( + context.api.ui.registerSettingsPage('preferences', { + label: 'Raid Helper', + settingsKey: 'raid-helper', + order: 20, + render: () => { + const wrapper = document.createElement('section'); + const label = document.createElement('label'); + const checkbox = document.createElement('input'); - checkbox.type = 'checkbox'; - checkbox.checked = true; - label.append(checkbox, ' Enable ready-check reminders'); - wrapper.append(label); - return wrapper; - } - })); + checkbox.type = 'checkbox'; + checkbox.checked = true; + label.append(checkbox, ' Enable ready-check reminders'); + wrapper.append(label); + return wrapper; + } + }) + ); } ``` @@ -71,23 +75,26 @@ Use `ui.registerSidePanel` for content that belongs in the server sidebar plugin ```js export function activate(context) { - context.subscriptions.push(context.api.ui.registerSidePanel('soundboard', { - label: 'Soundboard', - order: 10, - render: () => { - const panel = document.createElement('div'); - const button = document.createElement('button'); + context.subscriptions.push( + context.api.ui.registerSidePanel('soundboard', { + label: 'Soundboard', + order: 10, + render: () => { + const panel = document.createElement('div'); + const button = document.createElement('button'); - button.type = 'button'; - button.textContent = 'Play chime'; - button.onclick = () => context.api.media.playAudioClip({ - url: 'https://cdn.example.com/chime.wav', - volume: 0.6 - }); - panel.append(button); - return panel; - } - })); + button.type = 'button'; + button.textContent = 'Play chime'; + button.onclick = () => + context.api.media.playAudioClip({ + url: 'https://cdn.example.com/chime.wav', + volume: 0.6 + }); + panel.append(button); + return panel; + } + }) + ); } ``` @@ -97,11 +104,13 @@ Capabilities required: `ui.sidePanel` and `media.playAudio`. ```js export function activate(context) { - context.subscriptions.push(context.api.ui.registerChannelSection('events', { - label: 'Event Rooms', - type: 'custom', - order: 50 - })); + context.subscriptions.push( + context.api.ui.registerChannelSection('events', { + label: 'Event Rooms', + type: 'custom', + order: 50 + }) + ); } ``` @@ -109,16 +118,15 @@ export function activate(context) { ```js export function activate(context) { - context.subscriptions.push(context.api.ui.registerComposerAction('insert-standup', { - icon: 'ST', - label: 'Insert standup prompt', - run: (actionContext) => { - context.api.messages.send( - 'Standup: yesterday I..., today I..., blocked by...', - actionContext.textChannel?.id - ); - } - })); + context.subscriptions.push( + context.api.ui.registerComposerAction('insert-standup', { + icon: 'ST', + label: 'Insert standup prompt', + run: (actionContext) => { + context.api.messages.send('Standup: yesterday I..., today I..., blocked by...', actionContext.textChannel?.id); + } + }) + ); } ``` @@ -128,45 +136,65 @@ Capabilities required: `ui.pages` and `messages.send`. ```js export function activate(context) { - context.subscriptions.push(context.api.ui.registerProfileAction('wave', { - label: 'Wave', - run: (actionContext) => { - context.api.messages.send(`Waving at ${actionContext.user?.displayName ?? 'someone'}!`); - } - })); + context.subscriptions.push( + context.api.ui.registerProfileAction('wave', { + label: 'Wave', + run: (actionContext) => { + context.api.messages.send(`Waving at ${actionContext.user?.displayName ?? 'someone'}!`); + } + }) + ); } ``` ## Toolbar Action +Toolbar actions are command-style plugin entries shown in the server side panel's View plugins menu. Use them for small actions that should be easy to launch from a server, such as opening a plugin page, sending a status message, starting a timer, or toggling a plugin feature. + +The View plugins link appears in `[data-testid="plugin-room-side-panel"]` when the plugin side-panel area is rendered. Opening it shows an overlay menu, positioned like profile-card overlays, with registered actions laid out as plugin icon tiles. The `icon` field can be short text such as `RH`, an emoji, or an image URL; when omitted, MetoYou falls back to initials from the plugin/action labels. + +Toolbar action callbacks receive an action context with `source: 'toolbarAction'`, the current user, current server, active text channel, and current voice channel when available. + ```js export function activate(context) { - context.subscriptions.push(context.api.ui.registerToolbarAction('open-dashboard', { - label: 'Raid Helper', - run: () => { - context.api.logger.info('Open the Raid Helper plugin page from /plugins/example.raid-helper/dashboard'); - } - })); + context.subscriptions.push( + context.api.ui.registerToolbarAction('open-dashboard', { + icon: 'RH', + label: 'Raid Helper', + run: (actionContext) => { + context.api.logger.info('Raid Helper opened', { + channelId: actionContext.textChannel?.id, + serverId: actionContext.server?.id + }); + } + }) + ); } ``` +Capabilities required: `ui.pages`. Add any capability your action uses, such as `messages.send` or `server.read`. + +Use `registerSidePanel` instead when the plugin needs persistent sidebar content, and use `registerAppPage` when the plugin needs a full-page workflow. + ## Embed Renderer ```js export function activate(context) { - context.subscriptions.push(context.api.ui.registerEmbedRenderer('raid-card', { - embedType: 'raid.card', - render: (payload) => { - const card = document.createElement('article'); - const title = document.createElement('h3'); - const body = document.createElement('p'); + context.subscriptions.push( + context.api.ui.registerEmbedRenderer('raid-card', { + embedType: 'raid.card', + render: (payload) => { + const card = document.createElement('article'); + const title = document.createElement('h3'); + const body = document.createElement('p'); - title.textContent = payload?.title ?? 'Raid'; - body.textContent = payload?.description ?? 'No description provided.'; - card.append(title, body); - return card; - } - })); + title.textContent = payload?.title ?? 'Raid'; + body.textContent = payload?.description ?? 'No description provided.'; + card.append(title, body); + return card; + } + }) + ); } ``` @@ -202,11 +230,13 @@ export function activate(context) { badge.style.color = 'white'; badge.style.borderRadius = '6px'; - context.subscriptions.push(context.api.ui.mountElement('active-badge', { - target: 'body', - position: 'beforeend', - element: badge - })); + context.subscriptions.push( + context.api.ui.mountElement('active-badge', { + target: 'body', + position: 'beforeend', + element: badge + }) + ); } ``` @@ -224,12 +254,14 @@ export function activate(context) { const banner = document.createElement('div'); banner.textContent = 'Raid helper active in this chat.'; - context.subscriptions.push(context.api.ui.mountElement('chat-banner', { - target, - position: 'afterbegin', - element: banner - })); + context.subscriptions.push( + context.api.ui.mountElement('chat-banner', { + target, + position: 'afterbegin', + element: banner + }) + ); } ``` -The runtime tags plugin-owned DOM and removes it on unload, but plugins should still keep mounts minimal and accessible. \ No newline at end of file +The runtime tags plugin-owned DOM and removes it on unload, but plugins should still keep mounts minimal and accessible. diff --git a/docs-site/docs/plugin-development/capabilities.md b/docs-site/docs/plugin-development/capabilities.md index b723757..85fec4a 100644 --- a/docs-site/docs/plugin-development/capabilities.md +++ b/docs-site/docs/plugin-development/capabilities.md @@ -6,44 +6,44 @@ sidebar_position: 3 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. | +| 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 action entry points, including View plugins menu 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 diff --git a/docs-site/docs/plugin-development/create-a-plugin.md b/docs-site/docs/plugin-development/create-a-plugin.md index d881012..1705b93 100644 --- a/docs-site/docs/plugin-development/create-a-plugin.md +++ b/docs-site/docs/plugin-development/create-a-plugin.md @@ -27,7 +27,7 @@ The manifest file can be named `toju-plugin.json` or `plugin.json`. Entrypoints "schemaVersion": 1, "id": "example.hello-world", "title": "Hello World", - "description": "Adds a toolbar action that sends a message.", + "description": "Adds a View plugins menu action that sends a message.", "version": "1.0.0", "kind": "client", "scope": "client", @@ -49,6 +49,7 @@ export function activate(context) { api.logger.info('Hello World activated'); const disposable = api.ui.registerToolbarAction('hello', { + icon: 'HI', label: 'Hello', run: () => api.messages.send('Hello from my plugin') }); @@ -65,15 +66,17 @@ export function deactivate(context) { } ``` +`registerToolbarAction()` adds an action tile to the server side panel's View plugins menu. Use `icon` for the tile badge and keep the `label` short enough to scan in a grid. + ## 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. | +| 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 diff --git a/docs-site/docs/plugin-development/examples.md b/docs-site/docs/plugin-development/examples.md index ec93a08..78b6ac7 100644 --- a/docs-site/docs/plugin-development/examples.md +++ b/docs-site/docs/plugin-development/examples.md @@ -13,7 +13,7 @@ sidebar_position: 5 "schemaVersion": 1, "id": "example.toolbar-message", "title": "Toolbar Message", - "description": "Adds a toolbar action that sends a reusable message.", + "description": "Adds a View plugins menu action that sends a reusable message.", "version": "1.0.0", "kind": "client", "scope": "client", @@ -33,13 +33,18 @@ sidebar_position: 5 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') - })); + 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'`. + ## Settings Page Plugin ```json @@ -67,19 +72,21 @@ export function activate(context) { 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'); + 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; - } - })); + button.type = 'button'; + button.textContent = 'Remember preference'; + button.onclick = () => api.storage.set('enabled', true); + root.append(button); + return root; + } + }) + ); } ``` @@ -99,13 +106,7 @@ A server-scoped plugin can be installed as a server requirement and auto-install "apiVersion": "1.0.0", "compatibility": { "minimumTojuVersion": "1.0.0" }, "entrypoint": "./main.js", - "capabilities": [ - "server.read", - "users.manage", - "ui.sidePanel", - "media.playAudio", - "messages.send" - ], + "capabilities": ["server.read", "users.manage", "ui.sidePanel", "media.playAudio", "messages.send"], "pluginUser": { "displayName": "Soundboard", "label": "Audio helper" @@ -121,23 +122,25 @@ export function activate(context) { displayName: 'Soundboard' }); - context.subscriptions.push(api.ui.registerSidePanel('sounds', { - label: 'Soundboard', - render: () => { - const panel = document.createElement('div'); - const button = document.createElement('button'); + 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' }); - }; + 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; - } - })); + panel.append(button); + return panel; + } + }) + ); } ``` @@ -162,12 +165,14 @@ export function activate(context) { 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) - })); + 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', @@ -192,10 +197,12 @@ export function activate(context) { badge.style.right = '1rem'; badge.style.bottom = '1rem'; - context.subscriptions.push(context.api.ui.mountElement('active-badge', { - target: 'body', - element: badge - })); + context.subscriptions.push( + context.api.ui.mountElement('active-badge', { + target: 'body', + element: badge + }) + ); } ``` diff --git a/docs-site/docs/user-guide/plugins.md b/docs-site/docs/user-guide/plugins.md index 21b2f2a..66db568 100644 --- a/docs-site/docs/user-guide/plugins.md +++ b/docs-site/docs/user-guide/plugins.md @@ -8,11 +8,11 @@ Plugins add features to MetoYou. They can add pages, buttons, panels, settings, ## Types of Plugins -| Type | What it means | -| --- | --- | -| Client plugin | Installed for your app. It follows you across servers when active. | -| Server plugin | Installed for a specific server. It may be required, recommended, optional, blocked, or incompatible. | -| Library plugin | Shared plugin code used by other plugins. It is not normally something users interact with directly. | +| Type | What it means | +| -------------- | ----------------------------------------------------------------------------------------------------- | +| Client plugin | Installed for your app. It follows you across servers when active. | +| Server plugin | Installed for a specific server. It may be required, recommended, optional, blocked, or incompatible. | +| Library plugin | Shared plugin code used by other plugins. It is not normally something users interact with directly. | ## Install from the Plugin Store @@ -26,6 +26,10 @@ Plugins add features to MetoYou. They can add pages, buttons, panels, settings, Server-scoped plugins installed to the server you are currently viewing are enabled and activated automatically after install, so their panels, actions, or embeds can appear immediately. +## Use Plugin Actions + +When plugins add quick actions to a server, the server side panel shows a View plugins link in the plugin area. Open it to see a grid of plugin action tiles. Selecting a tile runs that plugin's action in the current server and channel context. + ## Install a Local Plugin Desktop builds can discover local plugin folders from the app data plugins directory. @@ -40,12 +44,12 @@ Desktop builds can discover local plugin folders from the app data plugins direc When a server uses plugins, MetoYou may show a prompt. -| Status | Meaning | -| --- | --- | -| Required | You must install the plugin to join or continue using that server. | -| Recommended | The server suggests the plugin, but you can choose. | -| Optional | The plugin is available for the server, but not required. | -| Blocked | The server marks the plugin as not allowed. | +| Status | Meaning | +| ------------ | --------------------------------------------------------------------------------- | +| Required | You must install the plugin to join or continue using that server. | +| Recommended | The server suggests the plugin, but you can choose. | +| Optional | The plugin is available for the server, but not required. | +| Blocked | The server marks the plugin as not allowed. | | Incompatible | The plugin version does not work with your app version or the server requirement. | Required plugins are still installed locally on your device. The signaling server stores requirement metadata only; it does not run plugin code. @@ -56,13 +60,13 @@ Plugins must ask for capabilities before using sensitive features. Examples: -| Capability area | Why a plugin might ask | -| --- | --- | -| Messages | Send messages, read current messages, moderate messages, or render embeds. | -| Users and roles | Read member lists, create plugin users, or manage users. | -| Voice and media | Play audio, add an audio stream, add a video stream, or adjust volume. | -| UI | Add pages, settings pages, side panels, toolbar buttons, or DOM elements. | -| Storage | Save plugin preferences locally or per server. | +| Capability area | Why a plugin might ask | +| --------------- | -------------------------------------------------------------------------- | +| Messages | Send messages, read current messages, moderate messages, or render embeds. | +| Users and roles | Read member lists, create plugin users, or manage users. | +| Voice and media | Play audio, add an audio stream, add a video stream, or adjust volume. | +| UI | Add pages, settings pages, side panels, toolbar buttons, or DOM elements. | +| Storage | Save plugin preferences locally or per server. | Only grant capabilities to plugins you trust. @@ -79,4 +83,4 @@ The Plugin Manager lets you: ## Plugin Safety Notes -Plugins are browser-safe JavaScript modules loaded by the client. They do not run on the signaling server. A plugin can only call privileged MetoYou APIs when its manifest declares the capability and you grant it. \ No newline at end of file +Plugins are browser-safe JavaScript modules loaded by the client. They do not run on the signaling server. A plugin can only call privileged MetoYou APIs when its manifest declares the capability and you grant it. diff --git a/server/data/metoyou.sqlite b/server/data/metoyou.sqlite index cf301ec..ea76557 100644 Binary files a/server/data/metoyou.sqlite and b/server/data/metoyou.sqlite differ diff --git a/toju-app/src/app/domains/access-control/domain/rules/role-assignment.rules.spec.ts b/toju-app/src/app/domains/access-control/domain/rules/role-assignment.rules.spec.ts new file mode 100644 index 0000000..9f866d7 --- /dev/null +++ b/toju-app/src/app/domains/access-control/domain/rules/role-assignment.rules.spec.ts @@ -0,0 +1,49 @@ +import type { Room } from '../../../../shared-kernel'; +import { SYSTEM_ROLE_IDS } from '../constants/access-control.constants'; +import { normalizeRoomAccessControl } from './room.rules'; + +function buildRoom(overrides: PartialPlugins
+{{ actions().length }} available actions
++ No plugin actions available. +
+ } +