Reorganize project: move docs to docs/ and test scripts to tests/

Moved 9 documentation .md files from root into docs/.
Moved 4 test scripts from root into tests/.
Updated cross-references in README.md and docs to reflect new paths.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
cottongin
2026-02-07 14:07:39 -05:00
parent 974f0e4a67
commit 84398ebdd0
14 changed files with 95 additions and 16 deletions

474
docs/API_QUICK_REFERENCE.md Normal file
View File

@@ -0,0 +1,474 @@
# API Quick Reference
Quick reference for Live Voting, WebSocket, and Webhook endpoints.
## Base URL
```
http://localhost:5000/api
```
## Authentication
All endpoints require JWT authentication:
```
Authorization: Bearer YOUR_JWT_TOKEN
```
Get token via:
```bash
POST /api/auth/login
Body: { "key": "YOUR_ADMIN_KEY" }
```
---
## WebSocket Events
### Connect to WebSocket
```
ws://localhost:5000/api/sessions/live
```
**Message Protocol**:
```json
// Authenticate
{ "type": "auth", "token": "YOUR_JWT_TOKEN" }
// Subscribe to session
{ "type": "subscribe", "sessionId": 123 }
// Unsubscribe
{ "type": "unsubscribe", "sessionId": 123 }
// Heartbeat
{ "type": "ping" }
```
**Server Events**:
```json
// Auth success
{ "type": "auth_success", "message": "..." }
// Subscribed
{ "type": "subscribed", "sessionId": 123 }
// Session started
{
"type": "session.started",
"timestamp": "2025-11-01T...",
"data": {
"session": { "id": 123, "is_active": 1, "created_at": "...", "notes": "..." }
}
}
// Game added
{
"type": "game.added",
"timestamp": "2025-11-01T...",
"data": {
"session": { "id": 123, "is_active": true, "games_played": 5 },
"game": { "id": 45, "title": "Fibbage 4", ... }
}
}
// Session ended
{
"type": "session.ended",
"timestamp": "2025-11-01T...",
"data": {
"session": { "id": 123, "is_active": 0, "games_played": 5 }
}
}
// Pong
{ "type": "pong" }
```
---
## Live Voting
### Submit Live Vote
```http
POST /api/votes/live
```
**Request Body**:
```json
{
"username": "string",
"vote": "up" | "down",
"timestamp": "2025-11-01T20:30:00Z"
}
```
**Success Response (200)**:
```json
{
"success": true,
"message": "Vote recorded successfully",
"session": { "id": 123, "games_played": 5 },
"game": {
"id": 45,
"title": "Fibbage 4",
"upvotes": 46,
"downvotes": 3,
"popularity_score": 43
},
"vote": {
"username": "TestUser",
"type": "up",
"timestamp": "2025-11-01T20:30:00Z"
}
}
```
**Error Responses**:
- `400` - Invalid payload
- `404` - No active session or timestamp doesn't match any game
- `409` - Duplicate vote (within 1 second)
---
## Webhooks
### List Webhooks
```http
GET /api/webhooks
```
**Response**:
```json
[
{
"id": 1,
"name": "Kosmi Bot",
"url": "http://bot-url/webhook",
"events": ["game.added"],
"enabled": true,
"created_at": "2025-11-01T20:00:00Z"
}
]
```
### Get Single Webhook
```http
GET /api/webhooks/:id
```
### Create Webhook
```http
POST /api/webhooks
```
**Request Body**:
```json
{
"name": "Kosmi Bot",
"url": "http://bot-url/webhook",
"secret": "your_shared_secret",
"events": ["game.added"]
}
```
**Response (201)**:
```json
{
"id": 1,
"name": "Kosmi Bot",
"url": "http://bot-url/webhook",
"events": ["game.added"],
"enabled": true,
"created_at": "2025-11-01T20:00:00Z",
"message": "Webhook created successfully"
}
```
### Update Webhook
```http
PATCH /api/webhooks/:id
```
**Request Body** (all fields optional):
```json
{
"name": "Updated Name",
"url": "http://new-url/webhook",
"secret": "new_secret",
"events": ["game.added"],
"enabled": false
}
```
### Delete Webhook
```http
DELETE /api/webhooks/:id
```
**Response (200)**:
```json
{
"message": "Webhook deleted successfully",
"webhookId": 1
}
```
### Test Webhook
```http
POST /api/webhooks/test/:id
```
Sends a test `game.added` event to verify webhook is working.
**Response (200)**:
```json
{
"message": "Test webhook sent",
"note": "Check webhook_logs table for delivery status"
}
```
### Get Webhook Logs
```http
GET /api/webhooks/:id/logs?limit=50
```
**Response**:
```json
[
{
"id": 1,
"webhook_id": 1,
"event_type": "game.added",
"payload": { /* full payload */ },
"response_status": 200,
"error_message": null,
"created_at": "2025-11-01T20:30:00Z"
}
]
```
---
## Webhook Payloads
### Event: `session.started`
Sent when a new session is created.
**Headers**:
- `Content-Type: application/json`
- `X-Webhook-Signature: sha256=<hmac_signature>`
- `X-Webhook-Event: session.started`
- `User-Agent: Jackbox-Game-Picker-Webhook/1.0`
**Payload**:
```json
{
"event": "session.started",
"timestamp": "2025-11-01T20:00:00Z",
"data": {
"session": {
"id": 123,
"is_active": 1,
"created_at": "2025-11-01T20:00:00Z",
"notes": "Friday Game Night"
}
}
}
```
### Event: `game.added`
Sent when a game is added to an active session.
**Headers**:
- `Content-Type: application/json`
- `X-Webhook-Signature: sha256=<hmac_signature>`
- `X-Webhook-Event: game.added`
- `User-Agent: Jackbox-Game-Picker-Webhook/1.0`
**Payload**:
```json
{
"event": "game.added",
"timestamp": "2025-11-01T20:30:00Z",
"data": {
"session": {
"id": 123,
"is_active": true,
"games_played": 5
},
"game": {
"id": 45,
"title": "Fibbage 4",
"pack_name": "The Jackbox Party Pack 9",
"min_players": 2,
"max_players": 8,
"manually_added": false
}
}
}
```
### Event: `session.ended`
Sent when a session is closed/ended.
**Headers**:
- `Content-Type: application/json`
- `X-Webhook-Signature: sha256=<hmac_signature>`
- `X-Webhook-Event: session.ended`
- `User-Agent: Jackbox-Game-Picker-Webhook/1.0`
**Payload**:
```json
{
"event": "session.ended",
"timestamp": "2025-11-01T20:30:00Z",
"data": {
"session": {
"id": 123,
"is_active": 0,
"games_played": 5
}
}
}
```
---
## cURL Examples
### Submit Vote
```bash
curl -X POST "http://localhost:5000/api/votes/live" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"username": "TestUser",
"vote": "up",
"timestamp": "2025-11-01T20:30:00Z"
}'
```
### Create Webhook
```bash
curl -X POST "http://localhost:5000/api/webhooks" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Kosmi Bot",
"url": "http://localhost:3001/webhook/jackbox",
"secret": "test_secret_123",
"events": ["game.added"]
}'
```
### Test Webhook
```bash
curl -X POST "http://localhost:5000/api/webhooks/test/1" \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
```
### View Webhook Logs
```bash
curl -X GET "http://localhost:5000/api/webhooks/1/logs?limit=10" \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
```
---
## Webhook Signature Verification
**Node.js Example**:
```javascript
const crypto = require('crypto');
function verifyWebhookSignature(signature, payload, secret) {
if (!signature || !signature.startsWith('sha256=')) {
return false;
}
const expectedSignature = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(JSON.stringify(payload))
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
// In your webhook endpoint:
app.post('/webhook/jackbox', (req, res) => {
const signature = req.headers['x-webhook-signature'];
const secret = process.env.WEBHOOK_SECRET;
if (!verifyWebhookSignature(signature, req.body, secret)) {
return res.status(401).send('Invalid signature');
}
// Process webhook...
res.status(200).send('OK');
});
```
---
## Error Codes
| Code | Meaning |
|------|---------|
| 200 | Success |
| 201 | Created |
| 400 | Bad Request - Invalid payload |
| 401 | Unauthorized - Invalid JWT or signature |
| 404 | Not Found - Resource doesn't exist |
| 409 | Conflict - Duplicate vote |
| 500 | Internal Server Error |
---
## Rate Limiting
Currently no rate limiting is implemented. Consider implementing rate limiting in production:
- Per IP address
- Per JWT token
- Per webhook endpoint
---
## Best Practices
1. **Always verify webhook signatures** before processing
2. **Use HTTPS** for webhook URLs in production
3. **Store secrets securely** in environment variables
4. **Respond quickly** to webhooks (< 5 seconds)
5. **Log webhook activity** for debugging
6. **Handle retries gracefully** if implementing retry logic
7. **Validate timestamps** to prevent replay attacks
---
For detailed documentation, see [BOT_INTEGRATION.md](BOT_INTEGRATION.md)

