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:
746
docs/BOT_INTEGRATION.md
Normal file
746
docs/BOT_INTEGRATION.md
Normal 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
|
||||
|
||||
Reference in New Issue
Block a user