# Plugin Development Guide ## Plugin Structure Each plugin must be a directory containing at least: - `index.js` - Main entry point - `plugin-metadata.json` - Plugin metadata ### Example Directory Structure ``` my-plugin/ ├── index.js ├── plugin-metadata.json └── assets/ └── icon.png ``` ## Plugin Metadata The `plugin-metadata.json` file contains information about your plugin: ```json { "name": "My Cool Plugin", "version": "1.0.0", "description": "Does cool things", "author": "Your Name", "homepage": "https://github.com/yourusername/my-plugin", "thumbnail": "https://example.com/thumbnail.png", "tags": ["utility", "ui"] } ``` ## Plugin Entry Point (index.js) Your `index.js` must export a default plugin object with an `onLoad` function: ```javascript // index.js module.exports = { name: "My Plugin", version: "1.0.0", dependencies: { "paarrot.utils": "^1.0.0" // Optional: depend on other plugins }, /** * Called when the plugin is loaded * @param {PluginContext} ctx - The plugin context */ onLoad: async (ctx) => { ctx.log('Plugin loaded!'); // Your plugin code here }, /** * Called when the plugin is unloaded (optional) */ onUnload: async () => { ctx.log('Plugin unloaded!'); // Cleanup resources here }, /** * Exports for other plugins to use (optional) */ exports: { myFunction: () => { /* ... */ } } }; ``` ## Plugin Context API The `ctx` object provided to your plugin contains: ### 🎯 Commands API Register slash commands with argument parsing: ```javascript // Simple command ctx.commands.register({ name: "shrug", run: () => "¯\\_(ツ)_/¯" }); // Command with args ctx.commands.register({ name: "echo", args: ["text"], run: ({ text }) => text }); // Advanced command ctx.commands.register({ name: "greet", description: "Greet someone", args: ["name", "greeting"], run: ({ name, greeting }) => { return `${greeting || 'Hello'}, ${name || 'stranger'}!`; } }); // Execute commands programmatically ctx.commands.execute("shrug", {}); // Unregister a command ctx.commands.unregister("mycommand"); ``` ### 💬 Messages API Intercept and modify messages: ```javascript // Before send interceptor ctx.messages.onBeforeSend((msg) => { if (msg.content === "brb") { msg.content = "be right back!"; } // Add prefix msg.content = `[Bot] ${msg.content}`; // Access metadata msg.metadata = { processed: true }; }); // Receive interceptor ctx.messages.onReceive((msg) => { if (msg.content.includes("@bot")) { ctx.log('Bot was mentioned!'); } }); ``` MessageContext interface: ```typescript interface MessageContext { content: string; roomId: string; eventType: string; formatted?: string; metadata?: Record; } ``` ### 🎨 UI API Plugins also receive `ctx.lucide` — the full [lucide-react](https://lucide.dev/) icon set for custom renderers and UI. ```javascript const { Link, Search, X } = ctx.lucide; ctx.ui.registerRenderer('message', (msg, defaultRenderer) => { return ctx.React.createElement(Link, { size: 16 }); }); ``` Register custom renderers: ```javascript ctx.ui.registerRenderer("message", (msg, defaultRenderer) => { if (msg.type === "image") { return ``; } // Fall back to default return defaultRenderer?.(); }); // Unregister renderer ctx.ui.unregisterRenderer("message"); ``` ### ⚙️ Settings API Define plugin settings with UI: ```javascript ctx.settings.define({ theme: { type: "select", label: "Theme", description: "Choose your theme", options: [ { value: "dark", label: "Dark" }, { value: "light", label: "Light" } ], default: "dark" }, spamFilter: { type: "boolean", label: "Spam Filter", description: "Enable spam filtering", default: true }, prefix: { type: "string", label: "Command Prefix", default: "!" }, maxMessages: { type: "number", label: "Max Messages", default: 100 }, accentColor: { type: "color", label: "Accent Color", default: "#6366f1" } }); // Get/set settings const theme = ctx.settings.get('theme'); ctx.settings.set('theme', 'light'); ``` ### 🪝 Matrix Events API Listen to raw Matrix events: ```javascript ctx.matrix.on("m.room.message", (event) => { const content = event.getContent(); ctx.log('Message received:', content.body); }); ctx.matrix.on("Room.timeline", (event) => { if (event.getType() === 'm.room.member') { ctx.log('Member event:', event.getSender()); } }); // Remove listener ctx.matrix.off("m.room.message", handler); ``` Common events: - `"m.room.message"` - Room messages - `"m.room.member"` - Membership changes - `"Room.timeline"` - Timeline events - `"sync"` - Sync state changes - `"RoomState.events"` - State events ### ⏱️ Timers API Run background tasks: ```javascript // Set interval const intervalId = ctx.timers.setInterval(() => { ctx.log('Running every 10 seconds'); // Auto-cleanup bots, sync data, etc. }, 10000); // Set timeout const timeoutId = ctx.timers.setTimeout(() => { ctx.log('Running once after 5 seconds'); }, 5000); // Clear timers ctx.timers.clearInterval(intervalId); ctx.timers.clearTimeout(timeoutId); // Timers are auto-cleaned on plugin unload ``` ### 📣 Notifications API Show notifications to users: ```javascript // Simple notification ctx.notify("Plugin loaded!"); // Advanced notification ctx.notify({ title: "New Message", body: "You have a new message from Steve", type: "info", // info, success, error, warning duration: 5000, actions: [ { label: "View", action: () => { // Handle action } } ] }); ``` ### 📝 Logging API Per-plugin logging: ```javascript ctx.log('Info message', { data: 123 }); ctx.warn('Warning message'); ctx.error('Error message', error); // Logs are visible in plugin logs panel // Format: [Plugin pluginId] message ``` ### 🔗 Require API Use other plugins: ```javascript // Require another plugin's exports const utils = ctx.require("paarrot.utils"); utils.formatDate(new Date()); // Check if plugin exists try { const plugin = ctx.require("optional.plugin"); } catch (err) { ctx.warn('Optional plugin not available'); } ``` ### 🔌 Matrix Client Direct access to Matrix.js SDK: ```javascript const mx = ctx.matrixClient; // Get user info const userId = mx.getUserId(); const user = mx.getUser(userId); // Get rooms const rooms = mx.getRooms(); const room = mx.getRoom(roomId); // Send messages await mx.sendTextMessage(roomId, "Hello!"); // Full Matrix.js SDK available ``` ### ⚛️ React Access to React: ```javascript const { useState, useEffect } = ctx.React; // Create custom UI components const MyComponent = () => { const [count, setCount] = useState(0); return ctx.React.createElement('button', { onClick: () => setCount(count + 1) }, `Count: ${count}`); }; ``` ## Complete Example See [`example-plugin/index.js`](../../../../example-plugin/index.js) for a full-featured example. ```javascript module.exports = { name: "Awesome Plugin", version: "1.0.0", onLoad: async (ctx) => { // Register commands ctx.commands.register({ name: "shrug", run: () => "¯\\_(ツ)_/¯" }); // Message interceptors ctx.messages.onBeforeSend((msg) => { if (msg.content === "brb") { msg.content = "be right back!"; } }); // Settings ctx.settings.define({ enabled: { type: "boolean", default: true } }); // Matrix events ctx.matrix.on("m.room.message", (event) => { ctx.log('Message:', event.getContent().body); }); // Background task ctx.timers.setInterval(() => { ctx.log('Checking for updates...'); }, 60000); ctx.notify("Plugin loaded!"); }, onUnload: async () => { // Cleanup happens automatically }, exports: { version: "1.0.0" } }; ``` ## Best Practices ### 1. Error Handling Always wrap your code in try-catch blocks: ```javascript onLoad: async (ctx) => { try { // Your code } catch (error) { ctx.error('Failed to load:', error); } } ``` ### 2. Cleanup Resources Timers and Matrix handlers are auto-cleaned, but clean up other resources: ```javascript onUnload: async () => { // Close connections, save state, etc. } ``` ### 3. Performance Don't block the main thread: ```javascript // Bad for (let i = 0; i < 1000000; i++) { /* ... */ } // Good ctx.timers.setTimeout(() => { // Heavy work in background }, 0); ``` ### 4. Settings Best Practices Provide defaults and descriptions: ```javascript ctx.settings.define({ maxRetries: { type: "number", label: "Max Retries", description: "Number of retry attempts", default: 3 } }); ``` ### 5. Logging Use ctx.log() instead of console.log(): ```javascript ctx.log('Info'); // Shows in plugin logs panel ctx.warn('Warning'); // Highlighted as warning ctx.error('Error'); // Highlighted as error ``` ## Advanced Patterns ### Plugin Dependencies Require and use other plugins: ```javascript onLoad: async (ctx) => { try { const utils = ctx.require("paarrot.utils"); const formatted = utils.formatDate(new Date()); ctx.log('Using utils:', formatted); } catch (err) { ctx.warn('Utils plugin not available, using fallback'); } } ``` ### Conditional Features Enable features based on settings: ```javascript const autoReply = ctx.settings.get('autoReply'); if (autoReply) { ctx.matrix.on("m.room.message", (event) => { // Auto-reply logic }); } ``` ### State Management Store plugin state in settings: ```javascript // Save state ctx.settings.set('lastRun', Date.now()); ctx.settings.set('messageCount', count); // Load state const lastRun = ctx.settings.get('lastRun'); ``` ### Command Aliases Create multiple commands for the same function: ```javascript const shrugFn = () => "¯\\_(ツ)_/¯"; ctx.commands.register({ name: "shrug", run: shrugFn }); ctx.commands.register({ name: "dunno", run: shrugFn }); ``` ### Hot Reload Support Make your plugin hot-reload friendly: ```javascript let messageHandler = null; onLoad: async (ctx) => { messageHandler = (event) => { /* ... */ }; ctx.matrix.on("m.room.message", messageHandler); }, onUnload: async () => { if (messageHandler) { // Cleanup happens automatically but explicit is good } } ``` ## Security Considerations 1. **Never store passwords or tokens in plaintext** 2. **Validate all user input** 3. **Don't execute arbitrary code from messages** 4. **Use HTTPS for external requests** 5. **Respect user privacy - don't log sensitive data** ## Packaging To package your plugin: 1. Create a ZIP file containing your plugin directory 2. The ZIP should contain the plugin folder at the root: ``` my-plugin.zip └── my-plugin/ ├── index.js ├── plugin-metadata.json └── assets/ ``` 3. Upload to a URL accessible by users 4. Add your plugin to the Plugin Directory repository ## Plugin Directory To list your plugin in the marketplace: 1. Fork the [Paarrot Plugin Directory](https://github.com/Paarrot/Plugin-Directory) 2. Add your plugin metadata to `plugins/index.json` 3. Create a plugin JSON file in `plugins/your-plugin.json` 4. Submit a pull request ## Debugging ### Browser Console Check the browser console (F12) for plugin logs: - `[PluginLoader]` - Plugin loading messages - `[Plugin yourplugin]` - Your plugin's logs ### Plugin Logs Panel View per-plugin logs in Settings → Plugins → Logs (coming soon) ### Common Issues **Plugin won't load:** - Check `index.js` exists - Verify `module.exports` is correct - Look for JavaScript syntax errors **Command not working:** - Ensure command is registered in onLoad - Check command name doesn't conflict - Verify run function returns a value **Settings not saving:** - Use ctx.settings.set() not localStorage - Check setting key matches defined schema ## Testing Test your plugin locally: 1. Copy plugin folder to `%APPDATA%/paarrot/plugins/` (Windows) or `~/.config/Paarrot/plugins/` (Linux) 2. Restart Paarrot 3. Enable plugin in Settings → Plugins 4. Check console for errors 5. Test all features ## Resources - [Matrix.js SDK Docs](https://matrix-org.github.io/matrix-js-sdk/) - [Paarrot Plugin Examples](../../../../example-plugin/) - [Plugin Directory](https://github.com/Paarrot/Plugin-Directory) - [Report Issues](https://github.com/Paarrot/cinny-desktop/issues)