746
docs/BOT_INTEGRATION.md Normal file
View File

@@ -0,0 +1,746 @@
# Bot Integration Guide
This guide explains how to integrate your bot with the Jackbox Game Picker API for live voting and game notifications.
## Table of Contents
1. [Live Voting (Bot → API)](#live-voting-bot--api)
2. [Game Notifications (API → Bot)](#game-notifications-api--bot)
- [WebSocket Integration (Recommended)](#websocket-integration-recommended)
- [Webhook Integration](#webhook-integration)
3. [Webhook Management](#webhook-management)
4. [Testing](#testing)
5. [Available Events](#available-events)
---
## Live Voting (Bot → API)
Your bot can send real-time votes to the API when it detects "thisgame++" or "thisgame--" in Kosmi chat.
### Endpoint
```
POST /api/votes/live
```
### Authentication
Requires JWT token in Authorization header:
```
Authorization: Bearer YOUR_JWT_TOKEN
```
### Request Body
```json
{
"username": "string", // Username of the voter
"vote": "up" | "down", // "up" for thisgame++, "down" for thisgame--
"timestamp": "string" // ISO 8601 timestamp (e.g., "2025-11-01T20:30:00Z")
}
```
### Response (Success)
```json
{
"success": true,
"message": "Vote recorded successfully",
"session": {
"id": 123,
"games_played": 5
},
"game": {
"id": 45,
"title": "Fibbage 4",
"upvotes": 46,
"downvotes": 3,
"popularity_score": 43
},
"vote": {
"username": "TestUser",
"type": "up",
"timestamp": "2025-11-01T20:30:00Z"
}
}
```
### Error Responses
- **400 Bad Request**: Invalid payload or timestamp format
- **404 Not Found**: No active session or timestamp doesn't match any game
- **409 Conflict**: Duplicate vote (within 1 second of previous vote from same user)
- **500 Internal Server Error**: Server error
### Example Implementation (Node.js)
```javascript
// When bot detects "thisgame++" or "thisgame--" in Kosmi chat
async function handleVote(username, message) {
const isUpvote = message.includes('thisgame++');
const isDownvote = message.includes('thisgame--');
if (!isUpvote && !isDownvote) return;
try {
const response = await fetch('http://your-api-url/api/votes/live', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.JWT_TOKEN}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: username,
vote: isUpvote ? 'up' : 'down',
timestamp: new Date().toISOString()
})
});
const data = await response.json();
if (response.ok) {
console.log(`Vote recorded for ${data.game.title}: ${data.game.upvotes}👍 ${data.game.downvotes}👎`);
} else {
console.error('Vote failed:', data.error);
}
} catch (error) {
console.error('Error sending vote:', error);
}
}
```
### Important Notes
- **Deduplication**: Votes from the same user within 1 second are automatically rejected to prevent spam
- **Timestamp Matching**: The API matches the vote timestamp to the correct game based on when games were played
- **Active Session Required**: Votes can only be recorded when there's an active session with games played
---
## Game Notifications (API → Bot)
The API can notify your bot when games are added to a session, allowing you to announce "Coming up next: Game Title!" in Kosmi chat.
There are two integration methods available:
1. **WebSocket (Recommended)**: Real-time bidirectional communication, simpler setup, works through firewalls
2. **Webhooks**: Traditional HTTP callbacks, good for serverless/stateless integrations
### WebSocket Integration (Recommended)
WebSocket provides real-time event streaming from the API to your bot. This is the recommended approach as it:
- Works through firewalls and NAT (bot initiates connection)
- No need to expose inbound ports
- Automatic reconnection on disconnect
- Lower latency than webhooks
- Bidirectional communication
#### Connection Flow
1. Bot connects to WebSocket endpoint
2. Bot authenticates with JWT token
3. Bot subscribes to active session
4. Bot receives `game.added` events in real-time
#### WebSocket Endpoint
```
wss://your-api-url/api/sessions/live
```
#### Message Protocol
All messages are JSON-formatted.
**Client → Server Messages:**
```json
// 1. Authenticate (first message after connecting)
{
"type": "auth",
"token": "YOUR_JWT_TOKEN"
}
// 2. Subscribe to a session
{
"type": "subscribe",
"sessionId": 123
}
// 3. Unsubscribe from a session
{
"type": "unsubscribe",
"sessionId": 123
}
// 4. Heartbeat (keep connection alive)
{
"type": "ping"
}
```
**Server → Client Messages:**
```json
// Authentication success
{
"type": "auth_success",
"message": "Authenticated successfully"
}
// Authentication failure
{
"type": "auth_error",
"message": "Invalid or expired token"
}
// Subscription confirmed
{
"type": "subscribed",
"sessionId": 123,
"message": "Subscribed to session 123"
}
// Game added event
{
"type": "game.added",
"timestamp": "2025-11-01T20:30:00Z",
"data": {
"session": {
"id": 123,
"is_active": true,
"games_played": 5
},
"game": {
"id": 45,
"title": "Fibbage 4",
"pack_name": "The Jackbox Party Pack 9",
"min_players": 2,
"max_players": 8,
"manually_added": false,
"room_code": "JYET"
}
}
}
// Session started event (broadcast to all authenticated clients)
{
"type": "session.started",
"timestamp": "2025-11-01T20:00:00Z",
"data": {
"session": {
"id": 123,
"is_active": true,
"created_at": "2025-11-01T20:00:00Z",
"notes": "Friday game night"
}
}
}
// Session ended event (broadcast to session subscribers)
{
"type": "session.ended",
"timestamp": "2025-11-01T23:00:00Z",
"data": {
"session": {
"id": 123,
"is_active": false,
"games_played": 8
}
}
}
// Game started event (broadcast to session subscribers)
// Fired when the Jackbox room becomes locked, meaning gameplay has begun
{
"type": "game.started",
"timestamp": "2025-11-01T20:31:00Z",
"data": {
"sessionId": 123,
"gameId": 456,
"roomCode": "JYET",
"maxPlayers": 8
}
}
// Audience joined event (broadcast to session subscribers)
// Confirms the app successfully joined a Jackbox room as an audience member
{
"type": "audience.joined",
"timestamp": "2025-11-01T20:31:05Z",
"data": {
"sessionId": 123,
"gameId": 456,
"roomCode": "JYET"
}
}
// Heartbeat response
{
"type": "pong"
}
// Error
{
"type": "error",
"message": "Error description"
}
```
#### Example Implementation (Node.js)
```javascript
const WebSocket = require('ws');
class JackboxWebSocketClient {
constructor(apiURL, jwtToken) {
this.apiURL = apiURL.replace(/^http/, 'ws') + '/api/sessions/live';
this.jwtToken = jwtToken;
this.ws = null;
this.reconnectDelay = 1000;
this.maxReconnectDelay = 30000;
}
connect() {
this.ws = new WebSocket(this.apiURL);
this.ws.on('open', () => {
console.log('WebSocket connected');
this.authenticate();
this.startHeartbeat();
});
this.ws.on('message', (data) => {
this.handleMessage(JSON.parse(data));
});
this.ws.on('close', () => {
console.log('WebSocket disconnected, reconnecting...');
this.reconnect();
});
this.ws.on('error', (err) => {
console.error('WebSocket error:', err);
});
}
authenticate() {
this.send({ type: 'auth', token: this.jwtToken });
}
subscribe(sessionId) {
this.send({ type: 'subscribe', sessionId });
}
send(message) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(message));
}
}
handleMessage(message) {
switch (message.type) {
case 'auth_success':
console.log('Authenticated successfully');
// Get active session and subscribe
this.getActiveSessionAndSubscribe();
break;
case 'auth_error':
console.error('Authentication failed:', message.message);
break;
case 'subscribed':
console.log('Subscribed to session:', message.sessionId);
break;
case 'game.added':
this.handleGameAdded(message.data);
break;
case 'pong':
// Heartbeat response
break;
case 'error':
console.error('Server error:', message.message);
break;
}
}
handleGameAdded(data) {
const { game } = data;
// Build announcement with room code if available
let announcement = `🎮 Coming up next: ${game.title}!`;
if (game.room_code) {
announcement += ` Join at jackbox.tv with code: ${game.room_code}`;
}
// Send to your chat platform (e.g., Kosmi chat)
this.broadcastToChat(announcement);
}
startHeartbeat() {
setInterval(() => {
this.send({ type: 'ping' });
}, 30000); // Every 30 seconds
}
reconnect() {
setTimeout(() => {
this.connect();
this.reconnectDelay = Math.min(this.reconnectDelay * 2, this.maxReconnectDelay);
}, this.reconnectDelay);
}
async getActiveSessionAndSubscribe() {
// Fetch active session from REST API
const response = await fetch(`${this.apiURL.replace('/api/sessions/live', '')}/api/sessions/active`, {
headers: { 'Authorization': `Bearer ${this.jwtToken}` }
});
if (response.ok) {
const session = await response.json();
if (session && session.id) {
this.subscribe(session.id);
}
}
}
broadcastToChat(message) {
// Implement your chat platform integration here
console.log('Broadcasting:', message);
}
}
// Usage
const client = new JackboxWebSocketClient('https://your-api-url', 'YOUR_JWT_TOKEN');
client.connect();
```
#### Example Implementation (Go)
See the reference implementation in `irc-kosmi-relay/bridge/jackbox/websocket_client.go`.
---
### Webhook Integration
Webhooks are HTTP callbacks sent from the API to your bot when events occur. This is an alternative to WebSocket for bots that prefer stateless integrations.
#### Webhook Event: `game.added`
Triggered whenever a game is added to an active session (either via picker or manual selection).
### Webhook Payload
```json
{
"event": "game.added",
"timestamp": "2025-11-01T20:30:00Z",
"data": {
"session": {
"id": 123,
"is_active": true,
"games_played": 5
},
"game": {
"id": 45,
"title": "Fibbage 4",
"pack_name": "The Jackbox Party Pack 9",
"min_players": 2,
"max_players": 8,
"manually_added": false,
"room_code": "JYET"
}
}
}
```
> **Note:** `room_code` is the 4-character Jackbox room code (e.g. `"JYET"`). It will be `null` if no room code was provided when the game was added.
### Webhook Headers
The API sends the following headers with each webhook:
- `Content-Type: application/json`
- `X-Webhook-Signature: sha256=<hmac_signature>` - HMAC-SHA256 signature for verification
- `X-Webhook-Event: game.added` - Event type
- `User-Agent: Jackbox-Game-Picker-Webhook/1.0`
### Signature Verification
**IMPORTANT**: Always verify the webhook signature to ensure the request is authentic.
```javascript
const crypto = require('crypto');
function verifyWebhookSignature(signature, payload, secret) {
if (!signature || !signature.startsWith('sha256=')) {
return false;
}
const expectedSignature = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(JSON.stringify(payload))
.digest('hex');
// Use timing-safe comparison
try {
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
} catch (err) {
return false;
}
}
```
### Example Implementation (Express.js)
```javascript
const express = require('express');
const crypto = require('crypto');
const app = express();
// IMPORTANT: Use express.json() with verify option to get raw body
app.use(express.json({
verify: (req, res, buf) => {
req.rawBody = buf.toString('utf8');
}
}));
app.post('/webhook/jackbox', (req, res) => {
const signature = req.headers['x-webhook-signature'];
const secret = process.env.WEBHOOK_SECRET; // Your webhook secret
// Verify signature
if (!signature || !signature.startsWith('sha256=')) {
return res.status(401).send('Missing or invalid signature');
}
const expectedSignature = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(req.rawBody)
.digest('hex');
// Timing-safe comparison
try {
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature))) {
return res.status(401).send('Invalid signature');
}
} catch (err) {
return res.status(401).send('Invalid signature');
}
// Handle the event
if (req.body.event === 'game.added') {
const game = req.body.data.game;
// Build announcement with room code if available
let message = `🎮 Coming up next: ${game.title}!`;
if (game.room_code) {
message += ` Join at jackbox.tv with code: ${game.room_code}`;
}
// Send message to Kosmi chat
sendKosmiMessage(message);
console.log(`Announced game: ${game.title} from ${game.pack_name} (code: ${game.room_code || 'N/A'})`);
}
// Always respond with 200 OK
res.status(200).send('OK');
});
function sendKosmiMessage(message) {
// Your Kosmi chat integration here
console.log('Sending to Kosmi:', message);
}
app.listen(3001, () => {
console.log('Webhook receiver listening on port 3001');
});
```
---
## Webhook Management
You can manage webhooks through the API using the following endpoints (all require JWT authentication).
### List All Webhooks
```bash
GET /api/webhooks
Authorization: Bearer YOUR_JWT_TOKEN
```
### Create Webhook
```bash
POST /api/webhooks
Authorization: Bearer YOUR_JWT_TOKEN
Content-Type: application/json
{
"name": "Kosmi Bot",
"url": "http://your-bot-url/webhook/jackbox",
"secret": "your_shared_secret_key",
"events": ["game.added"]
}
```
### Update Webhook
```bash
PATCH /api/webhooks/:id
Authorization: Bearer YOUR_JWT_TOKEN
Content-Type: application/json
{
"enabled": false // Disable webhook
}
```
### Delete Webhook
```bash
DELETE /api/webhooks/:id
Authorization: Bearer YOUR_JWT_TOKEN
```
### Test Webhook
```bash
POST /api/webhooks/test/:id
Authorization: Bearer YOUR_JWT_TOKEN
```
Sends a test `game.added` event to verify your webhook is working.
### View Webhook Logs
```bash
GET /api/webhooks/:id/logs?limit=50
Authorization: Bearer YOUR_JWT_TOKEN
```
Returns recent webhook delivery attempts with status codes and errors.
---
## Testing
### Test Live Voting
```bash
# Get your JWT token first
curl -X POST "http://localhost:5000/api/auth/login" \
-H "Content-Type: application/json" \
-d '{"apiKey": "YOUR_API_KEY"}'
# Send a test vote
curl -X POST "http://localhost:5000/api/votes/live" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"username": "TestUser",
"vote": "up",
"timestamp": "2025-11-01T20:30:00Z"
}'
```
### Test Webhooks
```bash
# Create a webhook
curl -X POST "http://localhost:5000/api/webhooks" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Test Webhook",
"url": "http://localhost:3001/webhook/jackbox",
"secret": "test_secret_123",
"events": ["game.added"]
}'
# Test the webhook
curl -X POST "http://localhost:5000/api/webhooks/test/1" \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
# Check webhook logs
curl -X GET "http://localhost:5000/api/webhooks/1/logs" \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
```
---
## Available Events
### Webhook Events
- `game.added` - Triggered when a game is added to an active session. Includes `room_code` (the 4-character Jackbox join code) if one was provided.
### WebSocket Events
- `game.added` - Triggered when a game is added to an active session. Sent to clients subscribed to that session. Includes `room_code`.
- `session.started` - Triggered when a new session is created. Broadcast to **all** authenticated clients (no subscription required).
- `session.ended` - Triggered when a session is closed. Sent to clients subscribed to that session.
- `game.started` - Triggered when the Jackbox room becomes locked (gameplay has begun). Detected by polling the Jackbox REST API every 10 seconds. Sent to clients subscribed to that session. Includes `roomCode` and `maxPlayers`.
- `audience.joined` - Triggered when the app successfully joins a Jackbox room as an audience member. Sent to clients subscribed to that session. This confirms the room code is valid and the game is being monitored.
- `player-count.updated` - Triggered when the player count for a game is updated. Sent to clients subscribed to that session.
> **Tip:** To receive `session.started` events, your bot only needs to authenticate — no subscription is needed. Once you receive a `session.started` event, subscribe to the new session ID to receive `game.added` and `session.ended` events for it.
More events may be added in the future (e.g., `vote.recorded`).
---
## Security Best Practices
1. **Always verify webhook signatures** - Never trust webhook payloads without verification
2. **Use HTTPS in production** - Webhook URLs should use HTTPS to prevent man-in-the-middle attacks
3. **Keep secrets secure** - Store webhook secrets in environment variables, never in code
4. **Implement rate limiting** - Protect your webhook endpoints from abuse
5. **Log webhook activity** - Keep logs of webhook deliveries for debugging
6. **Use strong secrets** - Generate cryptographically secure random strings for webhook secrets
---
## Troubleshooting
### Votes Not Being Recorded
- Check that there's an active session with games played
- Verify the timestamp is within the timeframe of a played game
- Ensure you're not sending duplicate votes within 1 second
- Check API logs for error messages
### Webhooks Not Being Received
- Verify your webhook URL is publicly accessible
- Check webhook logs via `/api/webhooks/:id/logs`
- Test with `ngrok` or similar tool if developing locally
- Ensure your webhook endpoint responds with 200 OK
- Check that webhook is enabled in the database
### Signature Verification Failing
- Ensure you're using the raw request body for signature verification
- Check that the secret matches what's stored in the database
- Verify you're using HMAC-SHA256 algorithm
- Make sure to prefix with "sha256=" when comparing
---
## Support
For issues or questions, contact: cottongin@cottongin.xyz

View File

@@ -0,0 +1,105 @@
# Session End Event - Quick Start Guide
## 🚀 Quick Start
### Listen for Session End Events
```javascript
const WebSocket = require('ws');
const ws = new WebSocket('ws://localhost:5000/api/sessions/live');
ws.on('open', () => {
// 1. Authenticate
ws.send(JSON.stringify({
type: 'auth',
token: 'YOUR_JWT_TOKEN'
}));
});
ws.on('message', (data) => {
const msg = JSON.parse(data.toString());
if (msg.type === 'auth_success') {
// 2. Subscribe to session
ws.send(JSON.stringify({
type: 'subscribe',
sessionId: 17
}));
}
if (msg.type === 'session.ended') {
// 3. Handle session end
console.log('Session ended!');
console.log(`Games played: ${msg.data.session.games_played}`);
// Announce to your users here
}
});
```
## 📦 Event Format
```json
{
"type": "session.ended",
"timestamp": "2025-11-01T02:30:45.123Z",
"data": {
"session": {
"id": 17,
"is_active": 0,
"games_played": 5
}
}
}
```
## 🧪 Test It
```bash
# Get your JWT token first
curl -X POST http://localhost:5000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"key":"YOUR_ADMIN_KEY"}'
# Run the test script
node ../tests/test-session-end-websocket.js 17 YOUR_JWT_TOKEN
# In another terminal, close the session
curl -X POST http://localhost:5000/api/sessions/17/close \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
```
## 🤖 Bot Integration
When your bot receives `session.ended`:
```javascript
if (msg.type === 'session.ended') {
const { id, games_played } = msg.data.session;
// Announce to IRC/Discord/etc
bot.announce(`🌙 Game Night has ended! We played ${games_played} games.`);
bot.announce('Thanks for playing!');
}
```
## 📚 Full Documentation
See [SESSION_END_WEBSOCKET.md](SESSION_END_WEBSOCKET.md) for complete documentation.
## ⚡ Key Points
-**Instant** - No polling needed
-**Reliable** - Broadcast to all subscribers
-**Simple** - Same format as `game.added`
-**Tested** - Test script included
## 🔗 Related Events
| Event | When |
|-------|------|
| `session.started` | Session created |
| `game.added` | Game starts |
| `session.ended` | Session closes |
| `vote.received` | Vote cast |

View File

@@ -0,0 +1,306 @@
# Session End WebSocket Event
This document describes the `session.ended` WebSocket event that is broadcast when a game session is closed.
## 📋 Event Overview
When a session is closed (either manually or through timeout), the backend broadcasts a `session.ended` event to all subscribed WebSocket clients. This allows bots and other integrations to react immediately to session closures.
## 🔌 WebSocket Connection
**Endpoint:** `ws://localhost:5000/api/sessions/live`
**Authentication:** Required (JWT token)
## 📨 Event Format
### Event Type
```
session.ended
```
### Full Message Structure
```json
{
"type": "session.ended",
"timestamp": "2025-11-01T02:30:45.123Z",
"data": {
"session": {
"id": 17,
"is_active": 0,
"games_played": 5
}
}
}
```
### Field Descriptions
| Field | Type | Description |
|-------|------|-------------|
| `type` | string | Always `"session.ended"` |
| `timestamp` | string | ISO 8601 timestamp when the event was generated |
| `data.session.id` | number | The ID of the session that ended |
| `data.session.is_active` | number | Always `0` (inactive) for ended sessions |
| `data.session.games_played` | number | Total number of games played in the session |
## 🚀 Implementation
### Backend Implementation
The `session.ended` event is automatically broadcast when:
1. **Manual Session Close**: Admin closes a session via `POST /api/sessions/:id/close`
2. **Session Timeout**: (If implemented) When a session times out
**Code Location:** `backend/routes/sessions.js` - `POST /:id/close` endpoint
```javascript
// Broadcast session.ended event via WebSocket
const wsManager = getWebSocketManager();
if (wsManager) {
const eventData = {
session: {
id: closedSession.id,
is_active: 0,
games_played: closedSession.games_played
}
};
wsManager.broadcastEvent('session.ended', eventData, parseInt(req.params.id));
}
```
### Client Implementation Example
#### Node.js with `ws` library
```javascript
const WebSocket = require('ws');
const ws = new WebSocket('ws://localhost:5000/api/sessions/live');
ws.on('open', () => {
// Authenticate
ws.send(JSON.stringify({
type: 'auth',
token: 'your-jwt-token'
}));
});
ws.on('message', (data) => {
const message = JSON.parse(data.toString());
switch (message.type) {
case 'auth_success':
// Subscribe to session
ws.send(JSON.stringify({
type: 'subscribe',
sessionId: 17
}));
break;
case 'session.ended':
console.log('Session ended!');
console.log(`Session ID: ${message.data.session.id}`);
console.log(`Games played: ${message.data.session.games_played}`);
// Handle session end (e.g., announce in IRC, Discord, etc.)
break;
}
});
```
#### Python with `websockets` library
```python
import asyncio
import json
import websockets
async def listen_for_session_end():
uri = "ws://localhost:5000/api/sessions/live"
async with websockets.connect(uri) as websocket:
# Authenticate
await websocket.send(json.dumps({
"type": "auth",
"token": "your-jwt-token"
}))
async for message in websocket:
data = json.loads(message)
if data["type"] == "auth_success":
# Subscribe to session
await websocket.send(json.dumps({
"type": "subscribe",
"sessionId": 17
}))
elif data["type"] == "session.ended":
session = data["data"]["session"]
print(f"Session {session['id']} ended!")
print(f"Games played: {session['games_played']}")
# Handle session end
asyncio.run(listen_for_session_end())
```
## 🧪 Testing
### Using the Test Script
A test script is provided to verify the `session.ended` event:
```bash
node ../tests/test-session-end-websocket.js <session_id> <jwt_token>
```
**Example:**
```bash
node ../tests/test-session-end-websocket.js 17 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```
### Manual Testing Steps
1. **Start the backend server:**
```bash
cd backend
npm start
```
2. **Run the test script in another terminal:**
```bash
node ../tests/test-session-end-websocket.js 17 <your-jwt-token>
```
3. **Close the session in the Picker UI** or via API:
```bash
curl -X POST http://localhost:5000/api/sessions/17/close \
-H "Authorization: Bearer <your-jwt-token>" \
-H "Content-Type: application/json"
```
4. **Verify the event is received** in the test script output
### Expected Output
```
🚀 Testing session.ended WebSocket event
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📡 Connecting to: ws://localhost:5000/api/sessions/live
🎮 Session ID: 17
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✅ Connected to WebSocket server
🔐 Authenticating...
✅ Authentication successful
📻 Subscribing to session 17...
✅ Subscribed to session 17
👂 Listening for session.ended events...
(Close the session in the Picker to trigger the event)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🎉 SESSION.ENDED EVENT RECEIVED!
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📦 Event Data:
{
"type": "session.ended",
"timestamp": "2025-11-01T02:30:45.123Z",
"data": {
"session": {
"id": 17,
"is_active": 0,
"games_played": 5
}
}
}
✨ Event Details:
Session ID: 17
Active: No
Games Played: 5
Timestamp: 2025-11-01T02:30:45.123Z
✅ Test successful! The bot should now announce the session end.
```
## 🤖 Bot Integration
### IRC/Kosmi Bot Example
When the bot receives a `session.ended` event, it should:
1. **Announce the final vote counts** for the last game played
2. **Announce that the game night has ended**
3. **Optionally display session statistics**
Example bot response:
```
🗳️ Final votes for Quiplash 3: 5👍 1👎 (Score: +4)
🌙 Game Night has ended! Thanks for playing!
📊 Session Stats: 5 games played
```
### Fallback Behavior
The bot should also implement **polling detection** as a fallback in case the WebSocket connection fails or the event is not received:
- Poll `GET /api/sessions/active` every 30 seconds
- If a previously active session becomes inactive, treat it as a session end
- This ensures the bot will always detect session endings, even without WebSocket
## 🔍 Debugging
### Check WebSocket Logs
The backend logs WebSocket events:
```
[WebSocket] Client subscribed to session 17
[Sessions] Broadcasted session.ended event for session 17
[WebSocket] Broadcasted session.ended to 1 client(s) for session 17
```
### Common Issues
1. **Event not received:**
- Verify the client is authenticated (`auth_success` received)
- Verify the client is subscribed to the correct session
- Check backend logs for broadcast confirmation
2. **Connection drops:**
- Implement ping/pong heartbeat (send `{"type": "ping"}` every 30s)
- Handle reconnection logic in your client
3. **Multiple events received:**
- This is normal if multiple clients are subscribed
- Each client receives its own copy of the event
## 📚 Related Documentation
- [WebSocket Testing Guide](WEBSOCKET_TESTING.md)
- [Bot Integration Guide](BOT_INTEGRATION.md)
- [API Quick Reference](API_QUICK_REFERENCE.md)
## 🔗 Related Events
| Event Type | Description | When Triggered |
|------------|-------------|----------------|
| `session.started` | A new session was created | When session is created |
| `game.added` | A new game was added to the session | When a game starts |
| `session.ended` | The session has ended | When session is closed |
| `vote.received` | A vote was cast for a game | When a user votes |
## 📝 Notes
- The `session.ended` event is broadcast to **all clients subscribed to that session**
- The event includes the final `games_played` count for the session
- The `is_active` field will always be `0` for ended sessions
- The timestamp is in ISO 8601 format with timezone (UTC)

View File

@@ -0,0 +1,361 @@
# Session Start WebSocket Event
This document describes the `session.started` WebSocket event that is broadcast when a new game session is created.
## 📋 Event Overview
When a new session is created, the backend broadcasts a `session.started` event to all subscribed WebSocket clients. This allows bots and other integrations to react immediately to new game sessions.
## 🔌 WebSocket Connection
**Endpoint:** `ws://localhost:5000/api/sessions/live`
**Authentication:** Required (JWT token)
## 📨 Event Format
### Event Type
```
session.started
```
### Full Message Structure
```json
{
"type": "session.started",
"timestamp": "2025-11-01T20:00:00.123Z",
"data": {
"session": {
"id": 17,
"is_active": 1,
"created_at": "2025-11-01T20:00:00.123Z",
"notes": "Friday Game Night"
}
}
}
```
### Field Descriptions
| Field | Type | Description |
|-------|------|-------------|
| `type` | string | Always `"session.started"` |
| `timestamp` | string | ISO 8601 timestamp when the event was generated |
| `data.session.id` | number | The ID of the newly created session |
| `data.session.is_active` | number | Always `1` (active) for new sessions |
| `data.session.created_at` | string | ISO 8601 timestamp when the session was created |
| `data.session.notes` | string/null | Optional notes for the session |
## 🚀 Implementation
### Backend Implementation
The `session.started` event is automatically broadcast when:
1. **New Session Created**: Admin creates a session via `POST /api/sessions`
**Code Location:** `backend/routes/sessions.js` - `POST /` endpoint
```javascript
// Broadcast session.started event via WebSocket
const wsManager = getWebSocketManager();
if (wsManager) {
const eventData = {
session: {
id: newSession.id,
is_active: 1,
created_at: newSession.created_at,
notes: newSession.notes
}
};
wsManager.broadcastEvent('session.started', eventData, parseInt(newSession.id));
}
```
### Client Implementation Example
#### Node.js with `ws` library
```javascript
const WebSocket = require('ws');
const ws = new WebSocket('ws://localhost:5000/api/sessions/live');
ws.on('open', () => {
// Authenticate
ws.send(JSON.stringify({
type: 'auth',
token: 'your-jwt-token'
}));
});
ws.on('message', (data) => {
const message = JSON.parse(data.toString());
switch (message.type) {
case 'auth_success':
// Subscribe to the new session (or subscribe when you receive session.started)
ws.send(JSON.stringify({
type: 'subscribe',
sessionId: 17
}));
break;
case 'session.started':
console.log('New session started!');
console.log(`Session ID: ${message.data.session.id}`);
console.log(`Created at: ${message.data.session.created_at}`);
if (message.data.session.notes) {
console.log(`Notes: ${message.data.session.notes}`);
}
// Auto-subscribe to the new session
ws.send(JSON.stringify({
type: 'subscribe',
sessionId: message.data.session.id
}));
break;
}
});
```
#### Python with `websockets` library
```python
import asyncio
import json
import websockets
async def listen_for_session_start():
uri = "ws://localhost:5000/api/sessions/live"
async with websockets.connect(uri) as websocket:
# Authenticate
await websocket.send(json.dumps({
"type": "auth",
"token": "your-jwt-token"
}))
async for message in websocket:
data = json.loads(message)
if data["type"] == "auth_success":
print("Authenticated, waiting for sessions...")
elif data["type"] == "session.started":
session = data["data"]["session"]
print(f"🎮 New session started! ID: {session['id']}")
print(f"📅 Created: {session['created_at']}")
if session.get('notes'):
print(f"📝 Notes: {session['notes']}")
# Auto-subscribe to the new session
await websocket.send(json.dumps({
"type": "subscribe",
"sessionId": session["id"]
}))
asyncio.run(listen_for_session_start())
```
## 🧪 Testing
### Manual Testing Steps
1. **Start the backend server:**
```bash
cd backend
npm start
```
2. **Connect a WebSocket client** (use the test script or your own):
```bash
# You can modify ../tests/test-session-end-websocket.js to listen for session.started
```
3. **Create a new session** in the Picker UI or via API:
```bash
curl -X POST http://localhost:5000/api/sessions \
-H "Authorization: Bearer <your-jwt-token>" \
-H "Content-Type: application/json" \
-d '{"notes": "Friday Game Night"}'
```
4. **Verify the event is received** by your WebSocket client
### Expected Event
```json
{
"type": "session.started",
"timestamp": "2025-11-01T20:00:00.123Z",
"data": {
"session": {
"id": 18,
"is_active": 1,
"created_at": "2025-11-01T20:00:00.123Z",
"notes": "Friday Game Night"
}
}
}
```
## 🤖 Bot Integration
### IRC/Kosmi Bot Example
When the bot receives a `session.started` event, it should:
1. **Announce the new game session** to users
2. **Auto-subscribe to the session** to receive game.added and session.ended events
3. **Optionally display session info** (notes, ID, etc.)
Example bot response:
```
🎮 Game Night has started! Session #18
📝 Friday Game Night
🗳️ Vote with thisgame++ or thisgame-- during games!
```
### Implementation Example
```javascript
ws.on('message', (data) => {
const msg = JSON.parse(data.toString());
if (msg.type === 'session.started') {
const { id, notes, created_at } = msg.data.session;
// Announce to IRC/Discord/etc
bot.announce(`🎮 Game Night has started! Session #${id}`);
if (notes) {
bot.announce(`📝 ${notes}`);
}
bot.announce('🗳️ Vote with thisgame++ or thisgame-- during games!');
// Auto-subscribe to this session
ws.send(JSON.stringify({
type: 'subscribe',
sessionId: id
}));
}
});
```
## 🔍 Debugging
### Check WebSocket Logs
The backend logs WebSocket events:
```
[Sessions] Broadcasted session.started event for session 18
[WebSocket] Broadcasted session.started to 1 client(s) for session 18
```
### Common Issues
1. **Event not received:**
- Verify the client is authenticated (`auth_success` received)
- Check backend logs for broadcast confirmation
- **No subscription required** - All authenticated clients automatically receive `session.started` events
- Make sure your WebSocket connection is open and authenticated
2. **Missing session data:**
- Check if the session was created successfully
- Verify the API response includes all fields
3. **Duplicate events:**
- Normal if multiple clients are connected
- Each client receives its own copy of the event
## 📚 Related Documentation
- [Session End WebSocket Event](SESSION_END_WEBSOCKET.md)
- [WebSocket Testing Guide](WEBSOCKET_TESTING.md)
- [Bot Integration Guide](BOT_INTEGRATION.md)
- [API Quick Reference](API_QUICK_REFERENCE.md)
## 🔗 Session Lifecycle Events
```
session.started
game.added (multiple times)
vote.received (during each game)
session.ended
```
## 📝 Notes
- The `session.started` event is broadcast to **all authenticated clients** (not just subscribed ones)
- **No subscription required** - All authenticated clients automatically receive this event
- Clients should auto-subscribe to the new session to receive subsequent `game.added` and `vote.received` events
- The `is_active` field will always be `1` for new sessions
- The `notes` field may be `null` if no notes were provided
- The timestamp is in ISO 8601 format with timezone (UTC)
## 💡 Use Cases
1. **Bot Announcements** - Notify users when game night starts
2. **Auto-Subscription** - Automatically subscribe to new sessions
3. **Session Tracking** - Track all sessions in external systems
4. **Analytics** - Log session creation times and frequency
5. **Notifications** - Send push notifications to users
## 🎯 Best Practices
1. **Auto-subscribe** to new sessions when you receive `session.started`
2. **Store the session ID** for later reference
3. **Handle reconnections** gracefully (you might miss the event)
4. **Use polling as fallback** to detect sessions created while disconnected
5. **Validate session data** before processing
## 🔄 Complete Event Flow Example
```javascript
const WebSocket = require('ws');
const ws = new WebSocket('ws://localhost:5000/api/sessions/live');
let currentSessionId = null;
ws.on('message', (data) => {
const msg = JSON.parse(data.toString());
switch (msg.type) {
case 'session.started':
currentSessionId = msg.data.session.id;
console.log(`🎮 Session ${currentSessionId} started!`);
// Auto-subscribe
ws.send(JSON.stringify({
type: 'subscribe',
sessionId: currentSessionId
}));
break;
case 'game.added':
console.log(`🎲 New game: ${msg.data.game.title}`);
break;
case 'vote.received':
console.log(`🗳️ Vote: ${msg.data.vote.type}`);
break;
case 'session.ended':
console.log(`🌙 Session ${msg.data.session.id} ended!`);
console.log(`📊 Games played: ${msg.data.session.games_played}`);
currentSessionId = null;
break;
}
});
```
## ✨ Conclusion
The `session.started` WebSocket event provides instant notification when new game sessions are created, allowing bots and integrations to react immediately and provide a seamless user experience.

View File

@@ -0,0 +1,256 @@
# WebSocket Event Flow Diagram
## 🔄 Complete Session Lifecycle
```
┌─────────────────────────────────────────────────────────────────┐
│ BOT CONNECTS │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Bot → Server: { type: "auth", token: "..." } │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Server → Bot: { type: "auth_success" } │
│ ✅ Bot is now AUTHENTICATED │
│ ⏳ Bot waits... (no subscription yet) │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ ADMIN CREATES SESSION │
│ POST /api/sessions { notes: "Friday Game Night" } │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Server → ALL AUTHENTICATED CLIENTS: │
│ { │
│ type: "session.started", │
│ data: { │
│ session: { id: 22, is_active: 1, ... } │
│ } │
│ } │
│ 📢 Broadcast to ALL (no subscription needed) │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Bot receives session.started │
│ 🎮 Bot announces: "Game Night #22 has started!" │
│ │
│ Bot → Server: { type: "subscribe", sessionId: 22 } │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Server → Bot: { type: "subscribed", sessionId: 22 } │
│ ✅ Bot is now SUBSCRIBED to session 22 │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ ADMIN ADDS GAME │
│ POST /api/sessions/22/games { game_id: 45 } │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Server → SUBSCRIBED CLIENTS (session 22): │
│ { │
│ type: "game.added", │
│ data: { │
│ game: { title: "Quiplash 3", ... } │
│ } │
│ } │
│ 📢 Only to subscribers of session 22 │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Bot receives game.added │
│ 🎲 Bot announces: "Now playing: Quiplash 3" │
│ 🗳️ Bot announces: "Vote with thisgame++ or thisgame--" │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ USER VOTES │
│ POST /api/votes/live { username: "Alice", vote: "up" } │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Server → SUBSCRIBED CLIENTS (session 22): │
│ { │
│ type: "vote.received", │
│ data: { │
│ vote: { username: "Alice", type: "up" } │
│ } │
│ } │
│ 📢 Only to subscribers of session 22 │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Bot receives vote.received │
│ 🗳️ Bot tracks vote (may announce later) │
└─────────────────────────────────────────────────────────────────┘
(more games and votes...)
┌─────────────────────────────────────────────────────────────────┐
│ ADMIN CLOSES SESSION │
│ POST /api/sessions/22/close │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Server → SUBSCRIBED CLIENTS (session 22): │
│ { │
│ type: "session.ended", │
│ data: { │
│ session: { id: 22, is_active: 0, games_played: 5 } │
│ } │
│ } │
│ 📢 Only to subscribers of session 22 │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Bot receives session.ended │
│ 🗳️ Bot announces: "Final votes for Quiplash 3: 5👍 1👎" │
│ 🌙 Bot announces: "Game Night ended! 5 games played" │
│ ⏳ Bot waits for next session.started... │
└─────────────────────────────────────────────────────────────────┘
```
## 📊 Broadcast Scope Comparison
### session.started (Global Broadcast)
```
┌─────────────────────────────────────────────────────────────────┐
│ SERVER │
│ │
│ broadcastToAll('session.started', data) │
└─────────────────────────────────────────────────────────────────┘
↓ ↓ ↓
┌──────────┴───────────┴───────────┴──────────┐
↓ ↓ ↓
┌────────┐ ┌────────┐ ┌────────┐
│ Bot A │ │ Bot B │ │ Bot C │
│ ✅ │ │ ✅ │ │ ✅ │
└────────┘ └────────┘ └────────┘
ALL authenticated clients receive it
(no subscription required)
```
### game.added, vote.received, session.ended (Session-Specific)
```
┌─────────────────────────────────────────────────────────────────┐
│ SERVER │
│ │
│ broadcastEvent('game.added', data, sessionId: 22) │
└─────────────────────────────────────────────────────────────────┘
┌─────────┴─────────┐
↓ ↓
┌────────┐ ┌────────┐ ┌────────┐
│ Bot A │ │ Bot B │ │ Bot C │
│ ✅ │ │ ❌ │ │ ✅ │
│subscr. │ │ not │ │subscr. │
│sess 22 │ │subscr. │ │sess 22 │
└────────┘ └────────┘ └────────┘
ONLY subscribers to session 22 receive it
```
## 🎯 Bot State Machine
```
┌─────────────┐
│ DISCONNECTED│
└──────┬──────┘
│ connect()
┌─────────────┐
│ CONNECTED │
└──────┬──────┘
│ send auth
┌─────────────┐
│AUTHENTICATED│ ← Wait here for session.started
└──────┬──────┘ (no subscription yet)
│ receive session.started
┌─────────────┐
│ WAITING │
│ TO │
│ SUBSCRIBE │
└──────┬──────┘
│ send subscribe
┌─────────────┐
│ SUBSCRIBED │ ← Now receive game.added, vote.received, session.ended
└──────┬──────┘
│ receive session.ended
┌─────────────┐
│AUTHENTICATED│ ← Back to waiting for next session.started
└─────────────┘ (still authenticated, but not subscribed)
```
## 🔍 Event Flow by Subscription Status
### Before Subscription (Just Authenticated)
```
Server Events: Bot Receives:
───────────── ─────────────
session.started ✅ YES (broadcast to all)
game.added ❌ NO (not subscribed yet)
vote.received ❌ NO (not subscribed yet)
session.ended ❌ NO (not subscribed yet)
```
### After Subscription (Subscribed to Session 22)
```
Server Events: Bot Receives:
───────────── ─────────────
session.started ✅ YES (still broadcast to all)
game.added ✅ YES (subscribed to session 22)
vote.received ✅ YES (subscribed to session 22)
session.ended ✅ YES (subscribed to session 22)
```
## 🎮 Multiple Sessions Example
```
Time Event Bot A (sess 22) Bot B (sess 23)
──── ───── ─────────────── ───────────────
10:00 session.started (sess 22) ✅ Receives ✅ Receives
10:01 Bot A subscribes to sess 22 ✅ Subscribed ❌ Not subscribed
10:02 game.added (sess 22) ✅ Receives ❌ Doesn't receive
10:05 session.started (sess 23) ✅ Receives ✅ Receives
10:06 Bot B subscribes to sess 23 ✅ Still sess 22 ✅ Subscribed
10:07 game.added (sess 22) ✅ Receives ❌ Doesn't receive
10:08 game.added (sess 23) ❌ Doesn't receive ✅ Receives
10:10 session.ended (sess 22) ✅ Receives ❌ Doesn't receive
10:15 session.ended (sess 23) ❌ Doesn't receive ✅ Receives
```
## 📝 Quick Reference
| Event | Broadcast Method | Scope | Subscription Required? |
|-------|------------------|-------|------------------------|
| `session.started` | `broadcastToAll()` | All authenticated clients | ❌ NO |
| `game.added` | `broadcastEvent()` | Session subscribers only | ✅ YES |
| `vote.received` | `broadcastEvent()` | Session subscribers only | ✅ YES |
| `session.ended` | `broadcastEvent()` | Session subscribers only | ✅ YES |
## 🔗 Related Documentation
- [WEBSOCKET_SUBSCRIPTION_GUIDE.md](WEBSOCKET_SUBSCRIPTION_GUIDE.md) - Detailed subscription guide
- [SESSION_START_WEBSOCKET.md](SESSION_START_WEBSOCKET.md) - session.started event
- [SESSION_END_WEBSOCKET.md](SESSION_END_WEBSOCKET.md) - session.ended event
- [BOT_INTEGRATION.md](BOT_INTEGRATION.md) - Bot integration guide

View File

@@ -0,0 +1,310 @@
# WebSocket Subscription Guide
## 📋 Overview
This guide explains how WebSocket subscriptions work in the Jackbox Game Picker and which events require subscriptions.
## 🔌 Connection & Authentication
### 1. Connect to WebSocket
```javascript
const ws = new WebSocket('ws://localhost:5000/api/sessions/live');
```
### 2. Authenticate
```javascript
ws.send(JSON.stringify({
type: 'auth',
token: 'YOUR_JWT_TOKEN'
}));
```
### 3. Wait for Auth Success
```javascript
ws.on('message', (data) => {
const msg = JSON.parse(data.toString());
if (msg.type === 'auth_success') {
console.log('Authenticated!');
// Now you can subscribe to sessions
}
});
```
## 📨 Event Types & Subscription Requirements
| Event Type | Requires Subscription? | Broadcast To | When to Subscribe |
|------------|------------------------|--------------|-------------------|
| `session.started` | ❌ **NO** | All authenticated clients | N/A - Automatic |
| `game.added` | ✅ **YES** | Subscribed clients only | After session.started |
| `vote.received` | ✅ **YES** | Subscribed clients only | After session.started |
| `session.ended` | ✅ **YES** | Subscribed clients only | After session.started |
## 🎯 Subscription Strategy
### Strategy 1: Auto-Subscribe to New Sessions (Recommended for Bots)
```javascript
ws.on('message', (data) => {
const msg = JSON.parse(data.toString());
// After authentication
if (msg.type === 'auth_success') {
console.log('✅ Authenticated');
}
// Auto-subscribe to new sessions
if (msg.type === 'session.started') {
const sessionId = msg.data.session.id;
console.log(`🎮 New session ${sessionId} started!`);
// Subscribe to this session
ws.send(JSON.stringify({
type: 'subscribe',
sessionId: sessionId
}));
}
// Now you'll receive game.added, vote.received, and session.ended
if (msg.type === 'game.added') {
console.log(`🎲 Game: ${msg.data.game.title}`);
}
if (msg.type === 'session.ended') {
console.log(`🌙 Session ended! ${msg.data.session.games_played} games played`);
}
});
```
### Strategy 2: Subscribe to Active Session on Connect
```javascript
ws.on('message', (data) => {
const msg = JSON.parse(data.toString());
if (msg.type === 'auth_success') {
console.log('✅ Authenticated');
// Fetch active session from API
fetch('http://localhost:5000/api/sessions/active')
.then(res => res.json())
.then(session => {
if (session && session.id) {
// Subscribe to active session
ws.send(JSON.stringify({
type: 'subscribe',
sessionId: session.id
}));
}
});
}
});
```
### Strategy 3: Subscribe to Specific Session
```javascript
ws.on('message', (data) => {
const msg = JSON.parse(data.toString());
if (msg.type === 'auth_success') {
console.log('✅ Authenticated');
// Subscribe to specific session
const sessionId = 17;
ws.send(JSON.stringify({
type: 'subscribe',
sessionId: sessionId
}));
}
if (msg.type === 'subscribed') {
console.log(`✅ Subscribed to session ${msg.sessionId}`);
}
});
```
## 🔄 Complete Bot Flow
```javascript
const WebSocket = require('ws');
class JackboxBot {
constructor(token) {
this.token = token;
this.ws = null;
this.currentSessionId = null;
}
connect() {
this.ws = new WebSocket('ws://localhost:5000/api/sessions/live');
this.ws.on('open', () => {
console.log('🔌 Connected to WebSocket');
this.authenticate();
});
this.ws.on('message', (data) => {
this.handleMessage(JSON.parse(data.toString()));
});
}
authenticate() {
this.ws.send(JSON.stringify({
type: 'auth',
token: this.token
}));
}
handleMessage(msg) {
switch (msg.type) {
case 'auth_success':
console.log('✅ Authenticated');
// Don't subscribe yet - wait for session.started
break;
case 'session.started':
this.currentSessionId = msg.data.session.id;
console.log(`🎮 Session ${this.currentSessionId} started!`);
// Auto-subscribe to this session
this.ws.send(JSON.stringify({
type: 'subscribe',
sessionId: this.currentSessionId
}));
// Announce to users
this.announce(`Game Night has started! Session #${this.currentSessionId}`);
break;
case 'subscribed':
console.log(`✅ Subscribed to session ${msg.sessionId}`);
break;
case 'game.added':
console.log(`🎲 Game: ${msg.data.game.title}`);
this.announce(`Now playing: ${msg.data.game.title}`);
break;
case 'vote.received':
console.log(`🗳️ Vote: ${msg.data.vote.type}`);
break;
case 'session.ended':
console.log(`🌙 Session ${msg.data.session.id} ended`);
this.announce(`Game Night ended! ${msg.data.session.games_played} games played`);
this.currentSessionId = null;
break;
}
}
announce(message) {
// Send to IRC/Discord/Kosmi/etc
console.log(`📢 ${message}`);
}
}
// Usage
const bot = new JackboxBot('YOUR_JWT_TOKEN');
bot.connect();
```
## 📊 Subscription Lifecycle
```
1. Connect to WebSocket
2. Send auth message
3. Receive auth_success
4. Wait for session.started (no subscription needed)
5. Receive session.started
6. Send subscribe message with sessionId
7. Receive subscribed confirmation
8. Now receive: game.added, vote.received, session.ended
9. Receive session.ended
10. Wait for next session.started (repeat from step 4)
```
## 🔍 Debugging
### Check What You're Subscribed To
The WebSocket manager tracks subscriptions. Check backend logs:
```
[WebSocket] Client subscribed to session 17
[WebSocket] Client unsubscribed from session 17
```
### Verify Event Reception
**session.started** - Should receive immediately after authentication (no subscription needed):
```
[WebSocket] Broadcasted session.started to 2 authenticated client(s)
```
**game.added, vote.received, session.ended** - Only after subscribing:
```
[WebSocket] Broadcasted game.added to 1 client(s) for session 17
```
### Common Issues
1. **Not receiving session.started:**
- ✅ Are you authenticated?
- ✅ Is your WebSocket connection open?
- ✅ Check backend logs for broadcast confirmation
2. **Not receiving game.added:**
- ✅ Did you subscribe to the session?
- ✅ Did you receive `subscribed` confirmation?
- ✅ Is the session ID correct?
3. **Not receiving session.ended:**
- ✅ Are you still subscribed to the session?
- ✅ Did the session actually close?
- ✅ Check backend logs
## 🎯 Best Practices
1. **Auto-subscribe to new sessions** when you receive `session.started`
2. **Don't subscribe before session.started** - there's nothing to subscribe to yet
3. **Handle reconnections** - re-authenticate and re-subscribe on reconnect
4. **Use polling as fallback** - poll `/api/sessions/active` every 30s as backup
5. **Unsubscribe when done** - clean up subscriptions when you're done with a session
6. **Validate session IDs** - make sure the session exists before subscribing
## 📝 Summary
### ❌ No Subscription Required
- `session.started` - Broadcast to **all authenticated clients**
### ✅ Subscription Required
- `game.added` - Only to **subscribed clients**
- `vote.received` - Only to **subscribed clients**
- `session.ended` - Only to **subscribed clients**
### 🎯 Recommended Flow
1. Authenticate
2. Wait for `session.started` (automatic)
3. Subscribe to the session
4. Receive `game.added`, `vote.received`, `session.ended`
5. Repeat from step 2 for next session
## 🔗 Related Documentation
- [SESSION_START_WEBSOCKET.md](SESSION_START_WEBSOCKET.md) - session.started event details
- [SESSION_END_WEBSOCKET.md](SESSION_END_WEBSOCKET.md) - session.ended event details
- [API_QUICK_REFERENCE.md](API_QUICK_REFERENCE.md) - API reference
- [BOT_INTEGRATION.md](BOT_INTEGRATION.md) - Bot integration guide

239
docs/WEBSOCKET_TESTING.md Normal file
View File

@@ -0,0 +1,239 @@
# WebSocket Integration Testing Guide
This guide walks you through testing the WebSocket event system for game notifications.
## Prerequisites
1. Backend API running with WebSocket support
2. Valid JWT token for authentication
3. Active session with games (or ability to create one)
## Testing Steps
### Step 1: Install Backend Dependencies
```bash
cd backend
npm install
```
This will install the `ws` package that was added to `package.json`.
### Step 2: Start the Backend
```bash
cd backend
npm start
```
You should see:
```
Server is running on port 5000
WebSocket server available at ws://localhost:5000/api/sessions/live
[WebSocket] WebSocket server initialized on /api/sessions/live
```
### Step 3: Get JWT Token
```bash
curl -X POST "http://localhost:5000/api/auth/login" \
-H "Content-Type: application/json" \
-d '{"key":"YOUR_ADMIN_KEY"}'
```
Save the token from the response.
### Step 4: Test WebSocket Connection
Run the test script:
```bash
cd backend
JWT_TOKEN="your_token_here" node test-websocket.js
```
Expected output:
```
🚀 WebSocket Test Client
═══════════════════════════════════════════════════════
Connecting to: ws://localhost:5000/api/sessions/live
✅ Connected to WebSocket server
📝 Step 1: Authenticating...
✅ Authentication successful
📝 Step 2: Subscribing to session 1...
✅ Subscribed to session 1
🎧 Listening for events...
Add a game in the Picker page to see events here
Press Ctrl+C to exit
```
### Step 5: Test Game Added Event
1. Keep the WebSocket test client running
2. Open the web app in your browser
3. Go to the Picker page
4. Add a game to the session
You should see in the test client:
```
🎮 GAME ADDED EVENT RECEIVED!
═══════════════════════════════════════════════════════
Game: Fibbage 4
Pack: The Jackbox Party Pack 9
Players: 2-8
Session ID: 1
Games Played: 1
Timestamp: 2025-11-01T...
═══════════════════════════════════════════════════════
```
### Step 6: Test Bot Integration
If you're using the `irc-kosmi-relay` bot:
1. Make sure `UseWebSocket=true` in `matterbridge.toml`
2. Build and run the bot:
```bash
cd irc-kosmi-relay
go build
./matterbridge -conf matterbridge.toml
```
3. Look for these log messages:
```
INFO Jackbox integration initialized successfully
INFO Connecting to WebSocket: wss://your-api-url/api/sessions/live
INFO WebSocket connected
INFO Authentication successful
INFO Subscribed to session X
```
4. Add a game in the Picker page
5. The bot should announce in Kosmi/IRC:
```
🎮 Coming up next: Fibbage 4!
```
## Troubleshooting
### Connection Refused
**Problem**: `Error: connect ECONNREFUSED`
**Solution**: Make sure the backend is running on the correct port (default 5000).
### Authentication Failed
**Problem**: `Authentication failed: Invalid or expired token`
**Solution**:
- Get a fresh JWT token
- Make sure you're using the correct admin key
- Check token hasn't expired (tokens expire after 24 hours)
### No Events Received
**Problem**: WebSocket connects but no `game.added` events are received
**Solution**:
- Make sure you're subscribed to the correct session ID
- Verify the session is active
- Check backend logs for errors
- Try adding a game manually via the Picker page
### Bot Not Connecting
**Problem**: Bot fails to connect to WebSocket
**Solution**:
- Check `APIURL` in `matterbridge.toml` is correct
- Verify `UseWebSocket=true` is set
- Check bot has valid JWT token (authentication succeeded)
- Look for error messages in bot logs
### Reconnection Issues
**Problem**: WebSocket disconnects and doesn't reconnect
**Solution**:
- Check network connectivity
- Backend automatically handles reconnection with exponential backoff
- Bot automatically reconnects on disconnect
- Check logs for reconnection attempts
## Advanced Testing
### Test Multiple Clients
You can run multiple test clients simultaneously:
```bash
# Terminal 1
JWT_TOKEN="token1" node test-websocket.js
# Terminal 2
JWT_TOKEN="token2" node test-websocket.js
```
Both should receive the same `game.added` events.
### Test Heartbeat
The WebSocket connection sends ping/pong messages every 30 seconds. You should see:
```
💓 Heartbeat
```
If you don't see heartbeats, the connection may be stale.
### Test Reconnection
1. Start the test client
2. Stop the backend (Ctrl+C)
3. The client should log: `WebSocket disconnected, reconnecting...`
4. Restart the backend
5. The client should reconnect automatically
## Integration Checklist
- [ ] Backend WebSocket server starts successfully
- [ ] Test client can connect and authenticate
- [ ] Test client receives `game.added` events
- [ ] Heartbeat keeps connection alive (30s interval)
- [ ] Auto-reconnect works after disconnect
- [ ] Multiple clients can connect simultaneously
- [ ] Invalid JWT is rejected properly
- [ ] Bot connects and authenticates
- [ ] Bot receives events and broadcasts to chat
- [ ] Bot reconnects after network issues
## Next Steps
Once testing is complete:
1. Update your bot configuration to use `UseWebSocket=true`
2. Deploy the updated backend with WebSocket support
3. Restart your bot to connect via WebSocket
4. Monitor logs for any connection issues
5. Webhooks remain available as a fallback option
## Comparison: WebSocket vs Webhooks
| Feature | WebSocket | Webhooks |
|---------|-----------|----------|
| Setup Complexity | Simple | Moderate |
| Inbound Ports | Not needed | Required |
| Docker Networking | Simple | Complex |
| Latency | Lower | Higher |
| Connection Type | Persistent | Per-event |
| Reconnection | Automatic | N/A |
| Best For | Real-time bots | Serverless integrations |
**Recommendation**: Use WebSocket for bot integrations. Use webhooks for serverless/stateless integrations or when WebSocket is not feasible.

34
docs/todos.md Normal file
View File

@@ -0,0 +1,34 @@
# TODO:
## Chrome Extension
- [x] /.old-chrome-extension/ contains OLD code that needs adjusting for new game picker format. (COMPLETED: New simplified extension in /chrome-extension/)
- [x] remove clunky gamealias system, we are only tracking "thisgame++" and "thisgame--" now.
- [x] ensure the extension is watching for "thisgame++" or "thisgame--" anywhere in each chat line.
- [x] if a chat line matches capture the whole message/line, the author, and the timestamp (UTC).
- [x] ensure our JSON output matches the new format:
```json
[
{
"username": "Alice",
"message": "thisgame++",
"timestamp": "2024-10-30T20:15:00Z"
},
{
"username": "Bob",
"message": "This is fun! thisgame++",
"timestamp": "2024-10-30T20:16:30Z"
}
]
```
## Game Manager
- [x] implement favoring system (for packs and games). (COMPLETED: Full weighted selection system with visual indicators)
- [x] if a game or pack is marked favored (👍), then we bias the picking algorithm to pick those games.
- [x] if a game or pack is marked as disfavored (👎), then we bias the picking algorithm to not pick those games.
- [x] biased games/packs should be highlighted subtley somehow in *any* lists they're in elsewhere in the UI, like the Pool Viewer.
## Bug Fixes
- [x] Entire App: local timezone display still isn't working. I see UTC times. (FIXED: Created dateUtils.js to properly parse SQLite UTC timestamps)
## Other Features
- [x] Session History: export sessions to plaintext and JSON. (COMPLETED: Export buttons in History page)