diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index fea421b..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,44 +0,0 @@ -# Contributing to Cinny - -First off, thanks for taking the time to contribute! ❤️ - -All types of contributions are encouraged and valued. Please make sure to read the relevant section before making your contribution. It will make it a lot easier for us maintainers and smooth out the experience for all involved. The community looks forward to your contributions. 🎉 - -> And if you like the project, but just don't have time to contribute, that's fine. There are other easy ways to support the project and show your appreciation, which we would also be very happy about: -> - Star the project -> - Tweet about it (tag @cinnyapp) -> - Refer this project in your project's readme -> - Mention the project at local meetups and tell your friends/colleagues -> - [Donate to us](https://cinny.in/#sponsor) - -## Bug reports - -Bug reports and feature suggestions must use descriptive and concise titles and be submitted to [GitHub Issues](https://github.com/ajbura/cinny/issues). Please use the search function to make sure that you are not submitting duplicates, and that a similar report or request has not already been resolved or rejected. - -## Pull requests - -> ### Legal Notice -> When contributing to this project, you must agree that you have authored 100% of the content, that you have the necessary rights to the content and that the content you contribute may be provided under the project license. - -**NOTE: If you want to add new features, please discuss with maintainers before coding or opening a pull request.** This is to ensure that we are on same track and following our roadmap. - -**Please use clean, concise titles for your pull requests.** We use commit squashing, so the final commit in the dev branch will carry the title of the pull request. For easier sorting in changelog, start your pull request titles using one of the verbs "Add", "Change", "Remove", or "Fix" (present tense). - -Example: - -|Not ideal|Better| -|---|----| -|Fixed markAllAsRead in RoomTimeline|Fix read marker when paginating room timeline| - -It is not always possible to phrase every change in such a manner, but it is desired. - -**The smaller the set of changes in the pull request is, the quicker it can be reviewed and merged.** Splitting tasks into multiple smaller pull requests is often preferable. - -Also, we use [ESLint](https://eslint.org/) for clean and stylistically consistent code syntax, so make sure your pull request follow it. - -**For any query or design discussion, join our [Matrix room](https://matrix.to/#/#cinny:matrix.org).** - -## Helpful links -- [BEM methodology](http://getbem.com/introduction/) -- [Atomic design](https://bradfrost.com/blog/post/atomic-web-design/) -- [Matrix JavaScript SDK documentation](https://matrix-org.github.io/matrix-js-sdk/index.html) diff --git a/docs/API-QUICKSTART.md b/docs/API-QUICKSTART.md new file mode 100644 index 0000000..ffbfaf6 --- /dev/null +++ b/docs/API-QUICKSTART.md @@ -0,0 +1,172 @@ +# Paarrot API Quick Start + +A local HTTP API server for controlling Paarrot from external devices like Stream Deck, scripts, and automation tools. + +## 🚀 Getting Started + +The API server starts automatically when you run Paarrot. It listens on `http://127.0.0.1:33384`. + +### Test the API + +**Option 1: Using Postman** + +Import the Postman collection for easy testing: +1. Open Postman +2. Click **Import** → **File** +3. Select `paarrot-api.postman_collection.json` +4. All endpoints will be ready to use! + +The collection is automatically updated on git push. + +**Option 2: Using the test script** +```bash +node test-api.js +``` + +**Option 3: Using curl** +```bash +./test-api.sh +``` + +**Option 4: Manual curl commands** +```bash +# Health check +curl http://127.0.0.1:33384/health + +# Toggle mute +curl -X POST http://127.0.0.1:33384/mute/toggle + +# Send message +curl -X POST http://127.0.0.1:33384/message/current \ + -H "Content-Type: application/json" \ + -d '{"message": "Hello from API!"}' +``` + +## 📝 Quick Reference + +### Common Endpoints + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/health` | GET | Check if API is running | +| `/status` | GET | Get app status (mute, deafen, etc.) | +| `/mute/toggle` | POST | Toggle microphone mute | +| `/deafen/toggle` | POST | Toggle deafen | +| `/channels` | GET | Get list of rooms | +| `/channel` | POST | Switch to a room | +| `/message/current` | POST | Send message to current room | + +## 🎮 Stream Deck Integration + +1. Install the "API Ninja" or "HTTP Request" plugin +2. Create buttons with these settings: + +**Mute Toggle Button:** +- URL: `http://127.0.0.1:33384/mute/toggle` +- Method: POST + +**Quick Message Button:** +- URL: `http://127.0.0.1:33384/message/current` +- Method: POST +- Body: `{"message": "BRB!"}` + +## 🔧 Integration with Paarrot + +To enable full functionality, you need to implement the action handlers in the Cinny frontend: + +1. Import the API handler in your app initialization: +```typescript +import { initPaarrotAPI } from './app/paarrot-api'; + +// After Matrix client is initialized +initPaarrotAPI(matrixClient); +``` + +2. Implement the TODO items in `cinny/src/app/paarrot-api.ts`: + - Mute/unmute logic (WebRTC audio) + - Deafen logic (WebRTC audio output) + - Navigation to rooms (router integration) + - Get current room from URL/state + +## 📖 Full Documentation + +See [API.md](API.md) for complete API documentation including: +- All available endpoints +- Request/response formats +- Error handling +- Code examples in multiple languages +- Detailed integration guide + +## 🛠️ Configuration + +Edit `electron/api-server.js` to customize: +- Port number (default: 33384) +- Timeout duration (default: 10s) +- CORS settings + +## 🐛 Troubleshooting + +**API not responding:** +1. Check if Paarrot is running +2. Look for "Paarrot API server listening" in console logs +3. Verify port 33384 is not in use: `lsof -i :33384` + +**Actions not working:** +1. Check browser console for errors +2. Ensure `initPaarrotAPI()` is called in your app +3. Implement the TODO items in `paarrot-api.ts` + +**Port in use:** +Edit `electron/api-server.js` and change the port number in the constructor. + +## 📦 Files + +- `electron/api-server.js` - API server implementation +- `cinny/src/app/paarrot-api.ts` - Client-side handler (needs implementation) +- `test-api.js` - Node.js test script +- `test-api.sh` - Bash test script +- `API.md` - Full documentation + +## 🔐 Security + +- API only listens on localhost (127.0.0.1) +- Not accessible from network +- No authentication required (local only) +- CORS enabled for all origins (safe since localhost only) + +## 💡 Examples + +**Python Script:** +```python +import requests + +def toggle_mute(): + r = requests.post('http://127.0.0.1:33384/mute/toggle') + print(r.json()) + +toggle_mute() +``` + +**JavaScript:** +```javascript +async function sendQuickMessage(msg) { + const res = await fetch('http://127.0.0.1:33384/message/current', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ message: msg }) + }); + return res.json(); +} +``` + +## 🎯 Next Steps + +1. Run Paarrot: `npm run dev` +2. Test the API: `node test-api.js` +3. Integrate the handler: Import and call `initPaarrotAPI()` +4. Implement the TODO items in `paarrot-api.ts` +5. Create Stream Deck buttons or automation scripts! + +--- + +For more details, see [API.md](API.md) diff --git a/docs/API.md b/docs/API.md new file mode 100644 index 0000000..14ec122 --- /dev/null +++ b/docs/API.md @@ -0,0 +1,537 @@ +# Paarrot API Server Documentation + +The Paarrot API server provides a local HTTP API for controlling the Paarrot desktop app from external applications like Stream Deck, keyboard macros, scripts, and other automation tools. + +## Server Details + +- **Base URL**: `http://127.0.0.1:33384` +- **Protocol**: HTTP +- **Content-Type**: `application/json` +- **CORS**: Enabled for all origins + +## Authentication + +Currently, no authentication is required. The API server only listens on localhost (127.0.0.1) for security. + +## API Endpoints + +### Health Check + +Check if the API server is running. + +**Endpoint**: `GET /health` + +**Response**: +```json +{ + "status": "ok", + "app": "Paarrot API", + "version": "1.0.0" +} +``` + +--- + +### Get Status + +Get the current status of the app (mute, deafen, current room, etc.). + +**Endpoint**: `GET /status` + +**Response**: +```json +{ + "success": true, + "data": { + "muted": false, + "deafened": false, + "currentRoom": "!abc123:matrix.org", + "connected": true + } +} +``` + +--- + +### Mute/Unmute + +Set the mute status. + +**Endpoint**: `POST /mute` + +**Request Body**: +```json +{ + "muted": true +} +``` + +**Response**: +```json +{ + "success": true, + "data": { + "muted": true + } +} +``` + +--- + +### Toggle Mute + +Toggle the current mute status. + +**Endpoint**: `POST /mute/toggle` + +**Response**: +```json +{ + "success": true, + "data": { + "muted": true + } +} +``` + +--- + +### Deafen/Undeafen + +Set the deafen status (deafening also mutes). + +**Endpoint**: `POST /deafen` + +**Request Body**: +```json +{ + "deafened": true +} +``` + +**Response**: +```json +{ + "success": true, + "data": { + "deafened": true + } +} +``` + +--- + +### Toggle Deafen + +Toggle the current deafen status. + +**Endpoint**: `POST /deafen/toggle` + +**Response**: +```json +{ + "success": true, + "data": { + "deafened": true + } +} +``` + +--- + +### Change Channel/Room + +Switch to a different Matrix room. + +**Endpoint**: `POST /channel` + +**Request Body**: +```json +{ + "roomId": "!abc123:matrix.org" +} +``` + +**Response**: +```json +{ + "success": true, + "data": { + "roomId": "!abc123:matrix.org", + "roomName": "General" + } +} +``` + +--- + +### Get Channels/Rooms + +Get a list of available rooms/channels. + +**Endpoint**: `GET /channels` + +**Response**: +```json +{ + "success": true, + "data": [ + { + "roomId": "!abc123:matrix.org", + "name": "General", + "server": "matrix.org", + "isDirect": false, + "avatar": null + }, + { + "roomId": "!def456:matrix.org", + "name": "Random", + "server": "matrix.org", + "isDirect": false, + "avatar": null + }, + { + "roomId": "!xyz789:example.com", + "name": "General", + "server": "example.com", + "isDirect": false, + "avatar": null + } + ] +} +``` + +**Note**: The `server` field helps distinguish between rooms with the same name on different servers. + +--- + +### Send Message + +Send a message to a specific room. + +**Endpoint**: `POST /message` + +**Request Body**: +```json +{ + "roomId": "!abc123:matrix.org", + "message": "Hello from the API!" +} +``` + +**Response**: +```json +{ + "success": true, + "data": { + "eventId": "$abc123", + "roomId": "!abc123:matrix.org" + } +} +``` + +--- + +### Send Message to Current Room + +Send a message to the currently active room. + +**Endpoint**: `POST /message/current` + +**Request Body**: +```json +{ + "message": "Hello from the API!" +} +``` + +**Response**: +```json +{ + "success": true, + "data": { + "eventId": "$abc123", + "roomId": "!abc123:matrix.org" + } +} +``` + +--- + +### Get Current Room + +Get information about the currently active room. + +**Endpoint**: `GET /room/current` + +**Response**: +```json +{ + "success": true, + "data": { + "roomId": "!abc123:matrix.org", + "name": "General", + "server": "matrix.org", + "avatar": "mxc://matrix.org/abc123", + "isDirect": false + } +} +``` + +--- + +## Error Responses + +All endpoints return error responses in the following format: + +```json +{ + "success": false, + "error": "Error message here" +} +``` + +Common HTTP status codes: +- `200 OK` - Request successful +- `400 Bad Request` - Missing or invalid parameters +- `404 Not Found` - Endpoint not found +- `500 Internal Server Error` - Server error + +--- + +## Example Usage + +### cURL + +```bash +# Toggle mute +curl -X POST http://127.0.0.1:33384/mute/toggle + +# Send message to current room +curl -X POST http://127.0.0.1:33384/message/current \ + -H "Content-Type: application/json" \ + -d '{"message": "Hello!"}' + +# Change channel +curl -X POST http://127.0.0.1:33384/channel \ + -H "Content-Type: application/json" \ + -d '{"roomId": "!abc123:matrix.org"}' +``` + +### JavaScript/Node.js + +```javascript +const fetch = require('node-fetch'); + +// Toggle mute +async function toggleMute() { + const response = await fetch('http://127.0.0.1:33384/mute/toggle', { + method: 'POST' + }); + const data = await response.json(); + console.log('Muted:', data.data.muted); +} + +// Send message +async function sendMessage(roomId, message) { + const response = await fetch('http://127.0.0.1:33384/message', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ roomId, message }) + }); + const data = await response.json(); + console.log('Message sent:', data.data.eventId); +} +``` + +### Python + +```python +import requests + +# Toggle mute +def toggle_mute(): + response = requests.post('http://127.0.0.1:33384/mute/toggle') + data = response.json() + print('Muted:', data['data']['muted']) + +# Send message +def send_message(room_id, message): + response = requests.post('http://127.0.0.1:33384/message', json={ + 'roomId': room_id, + 'message': message + }) + data = response.json() + print('Message sent:', data['data']['eventId']) +``` + +--- + +## Stream Deck Integration + +To integrate with Stream Deck: + +1. Install the "API Ninja" or "HTTP Request" plugin for Stream Deck +2. Create a button with the following configuration: + - **URL**: `http://127.0.0.1:33384/mute/toggle` + - **Method**: POST + - **Headers**: `Content-Type: application/json` + +Example configurations: + +**Mute Toggle Button**: +- URL: `http://127.0.0.1:33384/mute/toggle` +- Method: POST + +**Deafen Toggle Button**: +- URL: `http://127.0.0.1:33384/deafen/toggle` +- Method: POST + +**Send Quick Message**: +- URL: `http://127.0.0.1:33384/message/current` +- Method: POST +- Body: `{"message": "BRB!"}` + +--- + +## Client-Side Implementation + +To enable the API functionality in the Paarrot app, you need to implement the action handlers in your Matrix client code. The API server sends actions via IPC to the renderer process, which should handle them and respond. + +### Example Handler (TypeScript/React) + +Add this to your app initialization: + +```typescript +// Listen for API actions from the Electron API server +if (window.electron?.api?.onAction) { + window.electron.api.onAction(async (action: { + action: string; + params: any; + responseChannel: string; + }) => { + try { + let result; + + switch (action.action) { + case 'toggle-mute': + // Toggle microphone mute + result = await toggleMicrophone(); + break; + + case 'set-mute': + // Set microphone mute state + result = await setMicrophone(action.params.muted); + break; + + case 'toggle-deafen': + // Toggle deafen (mute speakers) + result = await toggleDeafen(); + break; + + case 'set-deafen': + // Set deafen state + result = await setDeafen(action.params.deafened); + break; + + case 'change-channel': + // Navigate to a different room + result = await changeRoom(action.params.roomId); + break; + + case 'get-channels': + // Get list of rooms + result = await getRoomList(); + break; + + case 'send-message': + // Send message to specific room + result = await sendMessage(action.params.roomId, action.params.message); + break; + + case 'send-message-current': + // Send message to current room + result = await sendMessageToCurrent(action.params.message); + break; + + case 'get-current-room': + // Get current room info + result = await getCurrentRoomInfo(); + break; + + case 'get-status': + // Get current app status + result = await getAppStatus(); + break; + + default: + throw new Error(`Unknown action: ${action.action}`); + } + + // Send success response back + window.electron.api.sendResponse(action.responseChannel, { + success: true, + data: result + }); + } catch (error) { + // Send error response back + window.electron.api.sendResponse(action.responseChannel, { + success: false, + error: error.message + }); + } + }); +} +``` + +--- + +## Configuration + +The API server can be configured by modifying `electron/api-server.js`: + +- **Default Port**: 33384 (can be changed in the constructor) +- **Bind Address**: 127.0.0.1 (localhost only for security) +- **Timeout**: 10 seconds for actions + +--- + +## Security Notes + +1. The API server only listens on localhost (127.0.0.1) and is not accessible from the network +2. No authentication is implemented - anyone with local access can control the app +3. Consider adding API key authentication if needed for your use case +4. CORS is enabled for all origins since it's localhost only + +--- + +## Troubleshooting + +**API server not starting**: +- Check if port 33384 is already in use +- Check console logs for errors +- Verify Express is installed: `npm install express cors body-parser` + +**Actions not working**: +- Ensure the client-side handler is implemented +- Check browser console for errors +- Verify the action names match between API server and client + +**Timeout errors**: +- Actions must respond within 10 seconds +- Check if the Matrix client is initialized +- Verify the action handler is registered + +--- + +## Future Enhancements + +Potential features to add: +- WebSocket support for real-time events +- Authentication with API keys +- Voice channel controls (join/leave) +- User status updates +- Notification controls +- Screen sharing controls diff --git a/docs/PLUGINS.md b/docs/PLUGINS.md new file mode 100644 index 0000000..7a21844 --- /dev/null +++ b/docs/PLUGINS.md @@ -0,0 +1,157 @@ +# Paarrot Plugin System + +The Paarrot desktop client includes a powerful plugin system that allows you to extend and customize your Matrix experience. + +## What are Plugins? + +Plugins are small JavaScript modules that run inside Paarrot and can: +- Add custom commands +- Modify the UI +- React to Matrix events +- Add new settings sections +- Enhance functionality + +## Installing Plugins + +### From the Plugin Marketplace + +1. Open **Settings** → **Plugins** +2. Browse the **Marketplace** tab +3. Click **Install** on any plugin you want to add +4. The plugin will be downloaded and enabled automatically + +### Manual Installation + +1. Download a plugin ZIP file +2. Extract it to your plugins directory: + - **Windows**: `%APPDATA%\paarrot\plugins\` + - **Linux**: `~/.config/Paarrot/plugins/` + - **macOS**: `~/Library/Application Support/Paarrot/plugins/` +3. Restart Paarrot +4. Enable the plugin in **Settings** → **Plugins** → **Installed** + +## Managing Plugins + +### Viewing Installed Plugins + +Go to **Settings** → **Plugins** → **Installed** to see all installed plugins. + +### Enabling/Disabling Plugins + +Click the toggle switch next to any plugin to enable or disable it. Disabled plugins won't run or use resources. + +### Uninstalling Plugins + +Click the **Uninstall** button next to any plugin to remove it completely. + +### Searching Plugins + +Use the search bar to filter plugins by name, description, author, or tags. + +## Plugin Security + +⚠️ **Important Security Information** + +Plugins run with access to your Matrix client and can: +- Read your messages +- Send messages on your behalf +- Access your room list +- Modify app behavior + +**Only install plugins from trusted sources!** + +### Safety Tips + +1. **Check the author**: Install plugins from known developers +2. **Read reviews**: Look for community feedback +3. **Review permissions**: Understand what the plugin can do +4. **Keep updated**: Update plugins regularly for security fixes +5. **Report issues**: If a plugin behaves suspiciously, report it immediately + +## Developing Plugins + +Want to create your own plugin? Check out the [Plugin API Reference](docs/PLUGIN_API.md). + +### Quick Start + +1. Create a plugin directory with: + - `index.js` - Your plugin code + - `plugin-metadata.json` - Plugin information + +2. Implement the plugin interface: +```javascript +module.exports = { + onLoad: async (context) => { + // Your plugin initialization + console.log('Plugin loaded!'); + }, + onUnload: async () => { + // Cleanup when disabled + } +}; +``` + +3. Test locally by copying to your plugins directory + +4. Publish to the [Plugin Directory](https://github.com/Paarrot/Plugin-Directory) + +## Plugin API + +Plugins have access to: +- **Matrix Client**: Full matrix-js-sdk client instance +- **React**: For building UI components +- **Commands**: Register custom slash commands +- **UI Buttons**: Inject buttons into 11 locations across the app (nav lists, toolbars, headers, sidebar) +- **UI Renderers**: Custom message and content renderers +- **Settings**: Add custom settings sections per plugin +- **Themes**: Register custom themes that appear in the theme selector +- **Matrix Events**: Hook into raw Matrix events +- **Timers**: Background intervals and timeouts, auto-cleaned on unload +- **Notifications**: System notifications + +See the [Plugin API Reference](docs/PLUGIN_API.md) and [Button Registration API](docs/PLUGIN_BUTTON_API.md) for complete documentation. + +## Example Plugins + +Check out the `example-plugin/` directory for a simple example, or the `plugins/example-showcase-plugin/` for a full demonstration of all 11 UI button locations. + +## Troubleshooting + +### Plugin Won't Load + +1. Check the browser console for errors (F12 → Console) +2. Look for `[PluginLoader]` messages +3. Verify `index.js` exists in the plugin directory +4. Ensure `plugin-metadata.json` is valid JSON + +### Plugin Crashes + +1. Disable the plugin in Settings → Plugins +2. Check console for error stack traces +3. Report the issue to the plugin author +4. Try reinstalling the plugin + +### Performance Issues + +If Paarrot becomes slow: +1. Disable plugins one by one to identify the culprit +2. Check for memory leaks in the console +3. Contact the plugin author with details + +## Plugin Directory + +The official Paarrot Plugin Directory is maintained at: +https://github.com/Paarrot/Plugin-Directory + +Submit your plugins there to make them available in the marketplace! + +## Support + +- **Plugin API Reference**: [docs/PLUGIN_API.md](docs/PLUGIN_API.md) +- **Button API Reference**: [docs/PLUGIN_BUTTON_API.md](docs/PLUGIN_BUTTON_API.md) +- **Issues**: https://github.com/Paarrot/cinny-desktop/issues +- **Community**: Join our Matrix room for plugin development help + +## License + +Plugins are independent works and have their own licenses. Check each plugin's metadata for license information. diff --git a/docs/PLUGIN_API.md b/docs/PLUGIN_API.md new file mode 100644 index 0000000..b1d89c4 --- /dev/null +++ b/docs/PLUGIN_API.md @@ -0,0 +1,1282 @@ +# 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\\` +- **Linux**: `~/.config/Paarrot/plugins//` +- **macOS**: `~/Library/Application Support/Paarrot/plugins//` + +### 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): Promise; +} + +interface PluginCommand { + name: string; + description: string; + args?: string[]; + run: (args: Record) => string | void | Promise; +} +``` + +--- + +### 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; +} + +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(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; +``` + +--- + +### 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 `