12 KiB
Plugin Development Guide
Plugin Structure
Each plugin must be a directory containing at least:
index.js- Main entry pointplugin-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:
{
"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:
// 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:
// 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:
// 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:
interface MessageContext {
content: string;
roomId: string;
eventType: string;
formatted?: string;
metadata?: Record<string, any>;
}
🎨 UI API
Plugins also receive ctx.lucide — the full lucide-react icon set for custom renderers and UI.
const { Link, Search, X } = ctx.lucide;
ctx.ui.registerRenderer('message', (msg, defaultRenderer) => {
return ctx.React.createElement(Link, { size: 16 });
});
Register custom renderers:
ctx.ui.registerRenderer("message", (msg, defaultRenderer) => {
if (msg.type === "image") {
return `<img src="${msg.url}" style="border-radius:12px"/>`;
}
// Fall back to default
return defaultRenderer?.();
});
// Unregister renderer
ctx.ui.unregisterRenderer("message");
⚙️ Settings API
Define plugin settings with UI:
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:
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:
// 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:
// 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:
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:
// 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:
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:
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 for a full-featured example.
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:
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:
onUnload: async () => {
// Close connections, save state, etc.
}
3. Performance
Don't block the main thread:
// 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:
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():
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:
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:
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:
// 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:
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:
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
- Never store passwords or tokens in plaintext
- Validate all user input
- Don't execute arbitrary code from messages
- Use HTTPS for external requests
- Respect user privacy - don't log sensitive data
Packaging
To package your plugin:
- Create a ZIP file containing your plugin directory
- The ZIP should contain the plugin folder at the root:
my-plugin.zip └── my-plugin/ ├── index.js ├── plugin-metadata.json └── assets/ - Upload to a URL accessible by users
- Add your plugin to the Plugin Directory repository
Plugin Directory
To list your plugin in the marketplace:
- Fork the Paarrot Plugin Directory
- Add your plugin metadata to
plugins/index.json - Create a plugin JSON file in
plugins/your-plugin.json - 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.jsexists - Verify
module.exportsis 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:
- Copy plugin folder to
%APPDATA%/paarrot/plugins/(Windows) or~/.config/Paarrot/plugins/(Linux) - Restart Paarrot
- Enable plugin in Settings → Plugins
- Check console for errors
- Test all features