# 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) - [UI: Button Registration](#ui-button-registration) - [UI: 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 { registerButton(button: UIButtonDefinition): void; unregisterButton(id: string): void; registerRenderer( type: string, renderer: (data: any, defaultRenderer?: () => ReactNode) => ReactNode ): void; } type CustomRenderer = ( data: any, defaultRenderer?: () => ReactNode ) => ReactNode; ``` --- ### UI: Button Registration Inject buttons into various parts of the Paarrot UI. Buttons render in two visual styles depending on location — **nav list rows** (icon + label, full width) or **icon buttons** (compact, toolbar-style). See [PLUGIN_BUTTON_API.md](PLUGIN_BUTTON_API.md) for the complete reference including positioning and grouping examples. #### Register a Button ```javascript ctx.ui.registerButton({ id: 'my-button', location: 'text-composer-toolbar', label: 'My Tool', icon: '🔧', onClick: () => ctx.log('clicked!') }); ``` #### UI Locations — Nav List Rows | Location | Where it appears | |---|---| | `channel-list` | Space channel list, below Lobby and Message Search | | `home-section` | Home panel, inside the Rooms category above the room list | | `direct-messages` | DMs panel, below "Create Chat" and above the CHATS dropdown | #### UI Locations — Icon Buttons | Location | Where it appears | |---|---| | `text-composer-toolbar` | Message composer toolbar (alongside emoji, sticker buttons) | | `composer-actions` | Left side of the composer, beside the `+` attach button | | `room-header` | Top room header bar, before the ⋮ menu | | `room-menu` | Room ⋮ dropdown menu | | `message-actions` | Message hover action bar | | `user-menu` | Right-click popup on the user avatar | | `search-notification-section` | Notifications page header (right side) | | `sidebar-actions` | Left sidebar — above Explore Servers icon, and above Search icon in the sticky bottom section | #### Positioning ```javascript ctx.ui.registerButton({ id: 'my-button', location: 'text-composer-toolbar', label: 'My Tool', icon: '🔧', position: { after: 'emoji-picker-button', group: 'my-tools', order: 1 }, onClick: () => ctx.log('clicked!') }); ``` #### Unregister ```javascript ctx.ui.unregisterButton('my-button'); // All buttons are automatically unregistered on plugin unload ``` #### API Reference ```typescript type UILocation = | 'channel-list' | 'direct-messages' | 'home-section' | 'text-composer-toolbar' | 'composer-actions' | 'room-header' | 'room-menu' | 'message-actions' | 'user-menu' | 'search-notification-section' | 'sidebar-actions'; interface UIButtonDefinition { id: string; location: UILocation; label: string; icon?: string; position?: UIButtonPosition; onClick?: () => void | Promise; } interface UIButtonPosition { before?: string; after?: string; group?: string; order?: number; } ``` --- ### 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 `