Files
cinny/src/app/features/settings/plugins/PLUGIN_DEVELOPMENT.md

623 lines
12 KiB
Markdown

# 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<string, any>;
}
```
### 🎨 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 `<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:
```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)