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