Files
cinny-desktop/docs/PLUGIN_API.md
Max Litruv Boonzaayer 8ccc3ecb78
All checks were successful
Build / increment-version (push) Successful in 6s
Build / build-linux (push) Successful in 2m29s
Build / build-windows (push) Successful in 6m50s
Build / create-release (push) Successful in 52s
feat: Add Plugin Button Registration API documentation and Plugin System implementation summary
- Introduced PLUGIN_BUTTON_API.md detailing button registration, UI locations, and usage examples.
- Added PLUGIN_SYSTEM_IMPLEMENTATION.md summarizing implemented features, usage examples, and API details for the plugin system.
2026-04-22 00:29:14 +10:00

1283 lines
27 KiB
Markdown

# Paarrot Plugin API Documentation
Complete reference for developing plugins for Paarrot/Cinny Desktop.
## Table of Contents
- [Getting Started](#getting-started)
- [Plugin Structure](#plugin-structure)
- [Plugin Context API](#plugin-context-api)
- [Commands](#commands)
- [Message Interceptors](#message-interceptors)
- [UI: Button Registration](#ui-button-registration)
- [UI: Custom Renderers](#custom-renderers)
- [Settings](#settings)
- [Themes](#themes)
- [Matrix Events](#matrix-events)
- [Background Tasks](#background-tasks)
- [Notifications](#notifications)
- [Logging](#logging)
- [Plugin Exports](#plugin-exports)
- [Matrix Client](#matrix-client)
---
## Getting Started
### Plugin Location
Plugins are installed in:
- **Windows**: `%APPDATA%\paarrot\plugins\<plugin-name>\`
- **Linux**: `~/.config/Paarrot/plugins/<plugin-name>/`
- **macOS**: `~/Library/Application Support/Paarrot/plugins/<plugin-name>/`
### Required Files
- `index.js` - Main plugin file (required)
- `plugin-metadata.json` - Plugin metadata (required)
- `README.md` - Documentation (recommended)
### plugin-metadata.json
```json
{
"name": "My Awesome Plugin",
"version": "1.0.0",
"description": "Does something cool",
"author": "Your Name",
"homepage": "https://github.com/username/plugin",
"thumbnail": "https://example.com/icon.png",
"tags": ["utility", "messaging", "fun"]
}
```
---
## Plugin Structure
### Basic Plugin
```javascript
module.exports = {
name: "My Plugin",
version: "1.0.0",
dependencies: {
// "other-plugin": "^1.0.0"
},
/**
* Called when plugin loads
* @param {PluginContext} ctx - Plugin context
*/
onLoad: async (ctx) => {
ctx.log('Plugin loaded!');
// Initialize your plugin here
ctx.commands.register({
name: "hello",
description: "Say hello",
run: () => "Hello, world!"
});
},
/**
* Called when plugin unloads
*/
onUnload: async () => {
console.log('Plugin unloaded');
},
/**
* API for other plugins
*/
exports: null
};
```
---
## Plugin Context API
The `PluginContext` provides access to all plugin APIs:
```typescript
interface PluginContext {
pluginId: string;
matrixClient: MatrixClient;
React: typeof import('react');
commands: CommandsAPI;
messages: MessagesAPI;
ui: UIAPI;
settings: SettingsAPI;
matrix: MatrixEventsAPI;
timers: TimersAPI;
notify: NotificationFunction;
log: LogFunction;
warn: LogFunction;
error: LogFunction;
require: RequireFunction;
}
```
### Commands
Register slash commands that users can type in chat.
#### Register a Command
```javascript
ctx.commands.register({
name: "mycommand",
description: "Does something cool",
args: ["arg1", "arg2"], // Optional
run: (args) => {
// Return string to send as message
return `Got args: ${args.arg1}, ${args.arg2}`;
}
});
```
#### Examples
**Simple command:**
```javascript
ctx.commands.register({
name: "shrug",
description: "Send a shrug emoji",
run: () => "¯\\_(ツ)_/¯"
});
// Usage: /shrug
```
**Command with arguments:**
```javascript
ctx.commands.register({
name: "greet",
description: "Greet someone",
args: ["name"],
run: ({ name }) => `Hello, ${name || 'stranger'}!`
});
// Usage: /greet Alice
```
**Advanced command:**
```javascript
ctx.commands.register({
name: "calc",
description: "Simple calculator",
args: ["expression"],
run: ({ expression }) => {
try {
const sanitized = expression.replace(/[^0-9+\-*/().\s]/g, '');
const result = Function(`'use strict'; return (${sanitized})`)();
return `${expression} = ${result}`;
} catch (err) {
return "Invalid expression";
}
}
});
// Usage: /calc 2 + 2 * 3
```
#### API Reference
```typescript
interface CommandsAPI {
register(command: PluginCommand): void;
unregister(name: string): void;
execute(name: string, args: Record<string, any>): Promise<string | void>;
}
interface PluginCommand {
name: string;
description: string;
args?: string[];
run: (args: Record<string, any>) => string | void | Promise<string | void>;
}
```
---
### Message Interceptors
Intercept and modify messages before they're sent or after they're received.
#### Before Send
Modify outgoing messages before they're sent to the server.
```javascript
ctx.messages.onBeforeSend((msg) => {
// Modify msg.content
if (msg.content.includes('secret')) {
msg.content = msg.content.replace(/secret/g, '█████');
}
// Add prefix
msg.content = `[Bot] ${msg.content}`;
});
```
#### On Receive
Process incoming messages (read-only monitoring).
```javascript
ctx.messages.onReceive((msg) => {
// Monitor for keywords
if (msg.content.includes('important')) {
ctx.notify({
title: 'Important Message',
body: msg.content,
type: 'info'
});
}
});
```
#### Real-World Examples
**Auto-expand abbreviations:**
```javascript
ctx.messages.onBeforeSend((msg) => {
const expansions = {
'brb': 'be right back',
'omw': 'on my way',
'gtg': 'got to go',
'afk': 'away from keyboard'
};
Object.entries(expansions).forEach(([abbr, full]) => {
const regex = new RegExp(`\\b${abbr}\\b`, 'gi');
msg.content = msg.content.replace(regex, full);
});
});
```
**Add auto-shrug to questions:**
```javascript
ctx.messages.onBeforeSend((msg) => {
if (msg.content.trim().endsWith('?')) {
msg.content += ' ¯\\_(ツ)_/¯';
}
});
```
**Track mentions:**
```javascript
ctx.messages.onReceive((msg) => {
const userId = ctx.matrixClient.getUserId();
if (msg.content.includes(userId)) {
ctx.log('You were mentioned!');
ctx.notify('Someone mentioned you!');
}
});
```
#### API Reference
```typescript
interface MessagesAPI {
onBeforeSend(handler: (msg: Message) => void): void;
onReceive(handler: (msg: Message) => void): void;
}
interface Message {
content: string;
roomId?: string;
sender?: string;
eventId?: string;
}
```
---
### Custom Renderers
Register custom UI renderers for messages, images, and other content types.
```javascript
// Custom message renderer
ctx.ui.registerRenderer("message", (msg, defaultRenderer) => {
// Custom logic
if (msg.sender?.includes('bot')) {
// Return custom React component
return ctx.React.createElement('div', {
style: { background: 'yellow' }
}, msg.content);
}
// Fall back to default
return defaultRenderer?.();
});
// Custom image renderer
ctx.ui.registerRenderer("image", (data, defaultRenderer) => {
// Add custom effects, borders, etc.
return defaultRenderer?.();
});
```
#### API Reference
```typescript
interface UIAPI {
registerButton(button: UIButtonDefinition): void;
unregisterButton(id: string): void;
registerRenderer(
type: string,
renderer: (data: any, defaultRenderer?: () => ReactNode) => ReactNode
): void;
}
type CustomRenderer = (
data: any,
defaultRenderer?: () => ReactNode
) => ReactNode;
```
---
### UI: Button Registration
Inject buttons into various parts of the Paarrot UI. Buttons render in two visual styles depending on location — **nav list rows** (icon + label, full width) or **icon buttons** (compact, toolbar-style).
See [PLUGIN_BUTTON_API.md](PLUGIN_BUTTON_API.md) for the complete reference including positioning and grouping examples.
#### Register a Button
```javascript
ctx.ui.registerButton({
id: 'my-button',
location: 'text-composer-toolbar',
label: 'My Tool',
icon: '🔧',
onClick: () => ctx.log('clicked!')
});
```
#### UI Locations — Nav List Rows
| Location | Where it appears |
|---|---|
| `channel-list` | Space channel list, below Lobby and Message Search |
| `home-section` | Home panel, inside the Rooms category above the room list |
| `direct-messages` | DMs panel, below "Create Chat" and above the CHATS dropdown |
#### UI Locations — Icon Buttons
| Location | Where it appears |
|---|---|
| `text-composer-toolbar` | Message composer toolbar (alongside emoji, sticker buttons) |
| `composer-actions` | Left side of the composer, beside the `+` attach button |
| `room-header` | Top room header bar, before the ⋮ menu |
| `room-menu` | Room ⋮ dropdown menu |
| `message-actions` | Message hover action bar |
| `user-menu` | Right-click popup on the user avatar |
| `search-notification-section` | Notifications page header (right side) |
| `sidebar-actions` | Left sidebar — above Explore Servers icon, and above Search icon in the sticky bottom section |
#### Positioning
```javascript
ctx.ui.registerButton({
id: 'my-button',
location: 'text-composer-toolbar',
label: 'My Tool',
icon: '🔧',
position: {
after: 'emoji-picker-button',
group: 'my-tools',
order: 1
},
onClick: () => ctx.log('clicked!')
});
```
#### Unregister
```javascript
ctx.ui.unregisterButton('my-button');
// All buttons are automatically unregistered on plugin unload
```
#### API Reference
```typescript
type UILocation =
| 'channel-list' | 'direct-messages' | 'home-section'
| 'text-composer-toolbar' | 'composer-actions'
| 'room-header' | 'room-menu' | 'message-actions'
| 'user-menu' | 'search-notification-section' | 'sidebar-actions';
interface UIButtonDefinition {
id: string;
location: UILocation;
label: string;
icon?: string;
position?: UIButtonPosition;
onClick?: () => void | Promise<void>;
}
interface UIButtonPosition {
before?: string;
after?: string;
group?: string;
order?: number;
}
```
---
### Settings
Define settings that users can configure in the UI.
#### Define Settings Schema
```javascript
ctx.settings.define({
// Boolean toggle
enableFeature: {
type: "boolean",
label: "Enable Feature",
description: "Toggle this feature on/off",
default: true
},
// String input
apiKey: {
type: "string",
label: "API Key",
description: "Your API key",
default: ""
},
// Number input
maxRetries: {
type: "number",
label: "Max Retries",
description: "Maximum retry attempts",
default: 3
},
// Select dropdown
theme: {
type: "select",
label: "Theme",
description: "Choose a theme",
options: [
{ value: "dark", label: "Dark Mode" },
{ value: "light", label: "Light Mode" }
],
default: "dark"
},
// Color picker
accentColor: {
type: "color",
label: "Accent Color",
description: "Choose your accent color",
default: "#6366f1"
}
});
```
#### Get/Set Settings
```javascript
// Get a setting
const theme = ctx.settings.get('theme');
const enabled = ctx.settings.get('enableFeature');
// Set a setting (usually done by user in UI)
ctx.settings.set('theme', 'dark');
```
#### API Reference
```typescript
interface SettingsAPI {
define(schema: SettingsSchema): void;
get<T = any>(key: string): T | undefined;
set(key: string, value: any): void;
}
interface SettingDefinition {
type: 'string' | 'number' | 'boolean' | 'select' | 'color';
label?: string;
description?: string;
default?: any;
options?: Array<{ value: any; label: string }>;
}
type SettingsSchema = Record<string, SettingDefinition>;
```
---
### Themes
Register custom themes that appear in the theme selector dropdown. Themes use CSS that's injected once when the plugin loads.
#### Register a Theme
```javascript
ctx.themes.register({
id: "my-theme",
name: "My Custom Theme",
kind: "dark", // "light" or "dark"
css: `
body.plugin-my-plugin-my-theme {
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
}
.plugin-my-plugin-my-theme #root {
background: linear-gradient(180deg, rgba(22, 33, 62, 0.5) 0%, rgba(15, 52, 96, 0.3) 100%);
}
.plugin-my-plugin-my-theme {
--tc-link: hsl(213, 100%, 80%);
}
`
});
```
#### How It Works
1. CSS is injected as a `<style>` tag when plugin loads
2. Theme appears in Settings → General → Theme dropdown
3. When selected, the class `.plugin-{pluginId}-{themeId}` is applied to `<body>`
4. Your CSS rules match and style the app
#### Theme Class Name
Your theme CSS selector will be: `.plugin-{pluginId}-{themeId}`
Example: plugin ID `my-plugin` + theme ID `dark-blue` = `.plugin-my-plugin-dark-blue`
#### CSS Custom Properties
Available CSS variables you can override:
- `--tc-link` - Link color
- `--mx-uc-1` through `--mx-uc-8` - Matrix user ID colors (for different users)
- `--font-emoji` - Emoji font family
- `--font-secondary` - Secondary font family
#### Styling Elements
Common selectors to style:
- `body.plugin-{pluginId}-{themeId}` - Body background
- `.plugin-{pluginId}-{themeId} #root` - Main app container
- `.plugin-{pluginId}-{themeId} [role="navigation"]` - Navigation sidebar
- `.plugin-{pluginId}-{themeId} aside` - Side panels
- `.plugin-{pluginId}-{themeId} main` - Main content area
- `.plugin-{pluginId}-{themeId} header` - Headers
- `.plugin-{pluginId}-{themeId} button` - Buttons
#### Examples
**Dark Theme with Gradients:**
```javascript
ctx.themes.register({
id: "midnight",
name: "Midnight Blue 🌙",
kind: "dark",
css: `
body.plugin-my-plugin-midnight {
background: linear-gradient(135deg, #0a192f 0%, #112240 50%, #1a365d 100%);
}
.plugin-my-plugin-midnight #root {
background: linear-gradient(180deg, rgba(17, 34, 64, 0.5) 0%, rgba(26, 54, 93, 0.3) 100%);
}
.plugin-my-plugin-midnight [role="navigation"],
.plugin-my-plugin-midnight aside {
background: linear-gradient(180deg, rgba(10, 25, 47, 0.8) 0%, rgba(17, 34, 64, 0.9) 100%);
backdrop-filter: blur(8px);
}
.plugin-my-plugin-midnight main {
background: linear-gradient(135deg, rgba(17, 34, 64, 0.4) 0%, rgba(26, 54, 93, 0.3) 100%);
}
.plugin-my-plugin-midnight header {
background: linear-gradient(90deg, rgba(10, 25, 47, 0.9) 0%, rgba(17, 34, 64, 0.8) 100%);
}
.plugin-my-plugin-midnight {
--tc-link: hsl(213, 100%, 80%);
}
`
});
```
**Light Theme:**
```javascript
ctx.themes.register({
id: "sunrise",
name: "Sunrise ☀️",
kind: "light",
css: `
body.plugin-my-plugin-sunrise {
background: linear-gradient(135deg, #fff4e6 0%, #ffe0b2 50%, #ffcc80 100%);
}
.plugin-my-plugin-sunrise #root {
background: linear-gradient(180deg, rgba(255, 224, 178, 0.5) 0%, rgba(255, 204, 128, 0.3) 100%);
}
.plugin-my-plugin-sunrise [role="navigation"],
.plugin-my-plugin-sunrise aside {
background: linear-gradient(180deg, rgba(255, 244, 230, 0.9) 0%, rgba(255, 224, 178, 0.95) 100%);
backdrop-filter: blur(8px);
}
.plugin-my-plugin-sunrise button:hover {
filter: brightness(0.95);
}
.plugin-my-plugin-sunrise {
--tc-link: hsl(30, 100%, 40%);
}
`
});
```
**Minimal Theme:**
```javascript
ctx.themes.register({
id: "forest",
name: "Forest Green 🌲",
kind: "dark",
css: `
body.plugin-my-plugin-forest {
background: #1a2f23;
}
.plugin-my-plugin-forest {
--tc-link: hsl(140, 60%, 70%);
--mx-uc-1: hsl(140, 60%, 65%);
--mx-uc-2: hsl(150, 60%, 70%);
--mx-uc-3: hsl(130, 60%, 65%);
--mx-uc-4: hsl(160, 60%, 70%);
--mx-uc-5: hsl(120, 60%, 65%);
--mx-uc-6: hsl(145, 60%, 60%);
--mx-uc-7: hsl(135, 60%, 70%);
--mx-uc-8: hsl(155, 60%, 70%);
}
`
});
```
#### Unregister Theme
```javascript
ctx.themes.unregister("my-theme");
```
#### API Reference
```typescript
interface ThemesAPI {
register(theme: PluginTheme): void;
unregister(themeId: string): void;
}
interface PluginTheme {
id: string;
name: string;
kind: 'light' | 'dark';
/** CSS string that will be injected when theme is active */
css: string;
}
```
**Notes:**
- CSS is injected once when the plugin loads (not per theme change)
- Theme IDs are automatically prefixed with `plugin-{pluginId}-` to avoid conflicts
- Theme class names follow the pattern: `.plugin-{pluginId}-{themeId}`
- Themes appear in Settings → General → Theme selector
- CSS stays in the page until plugin is unloaded
- Themes work exactly like built-in themes (Silver, Dark, Mocha, etc.)
- Use gradients, backdrop-filters, and transitions for polished themes
---
### Matrix Events
Hook into raw Matrix events for advanced functionality.
#### Listen to Events
```javascript
// Room messages
ctx.matrix.on("m.room.message", (event) => {
const content = event.getContent();
ctx.log('Message:', content.body);
});
// Membership changes
ctx.matrix.on("m.room.member", (event) => {
const content = event.getContent();
ctx.log('Membership:', content.membership);
});
// Timeline events
ctx.matrix.on("Room.timeline", (event) => {
if (event.getType() === 'm.room.message') {
const sender = event.getSender();
ctx.log('Timeline event from:', sender);
}
});
// Sync state
ctx.matrix.on("sync", (state, prevState) => {
ctx.log('Sync state:', state);
});
```
#### Remove Event Handlers
```javascript
const handler = (event) => {
ctx.log('Event received');
};
ctx.matrix.on("m.room.message", handler);
// Later...
ctx.matrix.off("m.room.message", handler);
```
#### API Reference
```typescript
interface MatrixEventsAPI {
on(eventType: string, handler: (event: MatrixEvent) => void): void;
off(eventType: string, handler: (event: MatrixEvent) => void): void;
}
```
**Common Event Types:**
- `m.room.message` - Room messages
- `m.room.member` - Membership changes
- `m.room.name` - Room name changes
- `m.room.topic` - Room topic changes
- `Room.timeline` - Timeline events
- `sync` - Sync state changes
---
### Background Tasks
Run periodic tasks or delayed actions.
#### Set Interval
```javascript
// Run every 30 seconds
const intervalId = ctx.timers.setInterval(() => {
ctx.log('Periodic task running');
// Do something periodically
const stats = {
timestamp: Date.now(),
setting: ctx.settings.get('theme')
};
ctx.log('Stats:', stats);
}, 30000);
// Clear interval
ctx.timers.clearInterval(intervalId);
```
#### Set Timeout
```javascript
// Run once after delay
const timeoutId = ctx.timers.setTimeout(() => {
ctx.notify({
title: 'Reminder',
body: 'This is a delayed notification',
type: 'info'
});
}, 10000); // 10 seconds
// Clear timeout
ctx.timers.clearTimeout(timeoutId);
```
#### API Reference
```typescript
interface TimersAPI {
setInterval(callback: () => void, ms: number): number;
setTimeout(callback: () => void, ms: number): number;
clearInterval(id: number): void;
clearTimeout(id: number): void;
}
```
**Note:** Timers are automatically cleaned up when the plugin is unloaded.
---
### Notifications
Show system notifications to the user.
#### Simple Notification
```javascript
ctx.notify('Hello, world!');
```
#### Detailed Notification
```javascript
ctx.notify({
title: 'Plugin Alert',
body: 'Something important happened!',
type: 'info', // 'info' | 'success' | 'error' | 'warning'
duration: 5000 // milliseconds (optional)
});
```
#### Examples
```javascript
// Success notification
ctx.notify({
title: 'Success',
body: 'Operation completed successfully!',
type: 'success'
});
// Error notification
ctx.notify({
title: 'Error',
body: 'Something went wrong',
type: 'error'
});
// Warning
ctx.notify({
title: 'Warning',
body: 'This action cannot be undone',
type: 'warning'
});
```
#### API Reference
```typescript
type NotificationFunction = (
options: NotificationOptions | string
) => void;
interface NotificationOptions {
title: string;
body: string;
type?: 'info' | 'success' | 'error' | 'warning';
duration?: number;
}
```
---
### Logging
Log messages to the console and plugin logs panel.
```javascript
// Info logs
ctx.log('Plugin started');
ctx.log('User count:', 42);
ctx.log('Complex object:', { foo: 'bar', nested: { value: 123 } });
// Warning logs
ctx.warn('This is a warning');
ctx.warn('Deprecated feature used');
// Error logs
ctx.error('Something went wrong');
ctx.error('Error details:', errorObject);
```
#### API Reference
```typescript
type LogFunction = (...args: any[]) => void;
interface PluginContext {
log: LogFunction; // Info level
warn: LogFunction; // Warning level
error: LogFunction; // Error level
}
```
**Note:** All logs are visible in:
- Browser console (F12)
- Plugin logs panel (Settings → Plugins → Logs) - coming soon
---
### Plugin Exports
Export functions and data for other plugins to use.
#### Define Exports
```javascript
module.exports = {
// ... onLoad, onUnload ...
exports: {
version: "1.0.0",
utils: {
greet: (name) => `Hello, ${name}!`,
processText: (text) => text.toUpperCase(),
getStats: () => ({
version: "1.0.0",
features: ['commands', 'settings']
})
}
}
};
```
#### Use Another Plugin's Exports
```javascript
// In your plugin
const otherPlugin = ctx.require('other-plugin-id');
if (otherPlugin) {
const greeting = otherPlugin.utils.greet('World');
ctx.log(greeting); // "Hello, World!"
const processed = otherPlugin.utils.processText('hello');
ctx.log(processed); // "HELLO"
const stats = otherPlugin.utils.getStats();
ctx.log('Stats:', stats);
}
```
#### API Reference
```typescript
type RequireFunction = (pluginId: string) => any;
interface Plugin {
name: string;
version: string;
dependencies?: Record<string, string>;
onLoad: (ctx: PluginContext) => Promise<void>;
onUnload?: () => Promise<void>;
exports?: any;
}
```
---
### Matrix Client
Direct access to the Matrix.js SDK for advanced operations.
```javascript
// Get user info
const userId = ctx.matrixClient.getUserId();
const displayName = ctx.matrixClient.getUser(userId)?.displayName;
// Get rooms
const rooms = ctx.matrixClient.getRooms();
ctx.log(`You're in ${rooms.length} rooms`);
// Get specific room
const room = ctx.matrixClient.getRoom(roomId);
if (room) {
const name = room.name;
const members = room.getMembers();
ctx.log(`Room: ${name}, Members: ${members.length}`);
}
// Sync state
const syncState = ctx.matrixClient.getSyncState();
ctx.log('Sync state:', syncState);
// Send custom event
await ctx.matrixClient.sendEvent(roomId, 'm.room.message', {
msgtype: 'm.text',
body: 'Hello from plugin!'
});
```
#### API Reference
```typescript
interface PluginContext {
matrixClient: MatrixClient; // Full Matrix.js SDK client
}
```
For full Matrix.js SDK documentation, see: https://matrix-org.github.io/matrix-js-sdk/
---
## Complete Example
Here's a complete plugin that demonstrates all features:
```javascript
/**
* Example Plugin - All Features
*/
module.exports = {
name: "Example Plugin",
version: "1.0.0",
dependencies: {},
onLoad: async (ctx) => {
ctx.log('🚀 Plugin loading...');
// === COMMANDS ===
ctx.commands.register({
name: "hello",
description: "Say hello",
run: () => "Hello, world!"
});
ctx.commands.register({
name: "greet",
description: "Greet someone",
args: ["name"],
run: ({ name }) => `Hello, ${name}!`
});
// === INTERCEPTORS ===
ctx.messages.onBeforeSend((msg) => {
// Auto-expand abbreviations
msg.content = msg.content.replace(/\bbrb\b/gi, 'be right back');
// Add emoji if enabled
if (ctx.settings.get('addEmoji')) {
msg.content += ' ✨';
}
});
ctx.messages.onReceive((msg) => {
// Track keywords
const keywords = ctx.settings.get('keywords') || '';
keywords.split(',').forEach(keyword => {
if (msg.content.includes(keyword.trim())) {
ctx.notify(`Keyword detected: ${keyword}`);
}
});
});
// === SETTINGS ===
ctx.settings.define({
addEmoji: {
type: "boolean",
label: "Add Emoji",
description: "Add sparkles to messages",
default: false
},
keywords: {
type: "string",
label: "Track Keywords",
description: "Comma-separated keywords to track",
default: "important,urgent"
},
theme: {
type: "select",
label: "Theme",
options: [
{ value: "light", label: "Light" },
{ value: "dark", label: "Dark" }
],
default: "dark"
}
});
// === THEMES ===
ctx.themes.register({
id: "example-theme",
name: "Example Theme",
kind: "dark",
colors: {
background: "#1a1a2e",
surface: "#16213e",
primary: "#0f3460",
secondary: "#533483",
onBackground: "#ffffff",
onSurface: "#e0e0e0"
}
});
// === MATRIX EVENTS ===
ctx.matrix.on("m.room.message", (event) => {
const content = event.getContent();
if (content.body?.includes('example')) {
ctx.log('Keyword found in message');
}
});
// === BACKGROUND TASKS ===
ctx.timers.setInterval(() => {
ctx.log('Periodic check');
}, 60000); // Every minute
// === NOTIFICATIONS ===
ctx.notify({
title: 'Example Plugin',
body: 'Plugin loaded successfully!',
type: 'success'
});
// === EXPORTS ===
this.exports = {
version: "1.0.0",
greet: (name) => `Hello, ${name}!`
};
ctx.log('✅ Plugin loaded');
},
onUnload: async () => {
console.log('Plugin unloaded');
},
exports: null
};
```
---
## Best Practices
### Error Handling
Always wrap plugin code in try-catch:
```javascript
onLoad: async (ctx) => {
try {
// Your code here
} catch (error) {
ctx.error('Plugin error:', error);
ctx.notify({
title: 'Plugin Error',
body: error.message,
type: 'error'
});
}
}
```
### Cleanup
Clean up resources in `onUnload`:
```javascript
onUnload: async () => {
// Timers are auto-cleared
// Event handlers are auto-removed
// Settings are persisted automatically
console.log('Plugin unloaded');
}
```
### Settings Validation
Validate settings before use:
```javascript
const maxRetries = ctx.settings.get('maxRetries') || 3;
const keywords = (ctx.settings.get('keywords') || '').split(',').filter(k => k.trim());
```
### Logging
Use appropriate log levels:
```javascript
ctx.log('Info message'); // Normal operation
ctx.warn('Warning message'); // Potential issues
ctx.error('Error message'); // Actual errors
```
### Performance
- Use intervals sparingly (30s+ recommended)
- Avoid heavy processing in message interceptors
- Cache expensive computations
- Clean up timers when not needed
---
## Troubleshooting
### Plugin Won't Load
1. Check console for errors (F12)
2. Verify `plugin-metadata.json` exists
3. Check for JavaScript syntax errors
4. Ensure `onLoad` function exists
### Commands Not Working
1. Ensure plugin is enabled
2. Check console for "Commands registered" message
3. Verify command name doesn't conflict
4. Try `/test` command first
### Settings Not Showing
1. Check settings schema is defined
2. Verify setting types are valid
3. Look for JavaScript errors in console
### Interceptors Not Working
1. Check plugin is enabled
2. Verify interceptors are registered in `onLoad`
3. Check console logs for interceptor messages
---
## Publishing Your Plugin
To publish your plugin to the Paarrot Plugin Directory:
1. Create a GitHub repository
2. Add these files:
- `index.js`
- `plugin-metadata.json`
- `README.md`
3. Create a ZIP file: `your-plugin.zip`
4. Submit to: https://github.com/Paarrot/Plugin-Directory
---
## Support
- Documentation: https://github.com/Paarrot/cinny-desktop/blob/main/PLUGIN_API.md
- Examples: https://github.com/Paarrot/cinny-desktop/tree/main/example-plugin
- Issues: https://github.com/Paarrot/cinny-desktop/issues
- Community: https://matrix.to/#/#paarrot:matrix.org
---
## License
MIT License - See LICENSE file for details