moved docs
This commit is contained in:
@@ -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)
|
|
||||||
172
docs/API-QUICKSTART.md
Normal file
172
docs/API-QUICKSTART.md
Normal file
@@ -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)
|
||||||
537
docs/API.md
Normal file
537
docs/API.md
Normal file
@@ -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
|
||||||
157
docs/PLUGINS.md
Normal file
157
docs/PLUGINS.md
Normal file
@@ -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.
|
||||||
1282
docs/PLUGIN_API.md
Normal file
1282
docs/PLUGIN_API.md
Normal file
File diff suppressed because it is too large
Load Diff
432
docs/PLUGIN_BUTTON_API.md
Normal file
432
docs/PLUGIN_BUTTON_API.md
Normal file
@@ -0,0 +1,432 @@
|
|||||||
|
# Plugin Button Registration API
|
||||||
|
|
||||||
|
An easy way to register plugin buttons in various UI locations with positioning and grouping support.
|
||||||
|
|
||||||
|
## Example Plugins
|
||||||
|
|
||||||
|
- **[example-button-plugin/](example-button-plugin/)** - Simple examples showing basic usage patterns
|
||||||
|
- **[example-showcase-plugin/](example-showcase-plugin/)** - Complete demonstration of ALL 11 locations ⭐
|
||||||
|
|
||||||
|
## UI Locations
|
||||||
|
|
||||||
|
Buttons render in two visual styles depending on location — **nav list rows** (icon + label, full width) or **icon buttons** (compact, toolbar/header style).
|
||||||
|
|
||||||
|
### Nav List Rows
|
||||||
|
|
||||||
|
These render as full-width list entries matching the style of built-in items like "Create Room" and "Message Search":
|
||||||
|
|
||||||
|
| 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` | Direct Messages panel, below "Create Chat" and above the CHATS dropdown |
|
||||||
|
|
||||||
|
### Icon Buttons
|
||||||
|
|
||||||
|
These render as compact icon buttons inline with other toolbar/header controls:
|
||||||
|
|
||||||
|
| 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 button |
|
||||||
|
| `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 — appears in **two** places: above the Explore Servers icon, and above the Search icon in the sticky bottom section |
|
||||||
|
|
||||||
|
> 💡 **Tip:** Install the [example-showcase-plugin](example-showcase-plugin/) to see exactly where each location appears in the UI!
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
|
||||||
|
### Simple Button
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
module.exports = {
|
||||||
|
name: 'my-plugin',
|
||||||
|
version: '1.0.0',
|
||||||
|
|
||||||
|
onLoad: (context) => {
|
||||||
|
context.ui.registerButton({
|
||||||
|
id: 'my-button',
|
||||||
|
location: 'text-composer-toolbar',
|
||||||
|
label: 'My Action',
|
||||||
|
icon: '🎨',
|
||||||
|
onClick: () => {
|
||||||
|
context.log('Button clicked!');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onUnload: () => {
|
||||||
|
// Buttons are automatically cleaned up on plugin unload
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Positioning
|
||||||
|
|
||||||
|
### Before/After Positioning
|
||||||
|
|
||||||
|
Place your button before or after existing buttons:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
context.ui.registerButton({
|
||||||
|
id: 'my-button',
|
||||||
|
location: 'text-composer-toolbar',
|
||||||
|
label: 'My Action',
|
||||||
|
icon: '✨',
|
||||||
|
position: {
|
||||||
|
before: 'emoji-picker-button', // Place before emoji picker
|
||||||
|
// OR
|
||||||
|
after: 'sticker-button' // Place after sticker button
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Grouping Buttons
|
||||||
|
|
||||||
|
Group related buttons together:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
context.ui.registerButton({
|
||||||
|
id: 'action-1',
|
||||||
|
location: 'text-composer-toolbar',
|
||||||
|
label: 'Action 1',
|
||||||
|
icon: '1️⃣',
|
||||||
|
position: {
|
||||||
|
group: 'my-plugin-tools',
|
||||||
|
order: 1 // Lower numbers appear first
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
context.ui.registerButton({
|
||||||
|
id: 'action-2',
|
||||||
|
location: 'text-composer-toolbar',
|
||||||
|
label: 'Action 2',
|
||||||
|
icon: '2️⃣',
|
||||||
|
position: {
|
||||||
|
group: 'my-plugin-tools',
|
||||||
|
order: 2
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Combined Positioning and Grouping
|
||||||
|
|
||||||
|
Place a group before/after other elements:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
context.ui.registerButton({
|
||||||
|
id: 'tool-1',
|
||||||
|
location: 'text-composer-toolbar',
|
||||||
|
label: 'Tool 1',
|
||||||
|
icon: '🔧',
|
||||||
|
position: {
|
||||||
|
group: 'my-tools',
|
||||||
|
after: 'emoji-picker-button',
|
||||||
|
order: 1
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
context.ui.registerButton({
|
||||||
|
id: 'tool-2',
|
||||||
|
location: 'text-composer-toolbar',
|
||||||
|
label: 'Tool 2',
|
||||||
|
icon: '🔨',
|
||||||
|
position: {
|
||||||
|
group: 'my-tools',
|
||||||
|
order: 2
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Complete Example
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
module.exports = {
|
||||||
|
name: 'custom-tools',
|
||||||
|
version: '1.0.0',
|
||||||
|
|
||||||
|
onLoad: (context) => {
|
||||||
|
// Nav list row — appears below "Create Chat" in the DMs panel
|
||||||
|
context.ui.registerButton({
|
||||||
|
id: 'dm-favorites',
|
||||||
|
location: 'direct-messages',
|
||||||
|
label: 'Favourites',
|
||||||
|
icon: '⭐',
|
||||||
|
onClick: () => context.log('Favourites clicked!')
|
||||||
|
});
|
||||||
|
|
||||||
|
// Nav list row — appears in the space channel list
|
||||||
|
context.ui.registerButton({
|
||||||
|
id: 'quick-access',
|
||||||
|
location: 'channel-list',
|
||||||
|
label: 'Quick Access',
|
||||||
|
icon: '⚡',
|
||||||
|
onClick: () => context.log('Quick access clicked!')
|
||||||
|
});
|
||||||
|
|
||||||
|
// Icon button — appears in the message composer toolbar
|
||||||
|
context.ui.registerButton({
|
||||||
|
id: 'format-bold',
|
||||||
|
location: 'text-composer-toolbar',
|
||||||
|
label: 'Bold',
|
||||||
|
icon: '𝐁',
|
||||||
|
position: {
|
||||||
|
group: 'custom-formatting',
|
||||||
|
order: 1
|
||||||
|
},
|
||||||
|
onClick: () => context.log('Bold clicked!')
|
||||||
|
});
|
||||||
|
|
||||||
|
// Icon button — appears in the room header
|
||||||
|
context.ui.registerButton({
|
||||||
|
id: 'special-search',
|
||||||
|
location: 'room-header',
|
||||||
|
label: 'Special Search',
|
||||||
|
icon: '🔍',
|
||||||
|
position: { after: 'search-button' },
|
||||||
|
onClick: () => context.log('Special search clicked!')
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sidebar icon — appears above Explore and above Search
|
||||||
|
context.ui.registerButton({
|
||||||
|
id: 'plugin-panel',
|
||||||
|
location: 'sidebar-actions',
|
||||||
|
label: 'Plugin Panel',
|
||||||
|
icon: '🧩',
|
||||||
|
onClick: () => context.log('Plugin panel clicked!')
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onUnload: () => {
|
||||||
|
// All registered buttons are automatically cleaned up
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Unregistering Buttons
|
||||||
|
|
||||||
|
Buttons are automatically unregistered when the plugin is unloaded. To manually unregister:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
context.ui.unregisterButton('my-button-id');
|
||||||
|
```
|
||||||
|
|
||||||
|
## TypeScript Types
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import type { UIButtonDefinition, UIButtonPosition, UILocation } from '@paarrot/plugin-manager';
|
||||||
|
|
||||||
|
const button: UIButtonDefinition = {
|
||||||
|
id: 'my-button',
|
||||||
|
location: 'text-composer-toolbar',
|
||||||
|
label: 'My Action',
|
||||||
|
icon: '🎨',
|
||||||
|
position: {
|
||||||
|
group: 'my-tools',
|
||||||
|
after: 'emoji-picker',
|
||||||
|
order: 1
|
||||||
|
},
|
||||||
|
onClick: () => console.log('Clicked!')
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
|
||||||
|
### Simple Button
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
module.exports = {
|
||||||
|
name: 'my-plugin',
|
||||||
|
version: '1.0.0',
|
||||||
|
|
||||||
|
onLoad: (context) => {
|
||||||
|
context.ui.registerButton({
|
||||||
|
id: 'my-button',
|
||||||
|
location: 'text-composer-toolbar',
|
||||||
|
label: 'My Action',
|
||||||
|
icon: '🎨',
|
||||||
|
onClick: () => {
|
||||||
|
context.log('Button clicked!');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onUnload: () => {
|
||||||
|
// Buttons are automatically cleaned up on plugin unload
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Positioning
|
||||||
|
|
||||||
|
### Before/After Positioning
|
||||||
|
|
||||||
|
Place your button before or after existing buttons:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
context.ui.registerButton({
|
||||||
|
id: 'my-button',
|
||||||
|
location: 'text-composer-toolbar',
|
||||||
|
label: 'My Action',
|
||||||
|
icon: '✨',
|
||||||
|
position: {
|
||||||
|
before: 'emoji-picker-button', // Place before emoji picker
|
||||||
|
// OR
|
||||||
|
after: 'sticker-button' // Place after sticker button
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Grouping Buttons
|
||||||
|
|
||||||
|
Group related buttons together:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
context.ui.registerButton({
|
||||||
|
id: 'action-1',
|
||||||
|
location: 'text-composer-toolbar',
|
||||||
|
label: 'Action 1',
|
||||||
|
icon: '1️⃣',
|
||||||
|
position: {
|
||||||
|
group: 'my-plugin-tools',
|
||||||
|
order: 1 // Lower numbers appear first
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
context.ui.registerButton({
|
||||||
|
id: 'action-2',
|
||||||
|
location: 'text-composer-toolbar',
|
||||||
|
label: 'Action 2',
|
||||||
|
icon: '2️⃣',
|
||||||
|
position: {
|
||||||
|
group: 'my-plugin-tools',
|
||||||
|
order: 2
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Combined Positioning and Grouping
|
||||||
|
|
||||||
|
Place a group before/after other elements:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
context.ui.registerButton({
|
||||||
|
id: 'tool-1',
|
||||||
|
location: 'text-composer-toolbar',
|
||||||
|
label: 'Tool 1',
|
||||||
|
icon: '🔧',
|
||||||
|
position: {
|
||||||
|
group: 'my-tools',
|
||||||
|
after: 'emoji-picker-button',
|
||||||
|
order: 1
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
context.ui.registerButton({
|
||||||
|
id: 'tool-2',
|
||||||
|
location: 'text-composer-toolbar',
|
||||||
|
label: 'Tool 2',
|
||||||
|
icon: '🔨',
|
||||||
|
position: {
|
||||||
|
group: 'my-tools',
|
||||||
|
order: 2
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Complete Example
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
module.exports = {
|
||||||
|
name: 'custom-tools',
|
||||||
|
version: '1.0.0',
|
||||||
|
|
||||||
|
onLoad: (context) => {
|
||||||
|
// Nav list row — appears below "Create Chat" in the DMs panel
|
||||||
|
context.ui.registerButton({
|
||||||
|
id: 'dm-favorites',
|
||||||
|
location: 'direct-messages',
|
||||||
|
label: 'Favourites',
|
||||||
|
icon: '⭐',
|
||||||
|
onClick: () => context.log('Favourites clicked!')
|
||||||
|
});
|
||||||
|
|
||||||
|
// Nav list row — appears in the space channel list
|
||||||
|
context.ui.registerButton({
|
||||||
|
id: 'quick-access',
|
||||||
|
location: 'channel-list',
|
||||||
|
label: 'Quick Access',
|
||||||
|
icon: '⚡',
|
||||||
|
onClick: () => context.log('Quick access clicked!')
|
||||||
|
});
|
||||||
|
|
||||||
|
// Icon button — appears in the message composer toolbar
|
||||||
|
context.ui.registerButton({
|
||||||
|
id: 'format-bold',
|
||||||
|
location: 'text-composer-toolbar',
|
||||||
|
label: 'Bold',
|
||||||
|
icon: '𝐁',
|
||||||
|
position: {
|
||||||
|
group: 'custom-formatting',
|
||||||
|
order: 1
|
||||||
|
},
|
||||||
|
onClick: () => context.log('Bold clicked!')
|
||||||
|
});
|
||||||
|
|
||||||
|
// Icon button — appears in the room header
|
||||||
|
context.ui.registerButton({
|
||||||
|
id: 'special-search',
|
||||||
|
location: 'room-header',
|
||||||
|
label: 'Special Search',
|
||||||
|
icon: '🔍',
|
||||||
|
position: { after: 'search-button' },
|
||||||
|
onClick: () => context.log('Special search clicked!')
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sidebar icon — appears above Explore and above Search
|
||||||
|
context.ui.registerButton({
|
||||||
|
id: 'plugin-panel',
|
||||||
|
location: 'sidebar-actions',
|
||||||
|
label: 'Plugin Panel',
|
||||||
|
icon: '🧩',
|
||||||
|
onClick: () => context.log('Plugin panel clicked!')
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onUnload: () => {
|
||||||
|
// All registered buttons are automatically cleaned up
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Unregistering Buttons
|
||||||
|
|
||||||
|
Buttons are automatically unregistered when the plugin is unloaded. To manually unregister:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
context.ui.unregisterButton('my-button-id');
|
||||||
|
```
|
||||||
|
|
||||||
|
## TypeScript Types
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import type { UIButtonDefinition, UIButtonPosition, UILocation } from '@paarrot/plugin-manager';
|
||||||
|
|
||||||
|
const button: UIButtonDefinition = {
|
||||||
|
id: 'my-button',
|
||||||
|
location: 'text-composer-toolbar',
|
||||||
|
label: 'My Action',
|
||||||
|
icon: '🎨',
|
||||||
|
position: {
|
||||||
|
group: 'my-tools',
|
||||||
|
after: 'emoji-picker',
|
||||||
|
order: 1
|
||||||
|
},
|
||||||
|
onClick: () => console.log('Clicked!')
|
||||||
|
};
|
||||||
|
```
|
||||||
207
docs/PLUGIN_SYSTEM_IMPLEMENTATION.md
Normal file
207
docs/PLUGIN_SYSTEM_IMPLEMENTATION.md
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
# Plugin System Wishlist - Implementation Summary
|
||||||
|
|
||||||
|
All requested features have been implemented! 🎉
|
||||||
|
|
||||||
|
## ✅ Implemented Features
|
||||||
|
|
||||||
|
### 1. ✅ Enhanced Slash Commands
|
||||||
|
- **Simple commands**: `ctx.commands.register({ name: "shrug", run: () => "¯\\_(ツ)_/¯" })`
|
||||||
|
- **Commands with args**: `ctx.commands.register({ name: "echo", args: ["text"], run: ({ text }) => text })`
|
||||||
|
- Automatic argument parsing
|
||||||
|
- Execution API: `ctx.commands.execute(name, args)`
|
||||||
|
|
||||||
|
### 2. ✅ Message Interceptors
|
||||||
|
- **Before send**: `ctx.messages.onBeforeSend((msg) => { msg.content = "modified" })`
|
||||||
|
- **On receive**: `ctx.messages.onReceive((msg) => { /* process */ })`
|
||||||
|
- Full MessageContext with content, roomId, metadata
|
||||||
|
- Async support for interceptors
|
||||||
|
|
||||||
|
### 3. ✅ Custom Renderers & Button Registration
|
||||||
|
- **Register renderer**: `ctx.ui.registerRenderer("message", (msg, defaultRenderer) => { /* render */ })`
|
||||||
|
- **Unregister renderer**: `ctx.ui.unregisterRenderer("message")`
|
||||||
|
- **Register button**: `ctx.ui.registerButton({ id, location, label, icon, onClick })`
|
||||||
|
- **Unregister button**: `ctx.ui.unregisterButton(id)`
|
||||||
|
- 11 UI locations across nav lists, toolbars, headers, sidebar, and menus
|
||||||
|
- Nav list rows (icon + label) for `channel-list`, `home-section`, `direct-messages`
|
||||||
|
- Icon buttons for all other locations
|
||||||
|
- Positioning and grouping support (`before`, `after`, `group`, `order`)
|
||||||
|
- Auto-cleanup on plugin unload
|
||||||
|
- See [PLUGIN_BUTTON_API.md](PLUGIN_BUTTON_API.md) for full reference
|
||||||
|
|
||||||
|
### 4. ✅ Settings UI Per Plugin
|
||||||
|
- **Define schema**: `ctx.settings.define({ theme: { type: "select", options: [...] } })`
|
||||||
|
- **Supported types**: string, number, boolean, select, color
|
||||||
|
- **Get/Set**: `ctx.settings.get(key)` / `ctx.settings.set(key, value)`
|
||||||
|
- Auto-persisted to localStorage
|
||||||
|
- No JSON editing needed!
|
||||||
|
|
||||||
|
### 5. ✅ Hot Reload
|
||||||
|
- **API**: `pluginRegistry.reloadPlugin(pluginId, newPlugin, newContext)`
|
||||||
|
- Auto-cleanup of old plugin resources
|
||||||
|
- Maintains enabled/disabled state
|
||||||
|
- Dev-friendly workflow
|
||||||
|
|
||||||
|
### 6. ✅ Dependency System
|
||||||
|
- **Declare**: `dependencies: { "paarrot.core-utils": "^1.0.0" }`
|
||||||
|
- **Require**: `const utils = ctx.require("paarrot.utils")`
|
||||||
|
- Error handling for missing dependencies
|
||||||
|
- Plugin exports system
|
||||||
|
|
||||||
|
### 7. ✅ Raw Matrix Events Hook
|
||||||
|
- **Full SDK access**: `ctx.matrix.on("m.room.message", (ev) => { })`
|
||||||
|
- **Any event type**: Room.timeline, sync, RoomState.events, etc.
|
||||||
|
- Direct Matrix.js SDK integration
|
||||||
|
- Power users will go nuts!
|
||||||
|
|
||||||
|
### 8. ✅ Background Tasks / Intervals
|
||||||
|
- **Interval**: `ctx.timers.setInterval(() => { /* cursed shit */ }, 10000)`
|
||||||
|
- **Timeout**: `ctx.timers.setTimeout(() => { }, 5000)`
|
||||||
|
- **Clear**: `ctx.timers.clearInterval(id)` / `ctx.timers.clearTimeout(id)`
|
||||||
|
- Auto-cleanup on plugin unload
|
||||||
|
- Perfect for bots, reminders, auto-cleanup
|
||||||
|
|
||||||
|
### 9. ✅ Notifications API
|
||||||
|
- **Simple**: `ctx.notify("Oi")`
|
||||||
|
- **Advanced**: `ctx.notify({ title: "Oi", body: "New message from Steve", type: "info" })`
|
||||||
|
- Action buttons support
|
||||||
|
- Custom duration
|
||||||
|
|
||||||
|
### 10. ✅ Plugin Logs Panel
|
||||||
|
- **Logging**: `ctx.log()`, `ctx.warn()`, `ctx.error()`
|
||||||
|
- Per-plugin log storage
|
||||||
|
- Visible in `pluginRegistry.getLogs(pluginId)`
|
||||||
|
- Cleared on plugin unload
|
||||||
|
- Max 1000 logs retained
|
||||||
|
|
||||||
|
## 🎯 Bonus Features
|
||||||
|
|
||||||
|
- **Plugin exports**: Share functionality between plugins
|
||||||
|
- **Automatic cleanup**: Timers, handlers, resources auto-cleaned on unload
|
||||||
|
- **Error isolation**: Plugin errors don't crash the app
|
||||||
|
- **Type safety**: Full TypeScript definitions
|
||||||
|
- **Sandboxed execution**: Plugins run in controlled environment
|
||||||
|
- **Rich context**: Full Matrix client, React, all APIs
|
||||||
|
|
||||||
|
## 📁 Files Changed
|
||||||
|
|
||||||
|
### Core System
|
||||||
|
- [`PluginAPI.ts`](cinny/src/app/features/settings/plugins/PluginAPI.ts) - Enhanced with all APIs
|
||||||
|
- [`PluginLoader.tsx`](cinny/src/app/features/settings/plugins/PluginLoader.tsx) - Complete rewrite with context creation
|
||||||
|
- [`index.ts`](cinny/src/app/features/settings/plugins/index.ts) - Exports updated
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
- [`PLUGIN_DEVELOPMENT.md`](cinny/src/app/features/settings/plugins/PLUGIN_DEVELOPMENT.md) - Comprehensive guide
|
||||||
|
- [`PLUGINS.md`](PLUGINS.md) - User guide
|
||||||
|
- [`example-plugin/index.js`](example-plugin/index.js) - Full-featured example
|
||||||
|
|
||||||
|
### Integration
|
||||||
|
- [`ClientRoot.tsx`](cinny/src/app/pages/client/ClientRoot.tsx) - PluginLoader integrated
|
||||||
|
- [`main.js`](electron/main.js) - IPC handler for reading plugin code
|
||||||
|
- [`preload.js`](electron/preload.js) - readPluginCode API exposed
|
||||||
|
- [`ext.d.ts`](cinny/src/ext.d.ts) - TypeScript definitions
|
||||||
|
|
||||||
|
## 🚀 Usage Examples
|
||||||
|
|
||||||
|
### Command with Args
|
||||||
|
```javascript
|
||||||
|
ctx.commands.register({
|
||||||
|
name: "echo",
|
||||||
|
args: ["text"],
|
||||||
|
run: ({ text }) => text
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Message Interceptor
|
||||||
|
```javascript
|
||||||
|
ctx.messages.onBeforeSend((msg) => {
|
||||||
|
if (msg.content === "brb") {
|
||||||
|
msg.content = "be right back ya legend";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Settings
|
||||||
|
```javascript
|
||||||
|
ctx.settings.define({
|
||||||
|
theme: { type: "select", options: ["dark", "light"] },
|
||||||
|
spamFilter: { type: "boolean", default: true }
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Background Tasks
|
||||||
|
```javascript
|
||||||
|
ctx.timers.setInterval(() => {
|
||||||
|
// do cursed shit every 10s
|
||||||
|
}, 10000);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Raw Matrix Events
|
||||||
|
```javascript
|
||||||
|
ctx.matrix.on("m.room.message", (ev) => {
|
||||||
|
ctx.log('Message:', ev.getContent().body);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Plugin Dependencies
|
||||||
|
```javascript
|
||||||
|
const utils = ctx.require("paarrot.utils");
|
||||||
|
utils.doSomething();
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎨 Plugin API Summary
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface PluginContext {
|
||||||
|
pluginId: string;
|
||||||
|
matrixClient: MatrixClient;
|
||||||
|
React: typeof import('react');
|
||||||
|
|
||||||
|
commands: {
|
||||||
|
register: (command: PluginCommand) => void;
|
||||||
|
unregister: (name: string) => void;
|
||||||
|
execute: (name: string, args: Record<string, any>) => Promise<string | void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
messages: {
|
||||||
|
onBeforeSend: (interceptor: MessageInterceptor) => void;
|
||||||
|
onReceive: (interceptor: MessageInterceptor) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
ui: {
|
||||||
|
registerButton: (button: UIButtonDefinition) => void;
|
||||||
|
unregisterButton: (id: string) => void;
|
||||||
|
registerRenderer: (type: string, renderer: CustomRenderer) => void;
|
||||||
|
unregisterRenderer: (type: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
settings: {
|
||||||
|
define: (schema: SettingsSchema) => void;
|
||||||
|
get: <T = any>(key: string) => T | undefined;
|
||||||
|
set: (key: string, value: any) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
matrix: {
|
||||||
|
on: (eventType: string, handler: (event: MatrixEvent) => void) => void;
|
||||||
|
off: (eventType: string, handler: (event: MatrixEvent) => void) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
timers: {
|
||||||
|
setInterval: (callback: () => void, ms: number) => number;
|
||||||
|
setTimeout: (callback: () => void, ms: number) => number;
|
||||||
|
clearInterval: (id: number) => void;
|
||||||
|
clearTimeout: (id: number) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
notify: (options: NotificationOptions | string) => void;
|
||||||
|
log: (...args: any[]) => void;
|
||||||
|
warn: (...args: any[]) => void;
|
||||||
|
error: (...args: any[]) => void;
|
||||||
|
require: (pluginId: string) => any;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔥 The Result
|
||||||
|
|
||||||
|
A **production-ready plugin system** that rivals Discord, VSCode, and other extensible apps. Plugin developers will have everything they need to build powerful extensions without fighting the framework.
|
||||||
|
|
||||||
|
**No compromises. All features implemented. Ready to ship.**
|
||||||
Reference in New Issue
Block a user