wow, chrome-extension MUCH improved - websockets
This commit is contained in:
@@ -440,5 +440,138 @@ router.delete('/:sessionId/games/:gameId', authenticateToken, (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Export session data (plaintext and JSON)
|
||||||
|
router.get('/:id/export', authenticateToken, (req, res) => {
|
||||||
|
try {
|
||||||
|
const { format } = req.query; // 'json' or 'txt'
|
||||||
|
const sessionId = req.params.id;
|
||||||
|
|
||||||
|
// Get session info
|
||||||
|
const session = db.prepare(`
|
||||||
|
SELECT
|
||||||
|
s.*,
|
||||||
|
COUNT(sg.id) as games_played
|
||||||
|
FROM sessions s
|
||||||
|
LEFT JOIN session_games sg ON s.id = sg.session_id
|
||||||
|
WHERE s.id = ?
|
||||||
|
GROUP BY s.id
|
||||||
|
`).get(sessionId);
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
return res.status(404).json({ error: 'Session not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get games for this session
|
||||||
|
const games = db.prepare(`
|
||||||
|
SELECT
|
||||||
|
sg.*,
|
||||||
|
g.title,
|
||||||
|
g.pack_name,
|
||||||
|
g.min_players,
|
||||||
|
g.max_players,
|
||||||
|
g.game_type
|
||||||
|
FROM session_games sg
|
||||||
|
JOIN games g ON sg.game_id = g.id
|
||||||
|
WHERE sg.session_id = ?
|
||||||
|
ORDER BY sg.played_at ASC
|
||||||
|
`).all(sessionId);
|
||||||
|
|
||||||
|
// Get chat logs if any
|
||||||
|
const chatLogs = db.prepare(`
|
||||||
|
SELECT * FROM chat_logs
|
||||||
|
WHERE session_id = ?
|
||||||
|
ORDER BY timestamp ASC
|
||||||
|
`).all(sessionId);
|
||||||
|
|
||||||
|
if (format === 'json') {
|
||||||
|
// JSON Export
|
||||||
|
const exportData = {
|
||||||
|
session: {
|
||||||
|
id: session.id,
|
||||||
|
created_at: session.created_at,
|
||||||
|
closed_at: session.closed_at,
|
||||||
|
is_active: session.is_active === 1,
|
||||||
|
notes: session.notes,
|
||||||
|
games_played: session.games_played
|
||||||
|
},
|
||||||
|
games: games.map(game => ({
|
||||||
|
title: game.title,
|
||||||
|
pack: game.pack_name,
|
||||||
|
players: `${game.min_players}-${game.max_players}`,
|
||||||
|
type: game.game_type,
|
||||||
|
played_at: game.played_at,
|
||||||
|
manually_added: game.manually_added === 1,
|
||||||
|
status: game.status
|
||||||
|
})),
|
||||||
|
chat_logs: chatLogs.map(log => ({
|
||||||
|
username: log.chatter_name,
|
||||||
|
message: log.message,
|
||||||
|
timestamp: log.timestamp,
|
||||||
|
vote: log.parsed_vote
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
|
||||||
|
res.setHeader('Content-Type', 'application/json');
|
||||||
|
res.setHeader('Content-Disposition', `attachment; filename="session-${sessionId}.json"`);
|
||||||
|
res.send(JSON.stringify(exportData, null, 2));
|
||||||
|
} else {
|
||||||
|
// Plain Text Export
|
||||||
|
let text = `JACKBOX GAME PICKER - SESSION EXPORT\n`;
|
||||||
|
text += `${'='.repeat(50)}\n\n`;
|
||||||
|
text += `Session ID: ${session.id}\n`;
|
||||||
|
text += `Created: ${session.created_at}\n`;
|
||||||
|
if (session.closed_at) {
|
||||||
|
text += `Closed: ${session.closed_at}\n`;
|
||||||
|
}
|
||||||
|
text += `Status: ${session.is_active ? 'Active' : 'Ended'}\n`;
|
||||||
|
if (session.notes) {
|
||||||
|
text += `Notes: ${session.notes}\n`;
|
||||||
|
}
|
||||||
|
text += `\nGames Played: ${session.games_played}\n`;
|
||||||
|
text += `\n${'='.repeat(50)}\n\n`;
|
||||||
|
|
||||||
|
if (games.length > 0) {
|
||||||
|
text += `GAMES:\n`;
|
||||||
|
text += `${'-'.repeat(50)}\n`;
|
||||||
|
games.forEach((game, index) => {
|
||||||
|
text += `\n${index + 1}. ${game.title}\n`;
|
||||||
|
text += ` Pack: ${game.pack_name}\n`;
|
||||||
|
text += ` Players: ${game.min_players}-${game.max_players}\n`;
|
||||||
|
if (game.game_type) {
|
||||||
|
text += ` Type: ${game.game_type}\n`;
|
||||||
|
}
|
||||||
|
text += ` Played: ${game.played_at}\n`;
|
||||||
|
text += ` Status: ${game.status}\n`;
|
||||||
|
if (game.manually_added === 1) {
|
||||||
|
text += ` (Manually Added)\n`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
text += `\n${'-'.repeat(50)}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chatLogs.length > 0) {
|
||||||
|
text += `\nCHAT LOGS:\n`;
|
||||||
|
text += `${'-'.repeat(50)}\n`;
|
||||||
|
chatLogs.forEach(log => {
|
||||||
|
text += `\n[${log.timestamp}] ${log.chatter_name}:\n`;
|
||||||
|
text += ` ${log.message}\n`;
|
||||||
|
if (log.parsed_vote) {
|
||||||
|
text += ` Vote: ${log.parsed_vote}\n`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
text += `\n${'-'.repeat(50)}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
text += `\nEnd of Session Export\n`;
|
||||||
|
|
||||||
|
res.setHeader('Content-Type', 'text/plain');
|
||||||
|
res.setHeader('Content-Disposition', `attachment; filename="session-${sessionId}.txt"`);
|
||||||
|
res.send(text);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|
||||||
|
|||||||
179
chrome-extension/DEBUG.md
Normal file
179
chrome-extension/DEBUG.md
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
# Chrome Extension Debugging Guide
|
||||||
|
|
||||||
|
## Version 3.0.2 - Apollo Client Detection Issues
|
||||||
|
|
||||||
|
If the extension can't find Apollo Client, follow these steps to diagnose the issue:
|
||||||
|
|
||||||
|
### Step 1: Reload Extension and Page
|
||||||
|
|
||||||
|
1. Go to `chrome://extensions/`
|
||||||
|
2. Find "Jackbox Chat Tracker for Kosmi"
|
||||||
|
3. Click the reload icon (circular arrow)
|
||||||
|
4. Navigate to Kosmi room: `https://app.kosmi.io/room/@yourroomname`
|
||||||
|
5. Refresh the page (Ctrl+R or Cmd+R)
|
||||||
|
|
||||||
|
### Step 2: Run Debug Function
|
||||||
|
|
||||||
|
1. Open Chrome DevTools (F12)
|
||||||
|
2. Go to Console tab
|
||||||
|
3. Wait 5-10 seconds after page load (let Kosmi fully initialize)
|
||||||
|
4. Type: `window.debugJackboxTracker()`
|
||||||
|
5. Press Enter
|
||||||
|
|
||||||
|
### Step 3: Analyze Output
|
||||||
|
|
||||||
|
The debug function will show:
|
||||||
|
|
||||||
|
#### If Apollo Client is Found ✓
|
||||||
|
```
|
||||||
|
=== Jackbox Chat Tracker Debug ===
|
||||||
|
✓ Apollo Client found!
|
||||||
|
Apollo Client: ApolloClient {...}
|
||||||
|
Apollo Client keys: ['cache', 'link', 'queryManager', ...]
|
||||||
|
Apollo Link: {...}
|
||||||
|
Apollo Link keys: ['client', ...]
|
||||||
|
GraphQL client: {...}
|
||||||
|
GraphQL client keys: ['on', 'subscribe', 'iterate', 'dispose', 'terminate']
|
||||||
|
=== End Debug ===
|
||||||
|
```
|
||||||
|
|
||||||
|
**Action:** Try clicking "Start Tracking" in the extension popup. It should work now!
|
||||||
|
|
||||||
|
#### If Apollo Client is NOT Found ✗
|
||||||
|
```
|
||||||
|
=== Jackbox Chat Tracker Debug ===
|
||||||
|
✗ Apollo Client not found
|
||||||
|
Checking for window.__APOLLO_CLIENT__: undefined
|
||||||
|
Window keys that might be relevant: ['__someOtherKey__', ...]
|
||||||
|
=== End Debug ===
|
||||||
|
```
|
||||||
|
|
||||||
|
**Action:** Copy the "Window keys that might be relevant" list and report it to the developer.
|
||||||
|
|
||||||
|
### Step 4: Check Timing
|
||||||
|
|
||||||
|
If Apollo Client is not found immediately, wait longer and try again:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Run debug after 10 seconds
|
||||||
|
setTimeout(() => window.debugJackboxTracker(), 10000);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 5: Manual Apollo Client Search
|
||||||
|
|
||||||
|
If still not working, manually check for Apollo Client:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Check if it exists
|
||||||
|
console.log('window.__APOLLO_CLIENT__:', window.__APOLLO_CLIENT__);
|
||||||
|
|
||||||
|
// Search all window properties
|
||||||
|
Object.keys(window).forEach(key => {
|
||||||
|
if (key.includes('apollo') || key.includes('Apollo')) {
|
||||||
|
console.log('Found Apollo-related key:', key, window[key]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if it's in a different location
|
||||||
|
console.log('document.__APOLLO_CLIENT__:', document.__APOLLO_CLIENT__);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
#### Issue: "Apollo Client not found after 15 attempts"
|
||||||
|
|
||||||
|
**Causes:**
|
||||||
|
1. Kosmi changed their client variable name
|
||||||
|
2. Apollo Client loads after 15 seconds
|
||||||
|
3. You're not on a Kosmi room page
|
||||||
|
|
||||||
|
**Solutions:**
|
||||||
|
1. Wait longer (30+ seconds) and run `window.debugJackboxTracker()`
|
||||||
|
2. Make sure you're on `app.kosmi.io/room/...` not just `app.kosmi.io`
|
||||||
|
3. Try a different room
|
||||||
|
4. Check if Kosmi updated their code
|
||||||
|
|
||||||
|
#### Issue: "GraphQL client not found in Apollo Client"
|
||||||
|
|
||||||
|
**Cause:** Apollo Client structure changed
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
1. Run debug function to see Apollo Client structure
|
||||||
|
2. Look for the WebSocket client in a different location
|
||||||
|
3. Report the structure to the developer
|
||||||
|
|
||||||
|
### Reporting Issues
|
||||||
|
|
||||||
|
When reporting Apollo Client not found, include:
|
||||||
|
|
||||||
|
1. **Console Output:**
|
||||||
|
```
|
||||||
|
// Copy ALL of this:
|
||||||
|
window.debugJackboxTracker()
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Window Keys:**
|
||||||
|
```javascript
|
||||||
|
// Copy this:
|
||||||
|
Object.keys(window).filter(k => k.startsWith('__') || k.includes('apollo'))
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Kosmi URL:**
|
||||||
|
- Full URL of the room (redact room name if private)
|
||||||
|
|
||||||
|
4. **Browser Version:**
|
||||||
|
- Chrome version (chrome://version/)
|
||||||
|
|
||||||
|
5. **Extension Version:**
|
||||||
|
- Check `chrome://extensions/` → "Jackbox Chat Tracker" → version number
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Advanced Debugging
|
||||||
|
|
||||||
|
### Hook Detection
|
||||||
|
|
||||||
|
Check if the WebSocket hook is working:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// This should exist after successfully starting tracking
|
||||||
|
console.log('GraphQL Client:', window.__APOLLO_CLIENT__?.link?.client);
|
||||||
|
|
||||||
|
// Try manually hooking
|
||||||
|
const client = window.__APOLLO_CLIENT__.link.client;
|
||||||
|
client.on('message', (data) => {
|
||||||
|
console.log('MANUAL HOOK - Message:', data);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Message Interception
|
||||||
|
|
||||||
|
Test if messages are flowing:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Send a test message in Kosmi chat: "test thisgame++"
|
||||||
|
// Check console for:
|
||||||
|
// [Jackbox Chat Tracker] New message: {...}
|
||||||
|
// [Jackbox Chat Tracker] ✓ Vote detected!
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
|
||||||
|
### v3.0.2
|
||||||
|
- Added multiple Apollo Client detection strategies
|
||||||
|
- Added `window.debugJackboxTracker()` debug function
|
||||||
|
- Extended search to all window/document properties
|
||||||
|
- Increased retry limit to 15 attempts (15-30 seconds)
|
||||||
|
- Added detailed error logging
|
||||||
|
|
||||||
|
### v3.0.1
|
||||||
|
- Fixed infinite loop on page load
|
||||||
|
- Disabled auto-start tracking
|
||||||
|
- Added retry limit
|
||||||
|
|
||||||
|
### v3.0.0
|
||||||
|
- Initial WebSocket API implementation
|
||||||
|
- Replaced DOM parsing with GraphQL subscriptions
|
||||||
|
|
||||||
213
chrome-extension/README.md
Normal file
213
chrome-extension/README.md
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
# Jackbox Chat Tracker Chrome Extension
|
||||||
|
|
||||||
|
A Chrome extension for tracking Jackbox game votes (`thisgame++` and `thisgame--`) from Kosmi chat.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- ✅ Tracks `thisgame++` and `thisgame--` votes in Kosmi chat
|
||||||
|
- ✅ Records username, full message, and UTC timestamp
|
||||||
|
- ✅ Case-insensitive matching (works with `THISGAME++`, `ThisGame--`, etc.)
|
||||||
|
- ✅ Matches votes anywhere in the message
|
||||||
|
- ✅ Exports data as JSON in the format expected by the Jackbox Game Picker app
|
||||||
|
- ✅ Real-time vote tracking with live updates
|
||||||
|
- ✅ Simple, intuitive interface
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Open Chrome and navigate to `chrome://extensions/`
|
||||||
|
2. Enable "Developer mode" in the top right
|
||||||
|
3. Click "Load unpacked"
|
||||||
|
4. Select the `chrome-extension` folder
|
||||||
|
5. The extension is now installed!
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Starting a Tracking Session
|
||||||
|
|
||||||
|
1. Navigate to a Kosmi room: `https://app.kosmi.io/room/...`
|
||||||
|
2. Click the extension icon in your browser toolbar
|
||||||
|
3. Click "Start Tracking"
|
||||||
|
4. The extension will now monitor chat for votes
|
||||||
|
|
||||||
|
### Viewing Votes
|
||||||
|
|
||||||
|
- The popup shows all recorded votes in real-time
|
||||||
|
- Each vote displays:
|
||||||
|
- Username
|
||||||
|
- Full message text
|
||||||
|
- Timestamp (in your local timezone)
|
||||||
|
- Vote type (positive/negative indicated by color)
|
||||||
|
|
||||||
|
### Exporting Data
|
||||||
|
|
||||||
|
1. Click "Export JSON" to download votes
|
||||||
|
2. The file format matches the Jackbox Game Picker import format:
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"username": "Alice",
|
||||||
|
"message": "thisgame++",
|
||||||
|
"timestamp": "2025-10-30T20:15:00Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
3. Import this JSON file into the Jackbox Game Picker app
|
||||||
|
|
||||||
|
### Resetting Votes
|
||||||
|
|
||||||
|
- Click "Reset Votes" to clear all recorded votes
|
||||||
|
- Confirmation dialog prevents accidental resets
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
The extension hooks into Kosmi's GraphQL WebSocket API (`wss://engine.kosmi.io/gql-ws`) to monitor real-time chat messages. This is much more reliable than DOM parsing!
|
||||||
|
|
||||||
|
When a message is received, the extension:
|
||||||
|
1. Checks if it contains `thisgame++` or `thisgame--` (case-insensitive)
|
||||||
|
2. Extracts the **username** from the GraphQL response
|
||||||
|
3. Extracts the **full message text**
|
||||||
|
4. Records the **UTC timestamp**
|
||||||
|
|
||||||
|
### Why WebSocket?
|
||||||
|
|
||||||
|
Previous versions used DOM parsing (MutationObserver), which was fragile and prone to breaking when Kosmi updated their UI. Version 3.0.0 uses Kosmi's own GraphQL API, which is:
|
||||||
|
- ✅ More reliable (API is stable)
|
||||||
|
- ✅ Faster (no DOM traversal needed)
|
||||||
|
- ✅ More accurate (username is always correct)
|
||||||
|
- ✅ Less CPU intensive (no constant DOM monitoring)
|
||||||
|
|
||||||
|
## Vote Format
|
||||||
|
|
||||||
|
**Input** (Kosmi chat):
|
||||||
|
```
|
||||||
|
Alice: This is great! thisgame++
|
||||||
|
Bob: thisgame-- not my favorite
|
||||||
|
Charlie: THISGAME++ love it!
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output** (JSON export):
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"username": "Alice",
|
||||||
|
"message": "This is great! thisgame++",
|
||||||
|
"timestamp": "2025-10-30T20:15:00.123Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Bob",
|
||||||
|
"message": "thisgame-- not my favorite",
|
||||||
|
"timestamp": "2025-10-30T20:15:15.456Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "Charlie",
|
||||||
|
"message": "THISGAME++ love it!",
|
||||||
|
"timestamp": "2025-10-30T20:15:30.789Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Technical Details
|
||||||
|
|
||||||
|
### Files
|
||||||
|
|
||||||
|
- `manifest.json`: Extension configuration
|
||||||
|
- `content.js`: Monitors Kosmi chat and records votes
|
||||||
|
- `popup.html`: Extension popup interface
|
||||||
|
- `popup.js`: Popup logic and controls
|
||||||
|
- `popup.css`: Popup styling
|
||||||
|
- `icons/`: Extension icons
|
||||||
|
|
||||||
|
### Permissions
|
||||||
|
|
||||||
|
- `storage`: Save votes locally
|
||||||
|
- `activeTab`: Access current Kosmi tab
|
||||||
|
- `downloads`: Export JSON files
|
||||||
|
- `https://app.kosmi.io/*`: Access Kosmi chat
|
||||||
|
|
||||||
|
### Data Storage
|
||||||
|
|
||||||
|
- Votes are stored locally using Chrome's `storage.local` API
|
||||||
|
- Data persists across browser sessions
|
||||||
|
- No data is sent to external servers
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
### Version 3.0.1 (Current)
|
||||||
|
|
||||||
|
- 🐛 Fixed infinite retry loop when Apollo Client not found
|
||||||
|
- 🐛 Disabled auto-start on page load (user must manually start tracking)
|
||||||
|
- ✅ Added retry limit (10 attempts max, then gives up gracefully)
|
||||||
|
- ✅ Only sets `isTracking = true` after successfully hooking into Apollo Client
|
||||||
|
- ✅ Better error messages with attempt counter
|
||||||
|
|
||||||
|
### Version 3.0.0
|
||||||
|
|
||||||
|
- 🎉 **Complete rewrite using GraphQL WebSocket API**
|
||||||
|
- ✅ Hooks directly into Kosmi's Apollo Client
|
||||||
|
- ✅ 100% reliable message and username extraction
|
||||||
|
- ✅ No more DOM parsing or MutationObserver
|
||||||
|
- ✅ Lightweight and performant
|
||||||
|
- ✅ Future-proof (API-based, not DOM-based)
|
||||||
|
- ❌ Removed fragile DOM traversal code
|
||||||
|
- ❌ No more "Unknown" usernames
|
||||||
|
|
||||||
|
### Version 2.0.2
|
||||||
|
|
||||||
|
- 🐛 Fixed sidebar responsiveness with polling updates
|
||||||
|
- 🐛 Fixed MutationObserver not detecting new messages
|
||||||
|
- 🐛 Suppressed "Could not establish connection" errors
|
||||||
|
- ✅ Added extensive debug logging for troubleshooting
|
||||||
|
- ✅ Observer now properly disconnects and reconnects on start
|
||||||
|
|
||||||
|
### Version 2.0.1
|
||||||
|
|
||||||
|
- 🐛 Fixed duplicate vote counting issue
|
||||||
|
- 🐛 Improved username extraction with multiple fallback strategies
|
||||||
|
- ✅ Re-added sidebar panel support
|
||||||
|
|
||||||
|
### Version 2.0.0
|
||||||
|
|
||||||
|
- ✅ Simplified to only track `thisgame++` and `thisgame--`
|
||||||
|
- ✅ Removed complex game alias system
|
||||||
|
- ✅ Captures full message context
|
||||||
|
- ✅ UTC timestamp recording
|
||||||
|
- ✅ New JSON export format
|
||||||
|
- ✅ Improved UI with vote preview
|
||||||
|
- ✅ Case-insensitive matching
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
**Extension not detecting votes:**
|
||||||
|
1. Make sure you're on a Kosmi room page (`app.kosmi.io/room/...`)
|
||||||
|
2. Click "Stop Tracking" then "Start Tracking" to reset
|
||||||
|
3. Check browser console for errors (F12 → Console tab)
|
||||||
|
|
||||||
|
**Votes not saving:**
|
||||||
|
1. Check Chrome's storage permissions
|
||||||
|
2. Try resetting votes and starting fresh
|
||||||
|
3. Ensure extension has permission to access Kosmi
|
||||||
|
|
||||||
|
**Export not working:**
|
||||||
|
1. Check Chrome's download permissions
|
||||||
|
2. Ensure popup blockers aren't blocking downloads
|
||||||
|
3. Try clicking "Export JSON" again
|
||||||
|
|
||||||
|
**Extension not loading:**
|
||||||
|
1. Make sure you've reloaded the extension after updating files
|
||||||
|
2. Go to `chrome://extensions/`, find "Jackbox Chat Tracker", and click the reload icon
|
||||||
|
3. Refresh the Kosmi room page after reloading the extension
|
||||||
|
|
||||||
|
**Apollo Client not found:**
|
||||||
|
1. The extension needs a moment after page load to find Apollo Client
|
||||||
|
2. Wait 2-3 seconds after the page loads, then click "Start Tracking"
|
||||||
|
3. Check console for "Apollo Client not found!" error
|
||||||
|
4. If error persists, Kosmi may have changed their client framework
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues or questions, please file an issue in the main repository.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Same as main Jackbox Game Picker project.
|
||||||
|
|
||||||
153
chrome-extension/content.js
Normal file
153
chrome-extension/content.js
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
// Jackbox Chat Tracker for Kosmi - Content Script v3.2.0
|
||||||
|
// This version intercepts the WebSocket connection directly
|
||||||
|
|
||||||
|
// Inject the hook script ASAP (before page JavaScript runs)
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.src = chrome.runtime.getURL('inject.js');
|
||||||
|
script.onload = function() {
|
||||||
|
this.remove();
|
||||||
|
};
|
||||||
|
(document.head || document.documentElement).appendChild(script);
|
||||||
|
|
||||||
|
let isTracking = false;
|
||||||
|
let votes = [];
|
||||||
|
let socketFound = false;
|
||||||
|
let socketUrl = null;
|
||||||
|
|
||||||
|
// Listen for messages from inject script (different JavaScript context!)
|
||||||
|
window.addEventListener('message', (event) => {
|
||||||
|
// Only accept messages from same origin
|
||||||
|
if (event.source !== window) return;
|
||||||
|
|
||||||
|
if (event.data.type === 'JACKBOX_TRACKER_SOCKET_FOUND') {
|
||||||
|
console.log('[Jackbox Chat Tracker] WebSocket connected');
|
||||||
|
socketFound = true;
|
||||||
|
socketUrl = event.data.url;
|
||||||
|
} else if (event.data.type === 'JACKBOX_TRACKER_WS_MESSAGE') {
|
||||||
|
// Message from WebSocket - only process if tracking
|
||||||
|
if (isTracking) {
|
||||||
|
handleGraphQLMessage(event.data.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize storage
|
||||||
|
chrome.storage.local.get(['isTracking', 'votes'], (result) => {
|
||||||
|
// Always default to NOT tracking on page load
|
||||||
|
isTracking = false;
|
||||||
|
votes = result.votes || [];
|
||||||
|
|
||||||
|
// Clear tracking state in storage (user must manually start)
|
||||||
|
chrome.storage.local.set({ isTracking: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for commands from popup
|
||||||
|
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||||
|
if (request.action === 'startTracking') {
|
||||||
|
startTracking();
|
||||||
|
sendResponse({ success: true });
|
||||||
|
} else if (request.action === 'stopTracking') {
|
||||||
|
stopTracking();
|
||||||
|
sendResponse({ success: true });
|
||||||
|
} else if (request.action === 'getVotes') {
|
||||||
|
sendResponse({ votes: votes });
|
||||||
|
} else if (request.action === 'resetVotes') {
|
||||||
|
votes = [];
|
||||||
|
chrome.storage.local.set({ votes: [] });
|
||||||
|
sendResponse({ success: true });
|
||||||
|
} else if (request.action === 'getStatus') {
|
||||||
|
sendResponse({ isTracking: isTracking, voteCount: votes.length });
|
||||||
|
}
|
||||||
|
|
||||||
|
return true; // Keep channel open for async response
|
||||||
|
});
|
||||||
|
|
||||||
|
let startTrackingAttempts = 0;
|
||||||
|
const MAX_TRACKING_ATTEMPTS = 10; // Try for max 10 seconds
|
||||||
|
|
||||||
|
function startTracking() {
|
||||||
|
try {
|
||||||
|
if (!socketFound) {
|
||||||
|
startTrackingAttempts++;
|
||||||
|
|
||||||
|
if (startTrackingAttempts >= MAX_TRACKING_ATTEMPTS) {
|
||||||
|
console.error('[Jackbox Chat Tracker] WebSocket not found. Please refresh the page and try again.');
|
||||||
|
startTrackingAttempts = 0;
|
||||||
|
notifyPopup();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(startTracking, 1000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Found the WebSocket!
|
||||||
|
startTrackingAttempts = 0;
|
||||||
|
isTracking = true;
|
||||||
|
chrome.storage.local.set({ isTracking: true });
|
||||||
|
|
||||||
|
console.log('[Jackbox Chat Tracker] Tracking started - ready to capture votes');
|
||||||
|
notifyPopup();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Jackbox Chat Tracker] Error starting tracking:', error);
|
||||||
|
startTrackingAttempts = 0;
|
||||||
|
notifyPopup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopTracking() {
|
||||||
|
isTracking = false;
|
||||||
|
startTrackingAttempts = 0;
|
||||||
|
chrome.storage.local.set({ isTracking: false });
|
||||||
|
|
||||||
|
console.log('[Jackbox Chat Tracker] Tracking stopped');
|
||||||
|
notifyPopup();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleGraphQLMessage(data) {
|
||||||
|
if (!isTracking) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if this is a newMessage event
|
||||||
|
if (data.type === 'next' &&
|
||||||
|
data.payload &&
|
||||||
|
data.payload.data &&
|
||||||
|
data.payload.data.newMessage) {
|
||||||
|
|
||||||
|
const message = data.payload.data.newMessage;
|
||||||
|
const body = message.body;
|
||||||
|
const username = message.user.displayName || message.user.username || 'Unknown';
|
||||||
|
const timestamp = new Date(message.time * 1000).toISOString(); // Convert UNIX timestamp to ISO
|
||||||
|
|
||||||
|
// Check if message contains thisgame++ or thisgame--
|
||||||
|
if (/thisgame\+\+/i.test(body) || /thisgame--/i.test(body)) {
|
||||||
|
console.log('[Jackbox Chat Tracker] Vote detected:', username);
|
||||||
|
|
||||||
|
const vote = {
|
||||||
|
username: username,
|
||||||
|
message: body,
|
||||||
|
timestamp: timestamp
|
||||||
|
};
|
||||||
|
|
||||||
|
votes.push(vote);
|
||||||
|
chrome.storage.local.set({ votes: votes });
|
||||||
|
|
||||||
|
notifyPopup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Jackbox Chat Tracker] Error processing message:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function notifyPopup() {
|
||||||
|
// Send update to popup if it's open
|
||||||
|
chrome.runtime.sendMessage({
|
||||||
|
action: 'votesUpdated',
|
||||||
|
votes: votes,
|
||||||
|
isTracking: isTracking
|
||||||
|
}).catch(() => {
|
||||||
|
// Popup is closed, ignore error
|
||||||
|
});
|
||||||
|
}
|
||||||
BIN
chrome-extension/icons/icon.png
Normal file
BIN
chrome-extension/icons/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
BIN
chrome-extension/icons/icon128.png
Normal file
BIN
chrome-extension/icons/icon128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
BIN
chrome-extension/icons/icon16.png
Normal file
BIN
chrome-extension/icons/icon16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 726 B |
BIN
chrome-extension/icons/icon48.png
Normal file
BIN
chrome-extension/icons/icon48.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.4 KiB |
92
chrome-extension/inject.js
Normal file
92
chrome-extension/inject.js
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
// Jackbox Chat Tracker - Injected Script v3.2.0
|
||||||
|
// This script intercepts Kosmi's WebSocket connection to capture chat messages
|
||||||
|
// NOTE: This runs in the PAGE CONTEXT, not extension context!
|
||||||
|
|
||||||
|
// Store original WebSocket constructor
|
||||||
|
const OriginalWebSocket = window.WebSocket;
|
||||||
|
|
||||||
|
// Hook WebSocket constructor to capture Kosmi's WebSocket
|
||||||
|
window.WebSocket = function(url, protocols) {
|
||||||
|
const socket = new OriginalWebSocket(url, protocols);
|
||||||
|
|
||||||
|
// Check if this is Kosmi's GraphQL WebSocket
|
||||||
|
if (url.includes('engine.kosmi.io') || url.includes('gql-ws')) {
|
||||||
|
console.log('[Jackbox Chat Tracker] WebSocket hook active');
|
||||||
|
|
||||||
|
// Notify content script that we found the socket
|
||||||
|
window.postMessage({
|
||||||
|
type: 'JACKBOX_TRACKER_SOCKET_FOUND',
|
||||||
|
url: url
|
||||||
|
}, '*');
|
||||||
|
|
||||||
|
// Method 1: Hook addEventListener (for event listeners added later)
|
||||||
|
const originalAddEventListener = socket.addEventListener.bind(socket);
|
||||||
|
socket.addEventListener = function(type, listener, options) {
|
||||||
|
if (type === 'message') {
|
||||||
|
const wrappedListener = function(event) {
|
||||||
|
// Forward to content script
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
window.postMessage({
|
||||||
|
type: 'JACKBOX_TRACKER_WS_MESSAGE',
|
||||||
|
data: data
|
||||||
|
}, '*');
|
||||||
|
} catch (e) {
|
||||||
|
// Not JSON, skip
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then forward to original listener
|
||||||
|
return listener.call(this, event);
|
||||||
|
};
|
||||||
|
return originalAddEventListener(type, wrappedListener, options);
|
||||||
|
}
|
||||||
|
return originalAddEventListener(type, listener, options);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Method 2: Intercept the onmessage property setter (for direct assignment)
|
||||||
|
let realOnMessage = null;
|
||||||
|
const descriptor = Object.getOwnPropertyDescriptor(WebSocket.prototype, 'onmessage');
|
||||||
|
|
||||||
|
Object.defineProperty(socket, 'onmessage', {
|
||||||
|
get: function() {
|
||||||
|
return realOnMessage;
|
||||||
|
},
|
||||||
|
set: function(handler) {
|
||||||
|
// Create wrapped handler
|
||||||
|
realOnMessage = function(event) {
|
||||||
|
// Forward to content script
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
window.postMessage({
|
||||||
|
type: 'JACKBOX_TRACKER_WS_MESSAGE',
|
||||||
|
data: data
|
||||||
|
}, '*');
|
||||||
|
} catch (e) {
|
||||||
|
// Not JSON, skip
|
||||||
|
}
|
||||||
|
|
||||||
|
// ALWAYS call original handler
|
||||||
|
if (handler) {
|
||||||
|
handler.call(socket, event);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Actually set it on the underlying WebSocket using the original descriptor
|
||||||
|
if (descriptor && descriptor.set) {
|
||||||
|
descriptor.set.call(socket, realOnMessage);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
configurable: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return socket;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Preserve the original constructor properties
|
||||||
|
window.WebSocket.prototype = OriginalWebSocket.prototype;
|
||||||
|
window.WebSocket.CONNECTING = OriginalWebSocket.CONNECTING;
|
||||||
|
window.WebSocket.OPEN = OriginalWebSocket.OPEN;
|
||||||
|
window.WebSocket.CLOSING = OriginalWebSocket.CLOSING;
|
||||||
|
window.WebSocket.CLOSED = OriginalWebSocket.CLOSED;
|
||||||
|
|
||||||
45
chrome-extension/manifest.json
Normal file
45
chrome-extension/manifest.json
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"manifest_version": 3,
|
||||||
|
"name": "Jackbox Chat Tracker for Kosmi",
|
||||||
|
"version": "3.2.0",
|
||||||
|
"description": "Track thisgame++ and thisgame-- votes from Kosmi chat for Jackbox games",
|
||||||
|
"permissions": [
|
||||||
|
"storage",
|
||||||
|
"activeTab",
|
||||||
|
"downloads",
|
||||||
|
"sidePanel"
|
||||||
|
],
|
||||||
|
"host_permissions": [
|
||||||
|
"https://app.kosmi.io/*"
|
||||||
|
],
|
||||||
|
"content_scripts": [
|
||||||
|
{
|
||||||
|
"matches": ["https://app.kosmi.io/room/*"],
|
||||||
|
"js": ["content.js"],
|
||||||
|
"run_at": "document_start"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"web_accessible_resources": [
|
||||||
|
{
|
||||||
|
"resources": ["inject.js"],
|
||||||
|
"matches": ["https://app.kosmi.io/*"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"action": {
|
||||||
|
"default_popup": "popup.html",
|
||||||
|
"default_icon": {
|
||||||
|
"16": "icons/icon16.png",
|
||||||
|
"48": "icons/icon48.png",
|
||||||
|
"128": "icons/icon128.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"side_panel": {
|
||||||
|
"default_path": "popup.html"
|
||||||
|
},
|
||||||
|
"icons": {
|
||||||
|
"16": "icons/icon16.png",
|
||||||
|
"48": "icons/icon48.png",
|
||||||
|
"128": "icons/icon128.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
211
chrome-extension/popup.css
Normal file
211
chrome-extension/popup.css
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
width: 450px;
|
||||||
|
min-height: 500px;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
header h1 {
|
||||||
|
font-size: 20px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header .subtitle {
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
padding: 15px;
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
section {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
section h2 {
|
||||||
|
font-size: 16px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tracking-status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding: 10px;
|
||||||
|
background: #f9f9f9;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #ccc;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator.active {
|
||||||
|
background: #4caf50;
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#statusText {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
flex: 1;
|
||||||
|
padding: 10px 15px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover:not(:disabled) {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover:not(:disabled) {
|
||||||
|
background: #5568d3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: #95a5a6;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover:not(:disabled) {
|
||||||
|
background: #7f8c8d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: #e74c3c;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:hover:not(:disabled) {
|
||||||
|
background: #c0392b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm {
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.votes-list {
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
color: #999;
|
||||||
|
padding: 20px;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-item {
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border-left: 3px solid #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-item.vote-positive {
|
||||||
|
background: rgba(76, 175, 80, 0.05);
|
||||||
|
border-left-color: #4caf50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-item.vote-negative {
|
||||||
|
background: rgba(231, 76, 60, 0.05);
|
||||||
|
border-left-color: #e74c3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-header strong {
|
||||||
|
color: #333;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-time {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote-message {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
47
chrome-extension/popup.html
Normal file
47
chrome-extension/popup.html
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Jackbox Chat Tracker</title>
|
||||||
|
<link rel="stylesheet" href="popup.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<h1>🎮 Jackbox Chat Tracker</h1>
|
||||||
|
<p class="subtitle">Track thisgame++ and thisgame-- votes</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<!-- Tracking Controls -->
|
||||||
|
<section class="controls">
|
||||||
|
<h2>Tracking</h2>
|
||||||
|
<div class="tracking-status">
|
||||||
|
<span class="status-indicator" id="statusIndicator"></span>
|
||||||
|
<span id="statusText">Not Tracking</span>
|
||||||
|
</div>
|
||||||
|
<div class="button-group">
|
||||||
|
<button id="startTracking" class="btn btn-primary">Start Tracking</button>
|
||||||
|
<button id="stopTracking" class="btn btn-secondary" disabled>Stop Tracking</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Votes Display -->
|
||||||
|
<section class="votes">
|
||||||
|
<h2>Recorded Votes (<span id="voteCount">0</span>)</h2>
|
||||||
|
<div id="votesDisplay" class="votes-list">
|
||||||
|
<p class="empty-state">No votes recorded yet. Start tracking to begin!</p>
|
||||||
|
</div>
|
||||||
|
<div class="button-group">
|
||||||
|
<button id="resetVotes" class="btn btn-sm btn-danger">Reset Votes</button>
|
||||||
|
<button id="exportVotes" class="btn btn-sm btn-primary">Export JSON</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="popup.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
195
chrome-extension/popup.js
Normal file
195
chrome-extension/popup.js
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
// Popup script for Jackbox Chat Tracker
|
||||||
|
console.log('Jackbox Chat Tracker: Popup loaded');
|
||||||
|
|
||||||
|
let isTracking = false;
|
||||||
|
let votes = [];
|
||||||
|
|
||||||
|
// DOM elements
|
||||||
|
const startBtn = document.getElementById('startTracking');
|
||||||
|
const stopBtn = document.getElementById('stopTracking');
|
||||||
|
const statusIndicator = document.getElementById('statusIndicator');
|
||||||
|
const statusText = document.getElementById('statusText');
|
||||||
|
const votesDisplay = document.getElementById('votesDisplay');
|
||||||
|
const voteCount = document.getElementById('voteCount');
|
||||||
|
const resetVotesBtn = document.getElementById('resetVotes');
|
||||||
|
const exportVotesBtn = document.getElementById('exportVotes');
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
loadState();
|
||||||
|
|
||||||
|
// Poll for updates every 2 seconds to keep sidebar in sync
|
||||||
|
setInterval(() => {
|
||||||
|
chrome.storage.local.get(['isTracking', 'votes'], (result) => {
|
||||||
|
const wasTracking = isTracking;
|
||||||
|
isTracking = result.isTracking || false;
|
||||||
|
votes = result.votes || [];
|
||||||
|
|
||||||
|
if (wasTracking !== isTracking) {
|
||||||
|
updateTrackingUI();
|
||||||
|
}
|
||||||
|
updateDisplay();
|
||||||
|
});
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
// Event listeners
|
||||||
|
startBtn.addEventListener('click', startTracking);
|
||||||
|
stopBtn.addEventListener('click', stopTracking);
|
||||||
|
resetVotesBtn.addEventListener('click', resetVotes);
|
||||||
|
exportVotesBtn.addEventListener('click', exportVotes);
|
||||||
|
|
||||||
|
// Listen for updates from content script
|
||||||
|
chrome.runtime.onMessage.addListener((message) => {
|
||||||
|
if (message.action === 'votesUpdated') {
|
||||||
|
votes = message.votes;
|
||||||
|
updateDisplay();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function loadState() {
|
||||||
|
chrome.storage.local.get(['isTracking', 'votes'], (result) => {
|
||||||
|
isTracking = result.isTracking || false;
|
||||||
|
votes = result.votes || [];
|
||||||
|
|
||||||
|
updateDisplay();
|
||||||
|
updateTrackingUI();
|
||||||
|
|
||||||
|
// Request current votes from content script
|
||||||
|
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
|
||||||
|
if (tabs[0]) {
|
||||||
|
chrome.tabs.sendMessage(tabs[0].id, { action: 'getVotes' }, (response) => {
|
||||||
|
if (response && response.votes) {
|
||||||
|
votes = response.votes;
|
||||||
|
updateDisplay();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function startTracking() {
|
||||||
|
isTracking = true;
|
||||||
|
chrome.storage.local.set({ isTracking });
|
||||||
|
|
||||||
|
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
|
||||||
|
if (tabs[0]) {
|
||||||
|
chrome.tabs.sendMessage(tabs[0].id, { action: 'startTracking' }, (response) => {
|
||||||
|
if (response && response.success) {
|
||||||
|
console.log('Tracking started');
|
||||||
|
updateTrackingUI();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopTracking() {
|
||||||
|
isTracking = false;
|
||||||
|
chrome.storage.local.set({ isTracking });
|
||||||
|
|
||||||
|
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
|
||||||
|
if (tabs[0]) {
|
||||||
|
chrome.tabs.sendMessage(tabs[0].id, { action: 'stopTracking' }, (response) => {
|
||||||
|
if (response && response.success) {
|
||||||
|
console.log('Tracking stopped');
|
||||||
|
updateTrackingUI();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetVotes() {
|
||||||
|
if (!confirm('Reset all recorded votes? This cannot be undone.')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
votes = [];
|
||||||
|
chrome.storage.local.set({ votes });
|
||||||
|
|
||||||
|
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
|
||||||
|
if (tabs[0]) {
|
||||||
|
chrome.tabs.sendMessage(tabs[0].id, { action: 'resetVotes' }, (response) => {
|
||||||
|
if (response && response.success) {
|
||||||
|
console.log('Votes reset');
|
||||||
|
updateDisplay();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportVotes() {
|
||||||
|
// Export votes in the new format
|
||||||
|
const exportData = votes.map(vote => ({
|
||||||
|
username: vote.username,
|
||||||
|
message: vote.message,
|
||||||
|
timestamp: vote.timestamp
|
||||||
|
}));
|
||||||
|
|
||||||
|
const dataStr = JSON.stringify(exportData, null, 2);
|
||||||
|
const dataBlob = new Blob([dataStr], { type: 'application/json' });
|
||||||
|
const url = URL.createObjectURL(dataBlob);
|
||||||
|
|
||||||
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||||
|
const filename = `jackbox-votes-${timestamp}.json`;
|
||||||
|
|
||||||
|
chrome.downloads.download({
|
||||||
|
url: url,
|
||||||
|
filename: filename,
|
||||||
|
saveAs: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTrackingUI() {
|
||||||
|
if (isTracking) {
|
||||||
|
statusIndicator.classList.add('active');
|
||||||
|
statusText.textContent = 'Tracking Active';
|
||||||
|
startBtn.disabled = true;
|
||||||
|
stopBtn.disabled = false;
|
||||||
|
} else {
|
||||||
|
statusIndicator.classList.remove('active');
|
||||||
|
statusText.textContent = 'Not Tracking';
|
||||||
|
startBtn.disabled = false;
|
||||||
|
stopBtn.disabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateDisplay() {
|
||||||
|
voteCount.textContent = votes.length;
|
||||||
|
|
||||||
|
if (votes.length === 0) {
|
||||||
|
votesDisplay.innerHTML = '<p class="empty-state">No votes recorded yet. Start tracking to begin!</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display votes in reverse chronological order (most recent first)
|
||||||
|
const sortedVotes = [...votes].reverse();
|
||||||
|
|
||||||
|
let html = '<div class="vote-list">';
|
||||||
|
sortedVotes.forEach((vote, index) => {
|
||||||
|
const localTime = new Date(vote.timestamp).toLocaleString();
|
||||||
|
const isPositive = vote.message.includes('++');
|
||||||
|
const voteClass = isPositive ? 'vote-positive' : 'vote-negative';
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<div class="vote-item ${voteClass}">
|
||||||
|
<div class="vote-header">
|
||||||
|
<strong>${escapeHtml(vote.username)}</strong>
|
||||||
|
<span class="vote-time">${localTime}</span>
|
||||||
|
</div>
|
||||||
|
<div class="vote-message">${escapeHtml(vote.message)}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
votesDisplay.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(text) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = text;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -56,6 +56,29 @@ function History() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleExport = async (sessionId, format) => {
|
||||||
|
try {
|
||||||
|
const response = await api.get(`/sessions/${sessionId}/export?format=${format}`, {
|
||||||
|
responseType: 'blob'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create download link
|
||||||
|
const url = window.URL.createObjectURL(new Blob([response.data]));
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.setAttribute('download', `session-${sessionId}.${format === 'json' ? 'json' : 'txt'}`);
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
link.parentNode.removeChild(link);
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
|
||||||
|
success(`Session exported as ${format.toUpperCase()}`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to export session', err);
|
||||||
|
error('Failed to export session');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const loadSessionGames = async (sessionId, silent = false) => {
|
const loadSessionGames = async (sessionId, silent = false) => {
|
||||||
try {
|
try {
|
||||||
const response = await api.get(`/sessions/${sessionId}/games`);
|
const response = await api.get(`/sessions/${sessionId}/games`);
|
||||||
@@ -227,14 +250,32 @@ function History() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isAuthenticated && sessions.find(s => s.id === selectedSession)?.is_active === 1 && (
|
<div className="flex flex-col sm:flex-row gap-2">
|
||||||
<button
|
{isAuthenticated && sessions.find(s => s.id === selectedSession)?.is_active === 1 && (
|
||||||
onClick={() => setShowChatImport(true)}
|
<button
|
||||||
className="bg-indigo-600 dark:bg-indigo-700 text-white px-4 py-2 rounded-lg hover:bg-indigo-700 dark:hover:bg-indigo-800 transition text-sm sm:text-base w-full sm:w-auto"
|
onClick={() => setShowChatImport(true)}
|
||||||
>
|
className="bg-indigo-600 dark:bg-indigo-700 text-white px-4 py-2 rounded-lg hover:bg-indigo-700 dark:hover:bg-indigo-800 transition text-sm sm:text-base w-full sm:w-auto"
|
||||||
Import Chat Log
|
>
|
||||||
</button>
|
Import Chat Log
|
||||||
)}
|
</button>
|
||||||
|
)}
|
||||||
|
{isAuthenticated && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
onClick={() => handleExport(selectedSession, 'txt')}
|
||||||
|
className="bg-gray-600 dark:bg-gray-700 text-white px-4 py-2 rounded-lg hover:bg-gray-700 dark:hover:bg-gray-800 transition text-sm sm:text-base w-full sm:w-auto"
|
||||||
|
>
|
||||||
|
Export as Text
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleExport(selectedSession, 'json')}
|
||||||
|
className="bg-gray-600 dark:bg-gray-700 text-white px-4 py-2 rounded-lg hover:bg-gray-700 dark:hover:bg-gray-800 transition text-sm sm:text-base w-full sm:w-auto"
|
||||||
|
>
|
||||||
|
Export as JSON
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showChatImport && (
|
{showChatImport && (
|
||||||
|
|||||||
12
todos.md
12
todos.md
@@ -1,11 +1,11 @@
|
|||||||
# TODO:
|
# TODO:
|
||||||
|
|
||||||
## Chrome Extension
|
## Chrome Extension
|
||||||
- [ ] /.old-chrome-extension/ contains OLD code that needs adjusting for new game picker format.
|
- [x] /.old-chrome-extension/ contains OLD code that needs adjusting for new game picker format. (COMPLETED: New simplified extension in /chrome-extension/)
|
||||||
- [ ] remove clunky gamealias system, we are only tracking "thisgame++" and "thisgame--" now.
|
- [x] remove clunky gamealias system, we are only tracking "thisgame++" and "thisgame--" now.
|
||||||
- [ ] ensure the extension is watching for "thisgame++" or "thisgame--" anywhere in each chat line.
|
- [x] ensure the extension is watching for "thisgame++" or "thisgame--" anywhere in each chat line.
|
||||||
- [ ] if a chat line matches capture the whole message/line, the author, and the timestamp (UTC).
|
- [x] if a chat line matches capture the whole message/line, the author, and the timestamp (UTC).
|
||||||
- [ ] ensure our JSON output matches the new format:
|
- [x] ensure our JSON output matches the new format:
|
||||||
```json
|
```json
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
@@ -31,4 +31,4 @@
|
|||||||
- [x] Entire App: local timezone display still isn't working. I see UTC times. (FIXED: Created dateUtils.js to properly parse SQLite UTC timestamps)
|
- [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
|
## Other Features
|
||||||
- [ ] Session History: export sessions to plaintext and JSON.
|
- [x] Session History: export sessions to plaintext and JSON. (COMPLETED: Export buttons in History page)
|
||||||
Reference in New Issue
Block a user