feat: plugins v1

This commit is contained in:
2026-04-29 01:14:14 +02:00
parent ec3802ade6
commit 6920f93b41
86 changed files with 9036 additions and 14 deletions

View File

@@ -0,0 +1,3 @@
# E2E All API Plugin
Fixture plugin for Playwright coverage. It calls every public Toju plugin API surface, registers UI contributions, writes storage, publishes events, creates plugin user data, and logs completion.

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="E2E plugin icon">
<rect width="64" height="64" rx="12" fill="#111827" />
<path d="M18 22h28v20H18z" fill="#38bdf8" />
<path d="M24 16h16v6H24zM24 42h16v6H24z" fill="#a7f3d0" />
<path d="M25 30h14v4H25z" fill="#111827" />
</svg>

After

Width:  |  Height:  |  Size: 319 B

View File

@@ -0,0 +1,273 @@
const tinyWave = 'data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEAESsAACJWAAACABAAZGF0YQAAAAA=';
const originalMessage = 'Plugin API original message';
const editedMessage = 'Plugin API edited message';
const deletedMessage = 'Plugin API deleted message';
const embedMessage = 'toju:embed:e2e.coverage:{"title":"Plugin API custom embed","body":"Rendered by plugin API"}';
const soundboardPlayedMessage = 'E2E soundboard played Airhorn to voice channel';
export async function activate(context) {
const api = context.api;
const currentUser = api.profile.getCurrent();
const shouldMutateChat = !currentUser?.displayName?.includes('Bob');
const pluginUserId = api.server.registerPluginUser({
displayName: 'E2E Plugin Bot',
id: 'e2e-plugin-bot'
});
context.subscriptions.push(api.ui.registerSettingsPage('coverage', {
label: 'E2E Coverage',
render: () => 'E2E settings contribution'
}));
context.subscriptions.push(api.ui.registerAppPage('coverage', {
label: 'E2E Page',
path: '/plugins/e2e/coverage',
render: () => 'E2E page contribution'
}));
context.subscriptions.push(api.ui.registerSidePanel('coverage', {
label: 'E2E Soundboard',
render: () => 'E2E soundboard ready'
}));
context.subscriptions.push(api.ui.registerChannelSection('coverage', {
label: 'E2E Soundboard',
type: 'custom'
}));
context.subscriptions.push(api.ui.registerComposerAction('coverage', {
icon: 'SFX',
label: 'E2E Soundboard',
run: () => openSoundboardModal(api, pluginUserId)
}));
context.subscriptions.push(api.ui.registerProfileAction('coverage', {
label: 'E2E Profile',
run: () => api.logger.info('profile action ran')
}));
context.subscriptions.push(api.ui.registerToolbarAction('coverage', {
label: 'E2E Toolbar',
run: () => api.logger.info('toolbar action ran')
}));
context.subscriptions.push(api.ui.registerEmbedRenderer('coverage', {
embedType: 'e2e.coverage',
render: (payload) => `E2E custom embed: ${payload?.title ?? 'missing title'}`
}));
const injectedBadge = document.createElement('div');
injectedBadge.dataset.testid = 'e2e-plugin-owned-dom';
injectedBadge.textContent = 'E2E plugin-owned DOM injected into chat';
injectedBadge.style.position = 'absolute';
injectedBadge.style.left = '1rem';
injectedBadge.style.bottom = '5.5rem';
injectedBadge.style.zIndex = '20';
injectedBadge.style.border = '1px solid hsl(var(--border))';
injectedBadge.style.borderRadius = '0.5rem';
injectedBadge.style.padding = '0.35rem 0.5rem';
injectedBadge.style.background = 'hsl(var(--card))';
injectedBadge.style.color = 'hsl(var(--foreground))';
injectedBadge.style.fontSize = '0.75rem';
context.subscriptions.push(api.ui.mountElement('chat-owned-badge', {
element: injectedBadge,
target: 'app-chat-messages'
}));
context.subscriptions.push(api.events.subscribeServer({ eventName: 'e2e:server', handler: () => {} }));
context.subscriptions.push(api.events.subscribeP2p({ eventName: 'e2e:p2p', handler: () => {} }));
api.storage.set('coverage', { ok: true });
api.storage.get('coverage');
await api.serverData.write('coverage', { ok: true });
await api.serverData.read('coverage');
api.profile.update({
description: 'Updated by E2E plugin',
displayName: `${currentUser?.displayName || 'E2E Plugin User'} Plugin Renamed`
});
api.profile.updateAvatar({
avatarHash: 'e2e-plugin-avatar',
avatarMime: 'image/svg+xml',
avatarUrl: '/plugins/e2e-all-api/icon.svg'
});
api.users.getCurrent();
api.users.list();
api.users.readMembers();
api.users.setRole(pluginUserId, 'member');
api.users.kick(pluginUserId);
api.users.ban(pluginUserId, 'E2E coverage');
api.roles.list();
api.roles.setAssignments([]);
api.channels.list();
api.channels.addAudioChannel({ id: 'e2e-audio', name: 'E2E Audio', position: 90 });
api.channels.addVideoChannel({ id: 'e2e-video', name: 'E2E Video', position: 91 });
api.channels.select('general');
api.channels.rename('e2e-audio', 'E2E Audio Renamed');
api.server.getCurrent();
api.server.updatePermissions({ allowVoice: true });
api.server.updateSettings({
name: api.server.getCurrent()?.name,
topic: 'Updated by E2E plugin'
});
api.messages.readCurrent();
if (shouldMutateChat) {
const sentMessage = api.messages.send(originalMessage);
api.messages.edit(sentMessage.id, editedMessage);
const removableMessage = api.messages.send(deletedMessage);
api.messages.delete(removableMessage.id);
api.messages.send(embedMessage);
}
api.messages.sendAsPluginUser({
content: 'Plugin bot message from all-api fixture',
pluginUserId
});
api.messages.moderateDelete('missing-message-id');
api.messages.sync(api.messages.readCurrent());
api.p2p.connectedPeers();
api.p2p.broadcastData('e2e:p2p', { ok: true });
api.p2p.sendData('missing-peer', 'e2e:p2p', { ok: true });
api.events.publishServer('e2e:server', { ok: true });
api.events.publishP2p('e2e:p2p', { ok: true });
api.media.setOutputVolume(0.8);
api.media.setInputVolume(0.8);
await api.media.playAudioClip({ url: tinyWave, volume: 0 }).catch((error) => api.logger.warn('audio clip rejected', String(error)));
await api.media.addCustomVideoStream({ label: 'e2e-video', stream: new MediaStream() });
const audioContext = new AudioContext();
const destination = audioContext.createMediaStreamDestination();
await api.media.addCustomAudioStream({ label: 'e2e-audio', stream: destination.stream }).catch((error) => api.logger.warn('audio stream rejected', String(error)));
await audioContext.close();
api.storage.remove('coverage');
await api.serverData.remove('coverage');
api.logger.info('all-api plugin completed');
}
export function ready(context) {
context.api.logger.info('all-api plugin ready');
}
export function deactivate(context) {
context.api.logger.info('all-api plugin deactivated');
}
function openSoundboardModal(api, pluginUserId) {
document.querySelector('[data-testid="e2e-soundboard-modal"]')?.remove();
const overlay = document.createElement('div');
overlay.dataset.testid = 'e2e-soundboard-modal';
overlay.setAttribute('role', 'dialog');
overlay.setAttribute('aria-modal', 'true');
overlay.setAttribute('aria-label', 'E2E Soundboard');
overlay.style.position = 'fixed';
overlay.style.inset = '0';
overlay.style.zIndex = '9999';
overlay.style.display = 'grid';
overlay.style.placeItems = 'center';
overlay.style.background = 'rgb(0 0 0 / 0.45)';
const panel = document.createElement('section');
panel.style.width = 'min(24rem, calc(100vw - 2rem))';
panel.style.border = '1px solid hsl(var(--border))';
panel.style.borderRadius = '0.5rem';
panel.style.padding = '1rem';
panel.style.color = 'hsl(var(--foreground))';
panel.style.background = 'hsl(var(--card))';
panel.style.boxShadow = '0 1.25rem 3rem rgb(0 0 0 / 0.25)';
const title = document.createElement('h2');
title.textContent = 'E2E Soundboard';
title.style.margin = '0 0 0.75rem';
title.style.fontSize = '1rem';
const status = document.createElement('p');
status.dataset.testid = 'e2e-soundboard-status';
status.textContent = 'Ready to play to voice channel';
status.style.margin = '0 0 1rem';
status.style.color = 'hsl(var(--muted-foreground))';
status.style.fontSize = '0.875rem';
const actions = document.createElement('div');
actions.style.display = 'flex';
actions.style.gap = '0.5rem';
actions.style.justifyContent = 'flex-end';
const closeButton = document.createElement('button');
closeButton.type = 'button';
closeButton.textContent = 'Close';
closeButton.style.border = '1px solid hsl(var(--border))';
closeButton.style.borderRadius = '0.375rem';
closeButton.style.padding = '0.5rem 0.75rem';
closeButton.style.background = 'transparent';
closeButton.style.color = 'hsl(var(--foreground))';
closeButton.addEventListener('click', () => overlay.remove());
const playButton = document.createElement('button');
playButton.type = 'button';
playButton.textContent = 'Play airhorn to voice';
playButton.style.border = '0';
playButton.style.borderRadius = '0.375rem';
playButton.style.padding = '0.5rem 0.75rem';
playButton.style.background = 'hsl(var(--primary))';
playButton.style.color = 'hsl(var(--primary-foreground))';
playButton.addEventListener('click', async () => {
playButton.disabled = true;
status.textContent = 'Playing Airhorn to voice channel';
try {
await playSoundboardClipToVoice(api);
api.p2p.broadcastData('e2e:p2p', { sound: 'airhorn', source: 'soundboard' });
api.events.publishP2p('e2e:p2p', { sound: 'airhorn', source: 'soundboard' });
api.messages.sendAsPluginUser({ content: soundboardPlayedMessage, pluginUserId });
api.logger.info('soundboard played to voice channel');
status.textContent = soundboardPlayedMessage;
} catch (error) {
status.textContent = error instanceof Error ? error.message : 'Soundboard playback failed';
api.logger.warn('soundboard playback failed', String(error));
} finally {
playButton.disabled = false;
}
});
actions.append(closeButton, playButton);
panel.append(title, status, actions);
overlay.append(panel);
api.ui.mountElement('soundboard-modal', {
element: overlay,
target: 'body'
});
}
async function playSoundboardClipToVoice(api) {
const audioContext = new AudioContext();
const oscillator = audioContext.createOscillator();
const gain = audioContext.createGain();
const destination = audioContext.createMediaStreamDestination();
oscillator.type = 'square';
oscillator.frequency.value = 330;
gain.gain.value = 0.08;
oscillator.connect(gain);
gain.connect(destination);
oscillator.start();
await api.media.addCustomAudioStream({ label: 'e2e-soundboard-airhorn', stream: destination.stream });
await api.media.playAudioClip({ url: tinyWave, volume: 0 }).catch((error) => api.logger.warn('soundboard preview rejected', String(error)));
await new Promise((resolve) => setTimeout(resolve, 150));
oscillator.stop();
await audioContext.close();
}

