From a4429fe9c2b93769f3f91229913f581133306fd4 Mon Sep 17 00:00:00 2001 From: Max Litruv Boonzaayer Date: Sat, 18 Apr 2026 02:55:38 +1000 Subject: [PATCH] feat: Add comprehensive plugin API documentation and example plugin - Introduced PLUGIN_API.md for detailed plugin development guidelines. - Implemented PLUGIN_SYSTEM_IMPLEMENTATION.md summarizing all requested features. - Created example-plugin showcasing all API features including commands, message interceptors, settings, and notifications. - Added plugin-metadata.json for the example plugin with relevant metadata. --- PLUGINS.md | 156 ++++ PLUGIN_API.md | 1188 +++++++++++++++++++++++++++ PLUGIN_SYSTEM_IMPLEMENTATION.md | 199 +++++ electron/main.js | 182 ++++ electron/preload.js | 9 + example-plugin/index.js | 161 ++++ example-plugin/plugin-metadata.json | 9 + package-lock.json | 14 +- package.json | 1 + 9 files changed, 1917 insertions(+), 2 deletions(-) create mode 100644 PLUGINS.md create mode 100644 PLUGIN_API.md create mode 100644 PLUGIN_SYSTEM_IMPLEMENTATION.md create mode 100644 example-plugin/index.js create mode 100644 example-plugin/plugin-metadata.json diff --git a/PLUGINS.md b/PLUGINS.md new file mode 100644 index 0000000..e92b017 --- /dev/null +++ b/PLUGINS.md @@ -0,0 +1,156 @@ +# Paarrot Plugin System + +The Paarrot desktop client includes a powerful plugin system that allows you to extend and customize your Matrix experience. + +## What are Plugins? + +Plugins are small JavaScript modules that run inside Paarrot and can: +- Add custom commands +- Modify the UI +- React to Matrix events +- Add new settings sections +- Enhance functionality + +## Installing Plugins + +### From the Plugin Marketplace + +1. Open **Settings** → **Plugins** +2. Browse the **Marketplace** tab +3. Click **Install** on any plugin you want to add +4. The plugin will be downloaded and enabled automatically + +### Manual Installation + +1. Download a plugin ZIP file +2. Extract it to your plugins directory: + - **Windows**: `%APPDATA%\paarrot\plugins\` + - **Linux**: `~/.config/Paarrot/plugins/` + - **macOS**: `~/Library/Application Support/Paarrot/plugins/` +3. Restart Paarrot +4. Enable the plugin in **Settings** → **Plugins** → **Installed** + +## Managing Plugins + +### Viewing Installed Plugins + +Go to **Settings** → **Plugins** → **Installed** to see all installed plugins. + +### Enabling/Disabling Plugins + +Click the toggle switch next to any plugin to enable or disable it. Disabled plugins won't run or use resources. + +### Uninstalling Plugins + +Click the **Uninstall** button next to any plugin to remove it completely. + +### Searching Plugins + +Use the search bar to filter plugins by name, description, author, or tags. + +## Plugin Security + +⚠️ **Important Security Information** + +Plugins run with access to your Matrix client and can: +- Read your messages +- Send messages on your behalf +- Access your room list +- Modify app behavior + +**Only install plugins from trusted sources!** + +### Safety Tips + +1. **Check the author**: Install plugins from known developers +2. **Read reviews**: Look for community feedback +3. **Review permissions**: Understand what the plugin can do +4. **Keep updated**: Update plugins regularly for security fixes +5. **Report issues**: If a plugin behaves suspiciously, report it immediately + +## Developing Plugins + +Want to create your own plugin? Check out the [Plugin Development Guide](./cinny/src/app/features/settings/plugins/PLUGIN_DEVELOPMENT.md). + +### Quick Start + +1. Create a plugin directory with: + - `index.js` - Your plugin code + - `plugin-metadata.json` - Plugin information + +2. Implement the plugin interface: +```javascript +module.exports = { + onLoad: async (context) => { + // Your plugin initialization + console.log('Plugin loaded!'); + }, + onUnload: async () => { + // Cleanup when disabled + } +}; +``` + +3. Test locally by copying to your plugins directory + +4. Publish to the [Plugin Directory](https://github.com/Paarrot/Plugin-Directory) + +## Plugin API + +Plugins have access to: +- **Matrix Client**: Full matrix-js-sdk client instance +- **React**: For building UI components +- **Commands**: Register custom slash commands +- **UI Hooks**: Inject UI elements +- **Settings**: Add custom settings sections +- **Utilities**: Notifications and helpers + +See the [Plugin Development Guide](./cinny/src/app/features/settings/plugins/PLUGIN_DEVELOPMENT.md) for complete API documentation. + +## Example Plugins + +Check out the `example-plugin/` directory for a simple example demonstrating: +- Matrix client access +- Event listeners +- Custom commands +- Notifications + +## Troubleshooting + +### Plugin Won't Load + +1. Check the browser console for errors (F12 → Console) +2. Look for `[PluginLoader]` messages +3. Verify `index.js` exists in the plugin directory +4. Ensure `plugin-metadata.json` is valid JSON + +### Plugin Crashes + +1. Disable the plugin in Settings → Plugins +2. Check console for error stack traces +3. Report the issue to the plugin author +4. Try reinstalling the plugin + +### Performance Issues + +If Paarrot becomes slow: +1. Disable plugins one by one to identify the culprit +2. Check for memory leaks in the console +3. Contact the plugin author with details + +## Plugin Directory + +The official Paarrot Plugin Directory is maintained at: +https://github.com/Paarrot/Plugin-Directory + +Submit your plugins there to make them available in the marketplace! + +## Support + +- **Documentation**: [Plugin Development Guide](./cinny/src/app/features/settings/plugins/PLUGIN_DEVELOPMENT.md) +- **Issues**: https://github.com/Paarrot/cinny-desktop/issues +- **Community**: Join our Matrix room for plugin development help + +## License + +Plugins are independent works and have their own licenses. Check each plugin's metadata for license information. diff --git a/PLUGIN_API.md b/PLUGIN_API.md new file mode 100644 index 0000000..5cc66d1 --- /dev/null +++ b/PLUGIN_API.md @@ -0,0 +1,1188 @@ +# Paarrot Plugin API Documentation + +Complete reference for developing plugins for Paarrot/Cinny Desktop. + +## Table of Contents + +- [Getting Started](#getting-started) +- [Plugin Structure](#plugin-structure) +- [Plugin Context API](#plugin-context-api) + - [Commands](#commands) + - [Message Interceptors](#message-interceptors) + - [Custom Renderers](#custom-renderers) + - [Settings](#settings) + - [Themes](#themes) + - [Matrix Events](#matrix-events) + - [Background Tasks](#background-tasks) + - [Notifications](#notifications) + - [Logging](#logging) + - [Plugin Exports](#plugin-exports) + - [Matrix Client](#matrix-client) + +--- + +## Getting Started + +### Plugin Location + +Plugins are installed in: +- **Windows**: `%APPDATA%\paarrot\plugins\\` +- **Linux**: `~/.config/Paarrot/plugins//` +- **macOS**: `~/Library/Application Support/Paarrot/plugins//` + +### Required Files + +- `index.js` - Main plugin file (required) +- `plugin-metadata.json` - Plugin metadata (required) +- `README.md` - Documentation (recommended) + +### plugin-metadata.json + +```json +{ + "name": "My Awesome Plugin", + "version": "1.0.0", + "description": "Does something cool", + "author": "Your Name", + "homepage": "https://github.com/username/plugin", + "thumbnail": "https://example.com/icon.png", + "tags": ["utility", "messaging", "fun"] +} +``` + +--- + +## Plugin Structure + +### Basic Plugin + +```javascript +module.exports = { + name: "My Plugin", + version: "1.0.0", + dependencies: { + // "other-plugin": "^1.0.0" + }, + + /** + * Called when plugin loads + * @param {PluginContext} ctx - Plugin context + */ + onLoad: async (ctx) => { + ctx.log('Plugin loaded!'); + + // Initialize your plugin here + ctx.commands.register({ + name: "hello", + description: "Say hello", + run: () => "Hello, world!" + }); + }, + + /** + * Called when plugin unloads + */ + onUnload: async () => { + console.log('Plugin unloaded'); + }, + + /** + * API for other plugins + */ + exports: null +}; +``` + +--- + +## Plugin Context API + +The `PluginContext` provides access to all plugin APIs: + +```typescript +interface PluginContext { + pluginId: string; + matrixClient: MatrixClient; + React: typeof import('react'); + + commands: CommandsAPI; + messages: MessagesAPI; + ui: UIAPI; + settings: SettingsAPI; + matrix: MatrixEventsAPI; + timers: TimersAPI; + notify: NotificationFunction; + log: LogFunction; + warn: LogFunction; + error: LogFunction; + require: RequireFunction; +} +``` + +### Commands + +Register slash commands that users can type in chat. + +#### Register a Command + +```javascript +ctx.commands.register({ + name: "mycommand", + description: "Does something cool", + args: ["arg1", "arg2"], // Optional + run: (args) => { + // Return string to send as message + return `Got args: ${args.arg1}, ${args.arg2}`; + } +}); +``` + +#### Examples + +**Simple command:** +```javascript +ctx.commands.register({ + name: "shrug", + description: "Send a shrug emoji", + run: () => "¯\\_(ツ)_/¯" +}); +// Usage: /shrug +``` + +**Command with arguments:** +```javascript +ctx.commands.register({ + name: "greet", + description: "Greet someone", + args: ["name"], + run: ({ name }) => `Hello, ${name || 'stranger'}!` +}); +// Usage: /greet Alice +``` + +**Advanced command:** +```javascript +ctx.commands.register({ + name: "calc", + description: "Simple calculator", + args: ["expression"], + run: ({ expression }) => { + try { + const sanitized = expression.replace(/[^0-9+\-*/().\s]/g, ''); + const result = Function(`'use strict'; return (${sanitized})`)(); + return `${expression} = ${result}`; + } catch (err) { + return "Invalid expression"; + } + } +}); +// Usage: /calc 2 + 2 * 3 +``` + +#### API Reference + +```typescript +interface CommandsAPI { + register(command: PluginCommand): void; + unregister(name: string): void; + execute(name: string, args: Record): Promise; +} + +interface PluginCommand { + name: string; + description: string; + args?: string[]; + run: (args: Record) => string | void | Promise; +} +``` + +--- + +### Message Interceptors + +Intercept and modify messages before they're sent or after they're received. + +#### Before Send + +Modify outgoing messages before they're sent to the server. + +```javascript +ctx.messages.onBeforeSend((msg) => { + // Modify msg.content + if (msg.content.includes('secret')) { + msg.content = msg.content.replace(/secret/g, '█████'); + } + + // Add prefix + msg.content = `[Bot] ${msg.content}`; +}); +``` + +#### On Receive + +Process incoming messages (read-only monitoring). + +```javascript +ctx.messages.onReceive((msg) => { + // Monitor for keywords + if (msg.content.includes('important')) { + ctx.notify({ + title: 'Important Message', + body: msg.content, + type: 'info' + }); + } +}); +``` + +#### Real-World Examples + +**Auto-expand abbreviations:** +```javascript +ctx.messages.onBeforeSend((msg) => { + const expansions = { + 'brb': 'be right back', + 'omw': 'on my way', + 'gtg': 'got to go', + 'afk': 'away from keyboard' + }; + + Object.entries(expansions).forEach(([abbr, full]) => { + const regex = new RegExp(`\\b${abbr}\\b`, 'gi'); + msg.content = msg.content.replace(regex, full); + }); +}); +``` + +**Add auto-shrug to questions:** +```javascript +ctx.messages.onBeforeSend((msg) => { + if (msg.content.trim().endsWith('?')) { + msg.content += ' ¯\\_(ツ)_/¯'; + } +}); +``` + +**Track mentions:** +```javascript +ctx.messages.onReceive((msg) => { + const userId = ctx.matrixClient.getUserId(); + if (msg.content.includes(userId)) { + ctx.log('You were mentioned!'); + ctx.notify('Someone mentioned you!'); + } +}); +``` + +#### API Reference + +```typescript +interface MessagesAPI { + onBeforeSend(handler: (msg: Message) => void): void; + onReceive(handler: (msg: Message) => void): void; +} + +interface Message { + content: string; + roomId?: string; + sender?: string; + eventId?: string; +} +``` + +--- + +### Custom Renderers + +Register custom UI renderers for messages, images, and other content types. + +```javascript +// Custom message renderer +ctx.ui.registerRenderer("message", (msg, defaultRenderer) => { + // Custom logic + if (msg.sender?.includes('bot')) { + // Return custom React component + return ctx.React.createElement('div', { + style: { background: 'yellow' } + }, msg.content); + } + + // Fall back to default + return defaultRenderer?.(); +}); + +// Custom image renderer +ctx.ui.registerRenderer("image", (data, defaultRenderer) => { + // Add custom effects, borders, etc. + return defaultRenderer?.(); +}); +``` + +#### API Reference + +```typescript +interface UIAPI { + registerRenderer( + type: string, + renderer: (data: any, defaultRenderer?: () => ReactNode) => ReactNode + ): void; +} + +type CustomRenderer = ( + data: any, + defaultRenderer?: () => ReactNode +) => ReactNode; +``` + +--- + +### Settings + +Define settings that users can configure in the UI. + +#### Define Settings Schema + +```javascript +ctx.settings.define({ + // Boolean toggle + enableFeature: { + type: "boolean", + label: "Enable Feature", + description: "Toggle this feature on/off", + default: true + }, + + // String input + apiKey: { + type: "string", + label: "API Key", + description: "Your API key", + default: "" + }, + + // Number input + maxRetries: { + type: "number", + label: "Max Retries", + description: "Maximum retry attempts", + default: 3 + }, + + // Select dropdown + theme: { + type: "select", + label: "Theme", + description: "Choose a theme", + options: [ + { value: "dark", label: "Dark Mode" }, + { value: "light", label: "Light Mode" } + ], + default: "dark" + }, + + // Color picker + accentColor: { + type: "color", + label: "Accent Color", + description: "Choose your accent color", + default: "#6366f1" + } +}); +``` + +#### Get/Set Settings + +```javascript +// Get a setting +const theme = ctx.settings.get('theme'); +const enabled = ctx.settings.get('enableFeature'); + +// Set a setting (usually done by user in UI) +ctx.settings.set('theme', 'dark'); +``` + +#### API Reference + +```typescript +interface SettingsAPI { + define(schema: SettingsSchema): void; + get(key: string): T | undefined; + set(key: string, value: any): void; +} + +interface SettingDefinition { + type: 'string' | 'number' | 'boolean' | 'select' | 'color'; + label?: string; + description?: string; + default?: any; + options?: Array<{ value: any; label: string }>; +} + +type SettingsSchema = Record; +``` + +--- + +### Themes + +Register custom themes that appear in the theme selector dropdown. Themes use CSS that's injected once when the plugin loads. + +#### Register a Theme + +```javascript +ctx.themes.register({ + id: "my-theme", + name: "My Custom Theme", + kind: "dark", // "light" or "dark" + css: ` + body.plugin-my-plugin-my-theme { + background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%); + } + + .plugin-my-plugin-my-theme #root { + background: linear-gradient(180deg, rgba(22, 33, 62, 0.5) 0%, rgba(15, 52, 96, 0.3) 100%); + } + + .plugin-my-plugin-my-theme { + --tc-link: hsl(213, 100%, 80%); + } + ` +}); +``` + +#### How It Works + +1. CSS is injected as a `