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;
|
||||
|
||||
|
||||
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) => {
|
||||
try {
|
||||
const response = await api.get(`/sessions/${sessionId}/games`);
|
||||
@@ -227,14 +250,32 @@ function History() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isAuthenticated && sessions.find(s => s.id === selectedSession)?.is_active === 1 && (
|
||||
<button
|
||||
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>
|
||||
)}
|
||||
<div className="flex flex-col sm:flex-row gap-2">
|
||||
{isAuthenticated && sessions.find(s => s.id === selectedSession)?.is_active === 1 && (
|
||||
<button
|
||||
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>
|
||||
)}
|
||||
{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>
|
||||
|
||||
{showChatImport && (
|
||||
|
||||
12
todos.md
12
todos.md
@@ -1,11 +1,11 @@
|
||||
# TODO:
|
||||
|
||||
## Chrome Extension
|
||||
- [ ] /.old-chrome-extension/ contains OLD code that needs adjusting for new game picker format.
|
||||
- [ ] 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.
|
||||
- [ ] 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] /.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
|
||||
[
|
||||
{
|
||||
@@ -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)
|
||||
|
||||
## 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