View File

@@ -0,0 +1,99 @@
{
"schemaVersion": 1,
"id": "e2e.all-api-plugin",
"title": "E2E All API Plugin",
"description": "Calls every public Toju plugin API surface for user-facing Playwright coverage.",
"version": "1.0.0",
"kind": "client",
"apiVersion": "1.0.0",
"compatibility": {
"minimumTojuVersion": "1.0.0",
"verifiedTojuVersion": "1.0.0"
},
"entrypoint": "./main.js",
"authors": [
{
"name": "MetoYou Tests",
"url": "https://git.azaaxin.com/myxelium/Toju"
}
],
"homepage": "https://git.azaaxin.com/myxelium/Toju",
"readme": "./README.md",
"capabilities": [
"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"
],
"events": [
{
"eventName": "e2e:server",
"direction": "serverRelay",
"scope": "server",
"maxPayloadBytes": 2048
},
{
"eventName": "e2e:p2p",
"direction": "p2pHint",
"scope": "user",
"maxPayloadBytes": 2048
}
],
"data": [
{
"key": "coverage",
"scope": "server",
"storage": "serverData"
}
],
"settings": {
"type": "object",
"properties": {
"enabled": {
"type": "boolean",
"default": true
}
}
},
"ui": {
"settingsPages": ["coverage"],
"sidePanels": ["coverage"],
"channelSections": ["coverage"]
},
"pluginUser": {
"displayName": "E2E Plugin Bot",
"label": "All API fixture"
}
}

View File

@@ -0,0 +1,17 @@
{
"title": "MetoYou E2E Plugin Source",
"plugins": [
{
"id": "e2e.all-api-plugin",
"title": "E2E All API Plugin",
"description": "Test plugin that calls every public Toju plugin API surface.",
"version": "1.0.0",
"author": "MetoYou Tests",
"image": "./e2e-all-api/icon.svg",
"github": "https://git.azaaxin.com/myxelium/Toju",
"homepage": "https://git.azaaxin.com/myxelium/Toju",
"install": "./e2e-all-api/toju.plugin.json",
"readme": "./e2e-all-api/README.md"
}
]
}