feat: Add Paarrot API server with endpoints for controlling app features
- Implemented API server using Express, including endpoints for health check, status, mute, deafen, channel management, and message sending. - Added middleware for CORS and JSON body parsing. - Created test scripts (Node.js and Bash) for API endpoint verification. - Documented API usage and integration in new API documentation files. - Updated package.json to include necessary dependencies (express, cors, body-parser).
This commit is contained in:
162
API-QUICKSTART.md
Normal file
162
API-QUICKSTART.md
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
# 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 the test script**
|
||||||
|
```bash
|
||||||
|
node test-api.js
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option 2: Using curl**
|
||||||
|
```bash
|
||||||
|
./test-api.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option 3: 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)
|
||||||
523
API.md
Normal file
523
API.md
Normal file
@@ -0,0 +1,523 @@
|
|||||||
|
# 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",
|
||||||
|
"isDirect": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"roomId": "!def456:matrix.org",
|
||||||
|
"name": "Random",
|
||||||
|
"isDirect": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 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",
|
||||||
|
"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
|
||||||
2
cinny
2
cinny
Submodule cinny updated: 6a31ea64ed...ef55d1583f
267
electron/api-server.js
Normal file
267
electron/api-server.js
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const cors = require('cors');
|
||||||
|
const bodyParser = require('body-parser');
|
||||||
|
|
||||||
|
class PaarrotAPIServer {
|
||||||
|
constructor(mainWindow) {
|
||||||
|
this.mainWindow = mainWindow;
|
||||||
|
this.app = express();
|
||||||
|
this.server = null;
|
||||||
|
this.port = 33384; // Default port for Paarrot API
|
||||||
|
|
||||||
|
this.setupMiddleware();
|
||||||
|
this.setupRoutes();
|
||||||
|
}
|
||||||
|
|
||||||
|
setupMiddleware() {
|
||||||
|
// Enable CORS for all origins (you can restrict this if needed)
|
||||||
|
this.app.use(cors());
|
||||||
|
|
||||||
|
// Parse JSON bodies
|
||||||
|
this.app.use(bodyParser.json());
|
||||||
|
|
||||||
|
// Log all requests
|
||||||
|
this.app.use((req, res, next) => {
|
||||||
|
console.log(`Paarrot API: ${req.method} ${req.path}`);
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setupRoutes() {
|
||||||
|
// Health check
|
||||||
|
this.app.get('/health', (req, res) => {
|
||||||
|
res.json({ status: 'ok', app: 'Paarrot API', version: '1.0.0' });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get current status
|
||||||
|
this.app.get('/status', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const status = await this.executeAction('get-status');
|
||||||
|
res.json({ success: true, data: status });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ success: false, error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mute/unmute
|
||||||
|
this.app.post('/mute', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { muted } = req.body;
|
||||||
|
const result = await this.executeAction('set-mute', { muted });
|
||||||
|
res.json({ success: true, data: result });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ success: false, error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Toggle mute
|
||||||
|
this.app.post('/mute/toggle', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const result = await this.executeAction('toggle-mute');
|
||||||
|
res.json({ success: true, data: result });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ success: false, error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Deafen/undeafen
|
||||||
|
this.app.post('/deafen', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { deafened } = req.body;
|
||||||
|
const result = await this.executeAction('set-deafen', { deafened });
|
||||||
|
res.json({ success: true, data: result });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ success: false, error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Toggle deafen
|
||||||
|
this.app.post('/deafen/toggle', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const result = await this.executeAction('toggle-deafen');
|
||||||
|
res.json({ success: true, data: result });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ success: false, error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Change channel/room
|
||||||
|
this.app.post('/channel', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { roomId } = req.body;
|
||||||
|
if (!roomId) {
|
||||||
|
return res.status(400).json({ success: false, error: 'roomId is required' });
|
||||||
|
}
|
||||||
|
const result = await this.executeAction('change-channel', { roomId });
|
||||||
|
res.json({ success: true, data: result });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ success: false, error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get list of rooms/channels
|
||||||
|
this.app.get('/channels', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const result = await this.executeAction('get-channels');
|
||||||
|
res.json({ success: true, data: result });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ success: false, error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send message
|
||||||
|
this.app.post('/message', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { roomId, message } = req.body;
|
||||||
|
if (!roomId || !message) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'roomId and message are required'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const result = await this.executeAction('send-message', { roomId, message });
|
||||||
|
res.json({ success: true, data: result });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ success: false, error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send message to current room
|
||||||
|
this.app.post('/message/current', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { message } = req.body;
|
||||||
|
if (!message) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'message is required'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const result = await this.executeAction('send-message-current', { message });
|
||||||
|
res.json({ success: true, data: result });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ success: false, error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get current room info
|
||||||
|
this.app.get('/room/current', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const result = await this.executeAction('get-current-room');
|
||||||
|
res.json({ success: true, data: result });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ success: false, error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 404 handler
|
||||||
|
this.app.use((req, res) => {
|
||||||
|
res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Endpoint not found',
|
||||||
|
availableEndpoints: [
|
||||||
|
'GET /health',
|
||||||
|
'GET /status',
|
||||||
|
'POST /mute',
|
||||||
|
'POST /mute/toggle',
|
||||||
|
'POST /deafen',
|
||||||
|
'POST /deafen/toggle',
|
||||||
|
'POST /channel',
|
||||||
|
'GET /channels',
|
||||||
|
'POST /message',
|
||||||
|
'POST /message/current',
|
||||||
|
'GET /room/current'
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Error handler
|
||||||
|
this.app.use((err, req, res, next) => {
|
||||||
|
console.error('Paarrot API Error:', err);
|
||||||
|
res.status(500).json({ success: false, error: err.message });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute an action by sending it to the renderer process
|
||||||
|
* @param {string} action - The action to execute
|
||||||
|
* @param {object} params - Parameters for the action
|
||||||
|
* @returns {Promise<any>} - Result from the renderer
|
||||||
|
*/
|
||||||
|
executeAction(action, params = {}) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!this.mainWindow || this.mainWindow.isDestroyed()) {
|
||||||
|
return reject(new Error('Main window is not available'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a unique response channel
|
||||||
|
const responseChannel = `api-response-${Date.now()}-${Math.random()}`;
|
||||||
|
|
||||||
|
// Set up one-time listener for the response
|
||||||
|
const { ipcMain } = require('electron');
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
ipcMain.removeHandler(responseChannel);
|
||||||
|
reject(new Error('Action timed out'));
|
||||||
|
}, 10000); // 10 second timeout
|
||||||
|
|
||||||
|
ipcMain.handleOnce(responseChannel, async (event, result) => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
if (result.success) {
|
||||||
|
resolve(result.data);
|
||||||
|
} else {
|
||||||
|
reject(new Error(result.error || 'Unknown error'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send the action to the renderer
|
||||||
|
this.mainWindow.webContents.send('api-action', {
|
||||||
|
action,
|
||||||
|
params,
|
||||||
|
responseChannel
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
start(port = this.port) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.server = this.app.listen(port, '127.0.0.1', () => {
|
||||||
|
console.log(`Paarrot API server listening on http://127.0.0.1:${port}`);
|
||||||
|
console.log('Available endpoints:');
|
||||||
|
console.log(' GET /health');
|
||||||
|
console.log(' GET /status');
|
||||||
|
console.log(' POST /mute');
|
||||||
|
console.log(' POST /mute/toggle');
|
||||||
|
console.log(' POST /deafen');
|
||||||
|
console.log(' POST /deafen/toggle');
|
||||||
|
console.log(' POST /channel');
|
||||||
|
console.log(' GET /channels');
|
||||||
|
console.log(' POST /message');
|
||||||
|
console.log(' POST /message/current');
|
||||||
|
console.log(' GET /room/current');
|
||||||
|
resolve(port);
|
||||||
|
}).on('error', (err) => {
|
||||||
|
if (err.code === 'EADDRINUSE') {
|
||||||
|
console.error(`Paarrot API: Port ${port} is already in use`);
|
||||||
|
reject(new Error(`Port ${port} is already in use`));
|
||||||
|
} else {
|
||||||
|
console.error('Paarrot API: Failed to start server:', err);
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (this.server) {
|
||||||
|
this.server.close(() => {
|
||||||
|
console.log('Paarrot API server stopped');
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = PaarrotAPIServer;
|
||||||
107
electron/main.js
107
electron/main.js
@@ -6,6 +6,7 @@ const { promisify } = require('util');
|
|||||||
const Store = require('electron-store');
|
const Store = require('electron-store');
|
||||||
const open = require('open');
|
const open = require('open');
|
||||||
const { autoUpdater } = require('electron-updater');
|
const { autoUpdater } = require('electron-updater');
|
||||||
|
const PaarrotAPIServer = require('./api-server');
|
||||||
|
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
const store = new Store();
|
const store = new Store();
|
||||||
@@ -13,6 +14,7 @@ const store = new Store();
|
|||||||
let mainWindow = null;
|
let mainWindow = null;
|
||||||
let tray = null;
|
let tray = null;
|
||||||
let isQuitting = false;
|
let isQuitting = false;
|
||||||
|
let apiServer = null;
|
||||||
|
|
||||||
// Configure auto-updater
|
// Configure auto-updater
|
||||||
autoUpdater.autoDownload = false;
|
autoUpdater.autoDownload = false;
|
||||||
@@ -106,12 +108,35 @@ function createWindow() {
|
|||||||
contextIsolation: true,
|
contextIsolation: true,
|
||||||
nodeIntegration: false,
|
nodeIntegration: false,
|
||||||
webSecurity: true,
|
webSecurity: true,
|
||||||
allowRunningInsecureContent: false
|
allowRunningInsecureContent: false,
|
||||||
|
// Enable navigation gestures (works on macOS and some Linux environments)
|
||||||
|
enableBlinkFeatures: 'OverscrollHistoryNavigation'
|
||||||
},
|
},
|
||||||
icon: getIconPath(process.platform === 'win32' ? 'icon.ico' : 'icon.png'),
|
icon: getIconPath(process.platform === 'win32' ? 'icon.ico' : 'icon.png'),
|
||||||
show: false // Don't show until ready
|
show: false // Don't show until ready
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Enable trackpad swipe gestures for navigation on macOS
|
||||||
|
if (process.platform === 'darwin') {
|
||||||
|
try {
|
||||||
|
mainWindow.on('swipe', (event, direction) => {
|
||||||
|
console.log(`Paarrot: Swipe gesture detected: ${direction}`);
|
||||||
|
if (direction === 'left' && mainWindow.webContents.canGoForward()) {
|
||||||
|
console.log('Paarrot: Navigating forward');
|
||||||
|
mainWindow.webContents.goForward();
|
||||||
|
} else if (direction === 'right' && mainWindow.webContents.canGoBack()) {
|
||||||
|
console.log('Paarrot: Navigating back');
|
||||||
|
mainWindow.webContents.goBack();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Paarrot: Failed to set up swipe gestures:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// On Linux, the OverscrollHistoryNavigation Blink feature should handle it automatically
|
||||||
|
// On Windows, mouse buttons 4 & 5 are typically used for navigation (handled by Chromium)
|
||||||
|
|
||||||
// Restore maximized state
|
// Restore maximized state
|
||||||
if (windowState.isMaximized) {
|
if (windowState.isMaximized) {
|
||||||
mainWindow.maximize();
|
mainWindow.maximize();
|
||||||
@@ -191,6 +216,75 @@ function createWindow() {
|
|||||||
}
|
}
|
||||||
}, { useSystemPicker: false });
|
}, { useSystemPicker: false });
|
||||||
|
|
||||||
|
// Disable default context menu, only show for text selection
|
||||||
|
mainWindow.webContents.on('context-menu', (event, params) => {
|
||||||
|
const { selectionText, isEditable } = params;
|
||||||
|
|
||||||
|
// Only show context menu if there's selected text or in an editable field
|
||||||
|
if (selectionText || isEditable) {
|
||||||
|
// Build a minimal menu for text operations
|
||||||
|
const template = [];
|
||||||
|
|
||||||
|
if (params.misspelledWord) {
|
||||||
|
// Add spelling suggestions
|
||||||
|
params.dictionarySuggestions.slice(0, 5).forEach(suggestion => {
|
||||||
|
template.push({
|
||||||
|
label: suggestion,
|
||||||
|
click: () => mainWindow.webContents.replaceMisspelling(suggestion)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if (template.length > 0) {
|
||||||
|
template.push({ type: 'separator' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectionText) {
|
||||||
|
template.push(
|
||||||
|
{
|
||||||
|
label: 'Copy',
|
||||||
|
role: 'copy',
|
||||||
|
accelerator: 'CmdOrCtrl+C'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEditable) {
|
||||||
|
if (selectionText) {
|
||||||
|
template.push(
|
||||||
|
{
|
||||||
|
label: 'Cut',
|
||||||
|
role: 'cut',
|
||||||
|
accelerator: 'CmdOrCtrl+X'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
template.push(
|
||||||
|
{
|
||||||
|
label: 'Paste',
|
||||||
|
role: 'paste',
|
||||||
|
accelerator: 'CmdOrCtrl+V'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (selectionText) {
|
||||||
|
template.push({ type: 'separator' });
|
||||||
|
template.push(
|
||||||
|
{
|
||||||
|
label: 'Select All',
|
||||||
|
role: 'selectAll',
|
||||||
|
accelerator: 'CmdOrCtrl+A'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (template.length > 0) {
|
||||||
|
const menu = Menu.buildFromTemplate(template);
|
||||||
|
menu.popup(mainWindow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If no text selected and not editable, suppress the context menu entirely
|
||||||
|
});
|
||||||
|
|
||||||
// Load the app
|
// Load the app
|
||||||
if (isDev) {
|
if (isDev) {
|
||||||
mainWindow.loadURL(VITE_DEV_SERVER);
|
mainWindow.loadURL(VITE_DEV_SERVER);
|
||||||
@@ -299,6 +393,12 @@ app.whenReady().then(() => {
|
|||||||
createWindow();
|
createWindow();
|
||||||
createTray();
|
createTray();
|
||||||
|
|
||||||
|
// Start API server
|
||||||
|
apiServer = new PaarrotAPIServer(mainWindow);
|
||||||
|
apiServer.start().catch(err => {
|
||||||
|
console.error('Failed to start API server:', err);
|
||||||
|
});
|
||||||
|
|
||||||
// Check for updates (not in development mode)
|
// Check for updates (not in development mode)
|
||||||
if (!isDev) {
|
if (!isDev) {
|
||||||
// Check for updates on start (after 3 seconds)
|
// Check for updates on start (after 3 seconds)
|
||||||
@@ -336,6 +436,11 @@ app.on('activate', () => {
|
|||||||
|
|
||||||
app.on('before-quit', () => {
|
app.on('before-quit', () => {
|
||||||
isQuitting = true;
|
isQuitting = true;
|
||||||
|
|
||||||
|
// Stop API server
|
||||||
|
if (apiServer) {
|
||||||
|
apiServer.stop();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ==================== IPC Handlers ====================
|
// ==================== IPC Handlers ====================
|
||||||
|
|||||||
@@ -136,6 +136,18 @@ contextBridge.exposeInMainWorld('electron', {
|
|||||||
// Desktop capturer (for screen sharing)
|
// Desktop capturer (for screen sharing)
|
||||||
desktopCapturer: {
|
desktopCapturer: {
|
||||||
getSources: (opts) => ipcRenderer.invoke('get-desktop-sources', opts)
|
getSources: (opts) => ipcRenderer.invoke('get-desktop-sources', opts)
|
||||||
|
},
|
||||||
|
|
||||||
|
// API action handler (for external API server)
|
||||||
|
api: {
|
||||||
|
onAction: (callback) => {
|
||||||
|
const handler = (_, data) => callback(data);
|
||||||
|
ipcRenderer.on('api-action', handler);
|
||||||
|
return () => ipcRenderer.removeListener('api-action', handler);
|
||||||
|
},
|
||||||
|
sendResponse: (channel, result) => {
|
||||||
|
ipcRenderer.invoke(channel, result);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
747
package-lock.json
generated
747
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -22,8 +22,11 @@
|
|||||||
},
|
},
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"body-parser": "2.2.2",
|
||||||
|
"cors": "2.8.6",
|
||||||
"electron-store": "^8.2.0",
|
"electron-store": "^8.2.0",
|
||||||
"electron-updater": "^6.3.9",
|
"electron-updater": "^6.3.9",
|
||||||
|
"express": "5.2.1",
|
||||||
"open": "11.0.0"
|
"open": "11.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
91
test-api.js
Normal file
91
test-api.js
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Paarrot API Test Script
|
||||||
|
*
|
||||||
|
* Simple test script to verify the API server is working.
|
||||||
|
* Run this while Paarrot is running to test the API endpoints.
|
||||||
|
*
|
||||||
|
* Usage: node test-api.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
const API_BASE = 'http://127.0.0.1:33384';
|
||||||
|
|
||||||
|
async function testEndpoint(name, method, path, body = null) {
|
||||||
|
console.log(`\n🧪 Testing: ${name}`);
|
||||||
|
console.log(` ${method} ${API_BASE}${path}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const options = {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (body) {
|
||||||
|
options.body = JSON.stringify(body);
|
||||||
|
console.log(` Body:`, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`${API_BASE}${path}`, options);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
console.log(` ✅ Success (${response.status})`);
|
||||||
|
console.log(` Response:`, JSON.stringify(data, null, 2));
|
||||||
|
} else {
|
||||||
|
console.log(` ❌ Failed (${response.status})`);
|
||||||
|
console.log(` Error:`, data.error || data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.log(` ❌ Error:`, error.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runTests() {
|
||||||
|
console.log('🚀 Paarrot API Test Suite\n');
|
||||||
|
console.log('Make sure Paarrot is running before running these tests!\n');
|
||||||
|
|
||||||
|
// Health check
|
||||||
|
await testEndpoint('Health Check', 'GET', '/health');
|
||||||
|
|
||||||
|
// Get status
|
||||||
|
await testEndpoint('Get Status', 'GET', '/status');
|
||||||
|
|
||||||
|
// Get channels
|
||||||
|
await testEndpoint('Get Channels', 'GET', '/channels');
|
||||||
|
|
||||||
|
// Get current room
|
||||||
|
await testEndpoint('Get Current Room', 'GET', '/room/current');
|
||||||
|
|
||||||
|
// Toggle mute
|
||||||
|
await testEndpoint('Toggle Mute', 'POST', '/mute/toggle');
|
||||||
|
|
||||||
|
// Set mute
|
||||||
|
await testEndpoint('Set Mute (true)', 'POST', '/mute', { muted: true });
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
|
await testEndpoint('Set Mute (false)', 'POST', '/mute', { muted: false });
|
||||||
|
|
||||||
|
// Toggle deafen
|
||||||
|
await testEndpoint('Toggle Deafen', 'POST', '/deafen/toggle');
|
||||||
|
|
||||||
|
// Send message to current room (commented out to avoid spam)
|
||||||
|
// await testEndpoint('Send Message to Current', 'POST', '/message/current', {
|
||||||
|
// message: 'Test message from API'
|
||||||
|
// });
|
||||||
|
|
||||||
|
// Test invalid endpoint
|
||||||
|
await testEndpoint('Invalid Endpoint (should 404)', 'GET', '/invalid');
|
||||||
|
|
||||||
|
console.log('\n✨ Tests complete!\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run tests if this script is executed directly
|
||||||
|
if (require.main === module) {
|
||||||
|
runTests().catch(console.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { testEndpoint, runTests };
|
||||||
53
test-api.sh
Executable file
53
test-api.sh
Executable file
@@ -0,0 +1,53 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Paarrot API Quick Test Script
|
||||||
|
#
|
||||||
|
# Simple curl commands to test the API endpoints
|
||||||
|
# Make sure Paarrot is running before using these commands
|
||||||
|
|
||||||
|
API_BASE="http://127.0.0.1:33384"
|
||||||
|
|
||||||
|
echo "🚀 Paarrot API Quick Test"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
echo "📡 Health Check:"
|
||||||
|
curl -s "$API_BASE/health" | jq '.' || curl -s "$API_BASE/health"
|
||||||
|
echo ""
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Get status
|
||||||
|
echo "📊 Get Status:"
|
||||||
|
curl -s "$API_BASE/status" | jq '.' || curl -s "$API_BASE/status"
|
||||||
|
echo ""
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Toggle mute
|
||||||
|
echo "🎤 Toggle Mute:"
|
||||||
|
curl -s -X POST "$API_BASE/mute/toggle" | jq '.' || curl -s -X POST "$API_BASE/mute/toggle"
|
||||||
|
echo ""
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Toggle deafen
|
||||||
|
echo "🔇 Toggle Deafen:"
|
||||||
|
curl -s -X POST "$API_BASE/deafen/toggle" | jq '.' || curl -s -X POST "$API_BASE/deafen/toggle"
|
||||||
|
echo ""
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Get channels
|
||||||
|
echo "📋 Get Channels:"
|
||||||
|
curl -s "$API_BASE/channels" | jq '.' || curl -s "$API_BASE/channels"
|
||||||
|
echo ""
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Get current room
|
||||||
|
echo "🏠 Get Current Room:"
|
||||||
|
curl -s "$API_BASE/room/current" | jq '.' || curl -s "$API_BASE/room/current"
|
||||||
|
echo ""
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "✨ Done!"
|
||||||
|
echo ""
|
||||||
|
echo "💡 Tips:"
|
||||||
|
echo " - Use 'jq' for pretty JSON output (install with: sudo apt install jq)"
|
||||||
|
echo " - Check API.md for full documentation"
|
||||||
|
echo " - Test message sending: curl -X POST $API_BASE/message/current -H 'Content-Type: application/json' -d '{\"message\": \"Hello!\"}'"
|
||||||
Reference in New Issue
Block a user