Files
cinny/docs/PLUGIN_API.md

28 KiB

Paarrot Plugin API Documentation

Complete reference for developing plugins for Paarrot/Cinny Desktop.

Table of Contents


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

{
  "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

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:

interface PluginContext {
  pluginId: string;
  matrixClient: MatrixClient;
  React: typeof import('react');
  lucide: typeof import('lucide-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

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:

ctx.commands.register({
  name: "shrug",
  description: "Send a shrug emoji",
  run: () => "¯\\_(ツ)_/¯"
});
// Usage: /shrug

Command with arguments:

ctx.commands.register({
  name: "greet",
  description: "Greet someone",
  args: ["name"],
  run: ({ name }) => `Hello, ${name || 'stranger'}!`
});
// Usage: /greet Alice

Advanced command:

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

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.

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).

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:

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:

ctx.messages.onBeforeSend((msg) => {
  if (msg.content.trim().endsWith('?')) {
    msg.content += ' ¯\\_(ツ)_/¯';
  }
});

Track mentions:

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

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.

// 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

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 for the complete reference including positioning and grouping examples.

Register a Button

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

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

ctx.ui.unregisterButton('my-button');
// All buttons are automatically unregistered on plugin unload

API Reference

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;
  /** Lucide icon name, e.g. `ImagePlay`. Also supported via `icon: 'lucide:ImagePlay'`. */
  lucideIcon?: 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

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

// 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

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

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:

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:

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:

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

ctx.themes.unregister("my-theme");

API Reference

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

// 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

const handler = (event) => {
  ctx.log('Event received');
};

ctx.matrix.on("m.room.message", handler);

// Later...
ctx.matrix.off("m.room.message", handler);

API Reference

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

// 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

// 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

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

ctx.notify('Hello, world!');

Detailed Notification

ctx.notify({
  title: 'Plugin Alert',
  body: 'Something important happened!',
  type: 'info',        // 'info' | 'success' | 'error' | 'warning'
  duration: 5000       // milliseconds (optional)
});

Examples

// 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

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.

// 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

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

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

// 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

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.

// 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

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:

/**
 * 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:

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:

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:

const maxRetries = ctx.settings.get('maxRetries') || 3;
const keywords = (ctx.settings.get('keywords') || '').split(',').filter(k => k.trim());

Logging

Use appropriate log levels:

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


License

MIT License - See LICENSE file for details