feat(plugins): implement plugin loading and management system
- Added PluginLoader component to dynamically load and initialize plugins. - Created Plugins component for managing installed and marketplace plugins. - Introduced PluginAPI for plugin interaction and settings management. - Defined types for plugin metadata, installed plugins, and plugin index. - Implemented settings rendering for plugins based on their schema. - Integrated marketplace plugin fetching and installation logic. - Added support for enabling/disabling and uninstalling plugins.
This commit is contained in:
612
src/app/features/settings/plugins/PLUGIN_DEVELOPMENT.md
Normal file
612
src/app/features/settings/plugins/PLUGIN_DEVELOPMENT.md
Normal file
@@ -0,0 +1,612 @@
|
||||
# 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
|
||||
|
||||
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)
|
||||
Reference in New Issue
Block a user