623 lines
12 KiB
Markdown
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)
|