chrome ext, enhancements
- SHA
a311f1334058fdfb635ccc310fb7347a4c6058ec- Parents
-
1e4e26d - Tree
2311a6e
a311f13
a311f1334058fdfb635ccc310fb7347a4c6058ec1e4e26d
2311a6e| Status | File | + | - |
|---|---|---|---|
| A |
dashboard/dashboard.js
|
825 | 0 |
| A |
dashboard/index.html
|
243 | 0 |
| A |
dashboard/styles.css
|
673 | 0 |
| A |
extension-chrome/background/pattern-learning.js
|
505 | 0 |
| A |
extension-chrome/background/screenshot-manager.js
|
408 | 0 |
| A |
extension-chrome/background/service-worker.js
|
393 | 0 |
| A |
extension-chrome/background/unified-handler-v3.js
|
293 | 0 |
| A |
extension-chrome/manifest.json
|
80 | 0 |
dashboard/dashboard.jsadded@@ -0,0 +1,825 @@ | ||
| 1 | +// LooseCannon Analytics Dashboard JavaScript | |
| 2 | +// Real-time monitoring and control interface | |
| 3 | + | |
| 4 | +class LooseCannonDashboard { | |
| 5 | + constructor() { | |
| 6 | + this.serverUrl = 'http://localhost:8765'; | |
| 7 | + this.ws = null; | |
| 8 | + this.charts = {}; | |
| 9 | + this.updateInterval = null; | |
| 10 | + this.startTime = Date.now(); | |
| 11 | + this.data = { | |
| 12 | + messages: [], | |
| 13 | + scammers: [], | |
| 14 | + conversations: new Map(), | |
| 15 | + patterns: [], | |
| 16 | + screenshots: [] | |
| 17 | + }; | |
| 18 | + this.init(); | |
| 19 | + } | |
| 20 | + | |
| 21 | + async init() { | |
| 22 | + await this.checkServerConnection(); | |
| 23 | + this.initializeCharts(); | |
| 24 | + this.setupEventListeners(); | |
| 25 | + this.connectWebSocket(); | |
| 26 | + this.startRealtimeUpdates(); | |
| 27 | + this.loadInitialData(); | |
| 28 | + } | |
| 29 | + | |
| 30 | + async checkServerConnection() { | |
| 31 | + const statusElement = document.getElementById('serverStatus'); | |
| 32 | + | |
| 33 | + try { | |
| 34 | + const response = await fetch(`${this.serverUrl}/status`); | |
| 35 | + if (response.ok) { | |
| 36 | + statusElement.classList.add('connected'); | |
| 37 | + console.log('Server connected'); | |
| 38 | + } else { | |
| 39 | + throw new Error('Server not responding'); | |
| 40 | + } | |
| 41 | + } catch (error) { | |
| 42 | + console.error('Server connection failed:', error); | |
| 43 | + statusElement.classList.remove('connected'); | |
| 44 | + this.showNotification('Server offline - some features unavailable', 'error'); | |
| 45 | + } | |
| 46 | + } | |
| 47 | + | |
| 48 | + connectWebSocket() { | |
| 49 | + // WebSocket for real-time updates | |
| 50 | + this.ws = new WebSocket('ws://localhost:8766'); | |
| 51 | + | |
| 52 | + this.ws.onopen = () => { | |
| 53 | + console.log('WebSocket connected'); | |
| 54 | + this.showNotification('Connected to real-time feed', 'success'); | |
| 55 | + }; | |
| 56 | + | |
| 57 | + this.ws.onmessage = (event) => { | |
| 58 | + const data = JSON.parse(event.data); | |
| 59 | + this.handleRealtimeUpdate(data); | |
| 60 | + }; | |
| 61 | + | |
| 62 | + this.ws.onerror = (error) => { | |
| 63 | + console.error('WebSocket error:', error); | |
| 64 | + }; | |
| 65 | + | |
| 66 | + this.ws.onclose = () => { | |
| 67 | + console.log('WebSocket disconnected, reconnecting...'); | |
| 68 | + setTimeout(() => this.connectWebSocket(), 5000); | |
| 69 | + }; | |
| 70 | + } | |
| 71 | + | |
| 72 | + handleRealtimeUpdate(data) { | |
| 73 | + switch (data.type) { | |
| 74 | + case 'MESSAGE': | |
| 75 | + this.addActivityItem(data); | |
| 76 | + this.updateMessageStats(data); | |
| 77 | + break; | |
| 78 | + | |
| 79 | + case 'SCAMMER_DETECTED': | |
| 80 | + this.handleScammerDetection(data); | |
| 81 | + break; | |
| 82 | + | |
| 83 | + case 'SESSION_UPDATE': | |
| 84 | + this.updateActiveSessions(data); | |
| 85 | + break; | |
| 86 | + | |
| 87 | + case 'SCREENSHOT': | |
| 88 | + this.addScreenshot(data); | |
| 89 | + break; | |
| 90 | + | |
| 91 | + case 'PATTERN_LEARNED': | |
| 92 | + this.addPattern(data); | |
| 93 | + break; | |
| 94 | + } | |
| 95 | + | |
| 96 | + this.updateCharts(); | |
| 97 | + } | |
| 98 | + | |
| 99 | + initializeCharts() { | |
| 100 | + const chartOptions = { | |
| 101 | + responsive: true, | |
| 102 | + maintainAspectRatio: false, | |
| 103 | + plugins: { | |
| 104 | + legend: { | |
| 105 | + labels: { | |
| 106 | + color: '#e0e0e0' | |
| 107 | + } | |
| 108 | + } | |
| 109 | + }, | |
| 110 | + scales: { | |
| 111 | + y: { | |
| 112 | + grid: { | |
| 113 | + color: '#333' | |
| 114 | + }, | |
| 115 | + ticks: { | |
| 116 | + color: '#888' | |
| 117 | + } | |
| 118 | + }, | |
| 119 | + x: { | |
| 120 | + grid: { | |
| 121 | + color: '#333' | |
| 122 | + }, | |
| 123 | + ticks: { | |
| 124 | + color: '#888' | |
| 125 | + } | |
| 126 | + } | |
| 127 | + } | |
| 128 | + }; | |
| 129 | + | |
| 130 | + // Volume Chart | |
| 131 | + this.charts.volume = new Chart(document.getElementById('volumeChart'), { | |
| 132 | + type: 'line', | |
| 133 | + data: { | |
| 134 | + labels: this.generateTimeLabels(24), | |
| 135 | + datasets: [{ | |
| 136 | + label: 'Messages', | |
| 137 | + data: new Array(24).fill(0), | |
| 138 | + borderColor: '#667eea', | |
| 139 | + backgroundColor: 'rgba(102, 126, 234, 0.1)', | |
| 140 | + tension: 0.4 | |
| 141 | + }] | |
| 142 | + }, | |
| 143 | + options: chartOptions | |
| 144 | + }); | |
| 145 | + | |
| 146 | + // Detection Chart | |
| 147 | + this.charts.detection = new Chart(document.getElementById('detectionChart'), { | |
| 148 | + type: 'bar', | |
| 149 | + data: { | |
| 150 | + labels: ['Low', 'Medium', 'High', 'Critical'], | |
| 151 | + datasets: [{ | |
| 152 | + label: 'Detections', | |
| 153 | + data: [0, 0, 0, 0], | |
| 154 | + backgroundColor: ['#10b981', '#f59e0b', '#ef4444', '#7c3aed'] | |
| 155 | + }] | |
| 156 | + }, | |
| 157 | + options: chartOptions | |
| 158 | + }); | |
| 159 | + | |
| 160 | + // Platform Chart | |
| 161 | + this.charts.platform = new Chart(document.getElementById('platformChart'), { | |
| 162 | + type: 'doughnut', | |
| 163 | + data: { | |
| 164 | + labels: ['WhatsApp', 'Telegram', 'Messenger'], | |
| 165 | + datasets: [{ | |
| 166 | + data: [0, 0, 0], | |
| 167 | + backgroundColor: ['#25d366', '#0088cc', '#0084ff'] | |
| 168 | + }] | |
| 169 | + }, | |
| 170 | + options: { | |
| 171 | + ...chartOptions, | |
| 172 | + plugins: { | |
| 173 | + legend: { | |
| 174 | + position: 'bottom', | |
| 175 | + labels: { | |
| 176 | + color: '#e0e0e0' | |
| 177 | + } | |
| 178 | + } | |
| 179 | + } | |
| 180 | + } | |
| 181 | + }); | |
| 182 | + | |
| 183 | + // Response Time Chart | |
| 184 | + this.charts.response = new Chart(document.getElementById('responseChart'), { | |
| 185 | + type: 'line', | |
| 186 | + data: { | |
| 187 | + labels: this.generateTimeLabels(60, 'minutes'), | |
| 188 | + datasets: [{ | |
| 189 | + label: 'Response Time (ms)', | |
| 190 | + data: new Array(60).fill(0), | |
| 191 | + borderColor: '#3b82f6', | |
| 192 | + backgroundColor: 'rgba(59, 130, 246, 0.1)', | |
| 193 | + tension: 0.4 | |
| 194 | + }] | |
| 195 | + }, | |
| 196 | + options: chartOptions | |
| 197 | + }); | |
| 198 | + } | |
| 199 | + | |
| 200 | + generateTimeLabels(count, unit = 'hours') { | |
| 201 | + const labels = []; | |
| 202 | + for (let i = count - 1; i >= 0; i--) { | |
| 203 | + if (unit === 'hours') { | |
| 204 | + labels.push(`${i}h`); | |
| 205 | + } else { | |
| 206 | + labels.push(`${i}m`); | |
| 207 | + } | |
| 208 | + } | |
| 209 | + return labels; | |
| 210 | + } | |
| 211 | + | |
| 212 | + updateCharts() { | |
| 213 | + // Update volume chart with message counts | |
| 214 | + const volumeData = this.calculateVolumeData(); | |
| 215 | + this.charts.volume.data.datasets[0].data = volumeData; | |
| 216 | + this.charts.volume.update(); | |
| 217 | + | |
| 218 | + // Update detection chart | |
| 219 | + const detectionData = this.calculateDetectionData(); | |
| 220 | + this.charts.detection.data.datasets[0].data = detectionData; | |
| 221 | + this.charts.detection.update(); | |
| 222 | + | |
| 223 | + // Update platform distribution | |
| 224 | + const platformData = this.calculatePlatformData(); | |
| 225 | + this.charts.platform.data.datasets[0].data = platformData; | |
| 226 | + this.charts.platform.update(); | |
| 227 | + | |
| 228 | + // Update response times | |
| 229 | + const responseData = this.calculateResponseData(); | |
| 230 | + this.charts.response.data.datasets[0].data = responseData; | |
| 231 | + this.charts.response.update(); | |
| 232 | + } | |
| 233 | + | |
| 234 | + calculateVolumeData() { | |
| 235 | + // Group messages by hour | |
| 236 | + const hourCounts = new Array(24).fill(0); | |
| 237 | + const now = Date.now(); | |
| 238 | + | |
| 239 | + this.data.messages.forEach(msg => { | |
| 240 | + const age = (now - msg.timestamp) / (1000 * 60 * 60); | |
| 241 | + if (age < 24) { | |
| 242 | + const hour = Math.floor(age); | |
| 243 | + hourCounts[23 - hour]++; | |
| 244 | + } | |
| 245 | + }); | |
| 246 | + | |
| 247 | + return hourCounts; | |
| 248 | + } | |
| 249 | + | |
| 250 | + calculateDetectionData() { | |
| 251 | + const counts = [0, 0, 0, 0]; // Low, Medium, High, Critical | |
| 252 | + | |
| 253 | + this.data.scammers.forEach(scammer => { | |
| 254 | + if (scammer.score < 0.3) counts[0]++; | |
| 255 | + else if (scammer.score < 0.6) counts[1]++; | |
| 256 | + else if (scammer.score < 0.9) counts[2]++; | |
| 257 | + else counts[3]++; | |
| 258 | + }); | |
| 259 | + | |
| 260 | + return counts; | |
| 261 | + } | |
| 262 | + | |
| 263 | + calculatePlatformData() { | |
| 264 | + const counts = { whatsapp: 0, telegram: 0, messenger: 0 }; | |
| 265 | + | |
| 266 | + this.data.conversations.forEach(conv => { | |
| 267 | + counts[conv.platform] = (counts[conv.platform] || 0) + 1; | |
| 268 | + }); | |
| 269 | + | |
| 270 | + return [counts.whatsapp, counts.telegram, counts.messenger]; | |
| 271 | + } | |
| 272 | + | |
| 273 | + calculateResponseData() { | |
| 274 | + // Return last 60 response time measurements | |
| 275 | + return this.data.messages | |
| 276 | + .slice(-60) | |
| 277 | + .map(msg => msg.responseTime || 0); | |
| 278 | + } | |
| 279 | + | |
| 280 | + addActivityItem(data) { | |
| 281 | + const feed = document.getElementById('activityFeed'); | |
| 282 | + const item = document.createElement('div'); | |
| 283 | + item.className = 'activity-item'; | |
| 284 | + | |
| 285 | + if (data.scammerScore > 0.7) { | |
| 286 | + item.classList.add('scammer'); | |
| 287 | + } | |
| 288 | + | |
| 289 | + const time = new Date(data.timestamp).toLocaleTimeString(); | |
| 290 | + | |
| 291 | + item.innerHTML = ` | |
| 292 | + <span class="activity-time">${time}</span> | |
| 293 | + <span class="activity-message">${data.message || 'New activity'}</span> | |
| 294 | + `; | |
| 295 | + | |
| 296 | + feed.insertBefore(item, feed.firstChild); | |
| 297 | + | |
| 298 | + // Keep only last 50 items | |
| 299 | + while (feed.children.length > 50) { | |
| 300 | + feed.removeChild(feed.lastChild); | |
| 301 | + } | |
| 302 | + } | |
| 303 | + | |
| 304 | + updateMessageStats(data) { | |
| 305 | + this.data.messages.push(data); | |
| 306 | + | |
| 307 | + // Update counters | |
| 308 | + document.getElementById('totalMessages').textContent = this.data.messages.length; | |
| 309 | + | |
| 310 | + // Calculate and show trend | |
| 311 | + const trend = this.calculateTrend('messages'); | |
| 312 | + const trendElement = document.getElementById('messageTrend'); | |
| 313 | + trendElement.textContent = `${trend > 0 ? '+' : ''}${trend}%`; | |
| 314 | + trendElement.className = trend >= 0 ? 'stat-trend' : 'stat-trend negative'; | |
| 315 | + } | |
| 316 | + | |
| 317 | + handleScammerDetection(data) { | |
| 318 | + this.data.scammers.push(data); | |
| 319 | + | |
| 320 | + // Update counter | |
| 321 | + document.getElementById('scammersDetected').textContent = this.data.scammers.length; | |
| 322 | + | |
| 323 | + // Add special notification | |
| 324 | + this.showNotification(`Scammer detected! Confidence: ${(data.score * 100).toFixed(0)}%`, 'warning'); | |
| 325 | + | |
| 326 | + // Flash the stat card | |
| 327 | + const card = document.querySelector('.stat-card.danger'); | |
| 328 | + card.style.animation = 'pulse 0.5s'; | |
| 329 | + setTimeout(() => { | |
| 330 | + card.style.animation = ''; | |
| 331 | + }, 500); | |
| 332 | + | |
| 333 | + // Add to activity feed | |
| 334 | + this.addActivityItem({ | |
| 335 | + ...data, | |
| 336 | + message: `Scammer detected on ${data.platform} (${(data.score * 100).toFixed(0)}% confidence)` | |
| 337 | + }); | |
| 338 | + } | |
| 339 | + | |
| 340 | + updateActiveSessions(data) { | |
| 341 | + document.getElementById('activeSessions').textContent = data.count || 0; | |
| 342 | + } | |
| 343 | + | |
| 344 | + addScreenshot(data) { | |
| 345 | + const grid = document.getElementById('evidenceGrid'); | |
| 346 | + | |
| 347 | + // Remove placeholder if exists | |
| 348 | + const placeholder = grid.querySelector('.evidence-placeholder'); | |
| 349 | + if (placeholder) { | |
| 350 | + placeholder.remove(); | |
| 351 | + } | |
| 352 | + | |
| 353 | + const item = document.createElement('div'); | |
| 354 | + item.className = 'evidence-item'; | |
| 355 | + item.innerHTML = ` | |
| 356 | + <img src="${data.thumbnail}" alt="Evidence"> | |
| 357 | + <div class="evidence-meta"> | |
| 358 | + ${data.platform} - ${new Date(data.timestamp).toLocaleString()} | |
| 359 | + </div> | |
| 360 | + `; | |
| 361 | + | |
| 362 | + grid.insertBefore(item, grid.firstChild); | |
| 363 | + | |
| 364 | + // Keep only last 12 screenshots | |
| 365 | + while (grid.children.length > 12) { | |
| 366 | + grid.removeChild(grid.lastChild); | |
| 367 | + } | |
| 368 | + | |
| 369 | + this.data.screenshots.push(data); | |
| 370 | + } | |
| 371 | + | |
| 372 | + addPattern(data) { | |
| 373 | + this.data.patterns.push(data); | |
| 374 | + | |
| 375 | + // Update pattern stats | |
| 376 | + document.getElementById('totalPatterns').textContent = this.data.patterns.length; | |
| 377 | + document.getElementById('patternAccuracy').textContent = | |
| 378 | + `${(this.calculatePatternAccuracy() * 100).toFixed(0)}%`; | |
| 379 | + | |
| 380 | + // Update patterns list | |
| 381 | + const list = document.getElementById('patternsList'); | |
| 382 | + const item = document.createElement('div'); | |
| 383 | + item.className = 'pattern-item'; | |
| 384 | + item.innerHTML = ` | |
| 385 | + <span class="pattern-type">${data.type}</span> | |
| 386 | + <span class="pattern-count">${data.occurrences} occurrences</span> | |
| 387 | + `; | |
| 388 | + | |
| 389 | + list.insertBefore(item, list.firstChild); | |
| 390 | + | |
| 391 | + // Keep only last 10 patterns | |
| 392 | + while (list.children.length > 10) { | |
| 393 | + list.removeChild(list.lastChild); | |
| 394 | + } | |
| 395 | + } | |
| 396 | + | |
| 397 | + calculatePatternAccuracy() { | |
| 398 | + if (this.data.patterns.length === 0) return 0; | |
| 399 | + | |
| 400 | + const correct = this.data.patterns.filter(p => p.verified).length; | |
| 401 | + return correct / this.data.patterns.length; | |
| 402 | + } | |
| 403 | + | |
| 404 | + calculateTrend(metric) { | |
| 405 | + // Simple trend calculation (current hour vs previous hour) | |
| 406 | + const now = Date.now(); | |
| 407 | + const hourAgo = now - (60 * 60 * 1000); | |
| 408 | + | |
| 409 | + let current = 0; | |
| 410 | + let previous = 0; | |
| 411 | + | |
| 412 | + if (metric === 'messages') { | |
| 413 | + this.data.messages.forEach(msg => { | |
| 414 | + if (msg.timestamp > hourAgo) current++; | |
| 415 | + else if (msg.timestamp > (hourAgo - 60 * 60 * 1000)) previous++; | |
| 416 | + }); | |
| 417 | + } | |
| 418 | + | |
| 419 | + if (previous === 0) return current > 0 ? 100 : 0; | |
| 420 | + return Math.round(((current - previous) / previous) * 100); | |
| 421 | + } | |
| 422 | + | |
| 423 | + setupEventListeners() { | |
| 424 | + // Export button | |
| 425 | + document.getElementById('exportBtn').addEventListener('click', () => { | |
| 426 | + this.exportData(); | |
| 427 | + }); | |
| 428 | + | |
| 429 | + // Emergency stop | |
| 430 | + document.getElementById('emergencyStop').addEventListener('click', () => { | |
| 431 | + this.emergencyStop(); | |
| 432 | + }); | |
| 433 | + | |
| 434 | + // Clear data | |
| 435 | + document.getElementById('clearDataBtn').addEventListener('click', () => { | |
| 436 | + this.clearOldData(); | |
| 437 | + }); | |
| 438 | + | |
| 439 | + // Sync patterns | |
| 440 | + document.getElementById('syncBtn').addEventListener('click', () => { | |
| 441 | + this.syncPatterns(); | |
| 442 | + }); | |
| 443 | + | |
| 444 | + // Personality builder | |
| 445 | + document.getElementById('personalityBtn').addEventListener('click', () => { | |
| 446 | + this.openPersonalityBuilder(); | |
| 447 | + }); | |
| 448 | + | |
| 449 | + // Conversation replay | |
| 450 | + document.getElementById('replayBtn').addEventListener('click', () => { | |
| 451 | + this.openConversationReplay(); | |
| 452 | + }); | |
| 453 | + | |
| 454 | + // Modal controls | |
| 455 | + document.getElementById('modalClose').addEventListener('click', () => { | |
| 456 | + document.getElementById('personalityModal').classList.remove('active'); | |
| 457 | + }); | |
| 458 | + | |
| 459 | + document.getElementById('replayClose').addEventListener('click', () => { | |
| 460 | + document.getElementById('replayModal').classList.remove('active'); | |
| 461 | + }); | |
| 462 | + | |
| 463 | + // Personality form | |
| 464 | + document.getElementById('personalityForm').addEventListener('submit', (e) => { | |
| 465 | + e.preventDefault(); | |
| 466 | + this.savePersonality(); | |
| 467 | + }); | |
| 468 | + | |
| 469 | + // Test personality | |
| 470 | + document.getElementById('testPersonality').addEventListener('click', () => { | |
| 471 | + this.testPersonality(); | |
| 472 | + }); | |
| 473 | + | |
| 474 | + // Temperature slider | |
| 475 | + document.getElementById('temperature').addEventListener('input', (e) => { | |
| 476 | + document.getElementById('tempValue').textContent = e.target.value; | |
| 477 | + }); | |
| 478 | + } | |
| 479 | + | |
| 480 | + async exportData() { | |
| 481 | + try { | |
| 482 | + const data = { | |
| 483 | + timestamp: new Date().toISOString(), | |
| 484 | + messages: this.data.messages, | |
| 485 | + scammers: this.data.scammers, | |
| 486 | + patterns: this.data.patterns, | |
| 487 | + screenshots: this.data.screenshots.map(s => s.id), | |
| 488 | + conversations: Array.from(this.data.conversations.values()) | |
| 489 | + }; | |
| 490 | + | |
| 491 | + const blob = new Blob([JSON.stringify(data, null, 2)], { | |
| 492 | + type: 'application/json' | |
| 493 | + }); | |
| 494 | + | |
| 495 | + const url = URL.createObjectURL(blob); | |
| 496 | + const a = document.createElement('a'); | |
| 497 | + a.href = url; | |
| 498 | + a.download = `loosecannon-export-${Date.now()}.json`; | |
| 499 | + a.click(); | |
| 500 | + | |
| 501 | + this.showNotification('Data exported successfully', 'success'); | |
| 502 | + } catch (error) { | |
| 503 | + console.error('Export error:', error); | |
| 504 | + this.showNotification('Export failed', 'error'); | |
| 505 | + } | |
| 506 | + } | |
| 507 | + | |
| 508 | + async emergencyStop() { | |
| 509 | + if (!confirm('This will stop ALL active LooseCannon sessions. Continue?')) { | |
| 510 | + return; | |
| 511 | + } | |
| 512 | + | |
| 513 | + try { | |
| 514 | + const response = await fetch(`${this.serverUrl}/emergency-stop`, { | |
| 515 | + method: 'POST' | |
| 516 | + }); | |
| 517 | + | |
| 518 | + if (response.ok) { | |
| 519 | + this.showNotification('Emergency stop activated', 'warning'); | |
| 520 | + | |
| 521 | + // Clear active sessions display | |
| 522 | + document.getElementById('activeSessions').textContent = '0'; | |
| 523 | + | |
| 524 | + // Send stop command via WebSocket | |
| 525 | + if (this.ws && this.ws.readyState === WebSocket.OPEN) { | |
| 526 | + this.ws.send(JSON.stringify({ type: 'EMERGENCY_STOP' })); | |
| 527 | + } | |
| 528 | + } | |
| 529 | + } catch (error) { | |
| 530 | + console.error('Emergency stop error:', error); | |
| 531 | + } | |
| 532 | + } | |
| 533 | + | |
| 534 | + async clearOldData() { | |
| 535 | + if (!confirm('Clear data older than 24 hours?')) { | |
| 536 | + return; | |
| 537 | + } | |
| 538 | + | |
| 539 | + const cutoff = Date.now() - (24 * 60 * 60 * 1000); | |
| 540 | + | |
| 541 | + // Filter data | |
| 542 | + this.data.messages = this.data.messages.filter(m => m.timestamp > cutoff); | |
| 543 | + this.data.scammers = this.data.scammers.filter(s => s.timestamp > cutoff); | |
| 544 | + this.data.screenshots = this.data.screenshots.filter(s => s.timestamp > cutoff); | |
| 545 | + | |
| 546 | + // Update displays | |
| 547 | + this.updateCharts(); | |
| 548 | + this.showNotification('Old data cleared', 'success'); | |
| 549 | + } | |
| 550 | + | |
| 551 | + async syncPatterns() { | |
| 552 | + try { | |
| 553 | + const response = await fetch(`${this.serverUrl}/patterns/sync`, { | |
| 554 | + method: 'GET' | |
| 555 | + }); | |
| 556 | + | |
| 557 | + if (response.ok) { | |
| 558 | + const patterns = await response.json(); | |
| 559 | + | |
| 560 | + // Merge with local patterns | |
| 561 | + patterns.forEach(pattern => { | |
| 562 | + if (!this.data.patterns.find(p => p.id === pattern.id)) { | |
| 563 | + this.addPattern(pattern); | |
| 564 | + } | |
| 565 | + }); | |
| 566 | + | |
| 567 | + this.showNotification(`Synced ${patterns.length} patterns`, 'success'); | |
| 568 | + } | |
| 569 | + } catch (error) { | |
| 570 | + console.error('Pattern sync error:', error); | |
| 571 | + this.showNotification('Pattern sync failed', 'error'); | |
| 572 | + } | |
| 573 | + } | |
| 574 | + | |
| 575 | + openPersonalityBuilder() { | |
| 576 | + document.getElementById('personalityModal').classList.add('active'); | |
| 577 | + } | |
| 578 | + | |
| 579 | + async savePersonality() { | |
| 580 | + const personality = { | |
| 581 | + id: document.getElementById('personalityName').value.toLowerCase().replace(/\s+/g, '-'), | |
| 582 | + name: document.getElementById('personalityName').value, | |
| 583 | + systemPrompt: document.getElementById('systemPrompt').value, | |
| 584 | + temperature: parseFloat(document.getElementById('temperature').value), | |
| 585 | + examples: document.getElementById('examples').value.split('\n').filter(e => e.trim()) | |
| 586 | + }; | |
| 587 | + | |
| 588 | + try { | |
| 589 | + const response = await fetch(`${this.serverUrl}/personalities`, { | |
| 590 | + method: 'POST', | |
| 591 | + headers: { 'Content-Type': 'application/json' }, | |
| 592 | + body: JSON.stringify(personality) | |
| 593 | + }); | |
| 594 | + | |
| 595 | + if (response.ok) { | |
| 596 | + this.showNotification('Personality saved successfully', 'success'); | |
| 597 | + document.getElementById('personalityModal').classList.remove('active'); | |
| 598 | + document.getElementById('personalityForm').reset(); | |
| 599 | + } | |
| 600 | + } catch (error) { | |
| 601 | + console.error('Save personality error:', error); | |
| 602 | + this.showNotification('Failed to save personality', 'error'); | |
| 603 | + } | |
| 604 | + } | |
| 605 | + | |
| 606 | + async testPersonality() { | |
| 607 | + const testPrompt = "Hello, I have an important message about your account."; | |
| 608 | + const systemPrompt = document.getElementById('systemPrompt').value; | |
| 609 | + const temperature = parseFloat(document.getElementById('temperature').value); | |
| 610 | + | |
| 611 | + try { | |
| 612 | + const response = await fetch(`${this.serverUrl}/test-personality`, { | |
| 613 | + method: 'POST', | |
| 614 | + headers: { 'Content-Type': 'application/json' }, | |
| 615 | + body: JSON.stringify({ | |
| 616 | + systemPrompt, | |
| 617 | + temperature, | |
| 618 | + message: testPrompt | |
| 619 | + }) | |
| 620 | + }); | |
| 621 | + | |
| 622 | + if (response.ok) { | |
| 623 | + const result = await response.json(); | |
| 624 | + document.getElementById('testOutput').innerHTML = ` | |
| 625 | + <strong>Test Input:</strong> "${testPrompt}"<br> | |
| 626 | + <strong>Response:</strong> "${result.response}" | |
| 627 | + `; | |
| 628 | + } | |
| 629 | + } catch (error) { | |
| 630 | + console.error('Test personality error:', error); | |
| 631 | + document.getElementById('testOutput').innerHTML = 'Test failed'; | |
| 632 | + } | |
| 633 | + } | |
| 634 | + | |
| 635 | + openConversationReplay() { | |
| 636 | + document.getElementById('replayModal').classList.add('active'); | |
| 637 | + this.loadConversations(); | |
| 638 | + } | |
| 639 | + | |
| 640 | + async loadConversations() { | |
| 641 | + const select = document.getElementById('conversationSelect'); | |
| 642 | + select.innerHTML = '<option>Select a conversation...</option>'; | |
| 643 | + | |
| 644 | + this.data.conversations.forEach((conv, id) => { | |
| 645 | + const option = document.createElement('option'); | |
| 646 | + option.value = id; | |
| 647 | + option.textContent = `${conv.platform} - ${conv.chatId} (${conv.messages.length} messages)`; | |
| 648 | + select.appendChild(option); | |
| 649 | + }); | |
| 650 | + } | |
| 651 | + | |
| 652 | + startRealtimeUpdates() { | |
| 653 | + // Update uptime | |
| 654 | + setInterval(() => { | |
| 655 | + const uptime = Date.now() - this.startTime; | |
| 656 | + const hours = Math.floor(uptime / (1000 * 60 * 60)); | |
| 657 | + const minutes = Math.floor((uptime % (1000 * 60 * 60)) / (1000 * 60)); | |
| 658 | + document.getElementById('uptime').textContent = `${hours}h ${minutes}m`; | |
| 659 | + }, 60000); | |
| 660 | + | |
| 661 | + // Update last updated | |
| 662 | + setInterval(() => { | |
| 663 | + document.getElementById('lastUpdated').textContent = new Date().toLocaleTimeString(); | |
| 664 | + }, 1000); | |
| 665 | + | |
| 666 | + // Update response time | |
| 667 | + setInterval(() => { | |
| 668 | + const avgResponse = this.calculateAverageResponseTime(); | |
| 669 | + document.getElementById('responseTime').textContent = `${avgResponse}ms`; | |
| 670 | + }, 5000); | |
| 671 | + } | |
| 672 | + | |
| 673 | + calculateAverageResponseTime() { | |
| 674 | + if (this.data.messages.length === 0) return 0; | |
| 675 | + | |
| 676 | + const recent = this.data.messages.slice(-20); | |
| 677 | + const sum = recent.reduce((acc, msg) => acc + (msg.responseTime || 0), 0); | |
| 678 | + return Math.round(sum / recent.length); | |
| 679 | + } | |
| 680 | + | |
| 681 | + async loadInitialData() { | |
| 682 | + try { | |
| 683 | + // Load analytics | |
| 684 | + const analyticsResponse = await fetch(`${this.serverUrl}/analytics`); | |
| 685 | + if (analyticsResponse.ok) { | |
| 686 | + const analytics = await analyticsResponse.json(); | |
| 687 | + document.getElementById('totalMessages').textContent = analytics.totalMessages || 0; | |
| 688 | + document.getElementById('scammersDetected').textContent = analytics.scammersDetected || 0; | |
| 689 | + } | |
| 690 | + | |
| 691 | + // Load active conversations | |
| 692 | + const conversationsResponse = await fetch(`${this.serverUrl}/conversations/active`); | |
| 693 | + if (conversationsResponse.ok) { | |
| 694 | + const conversations = await conversationsResponse.json(); | |
| 695 | + this.updateConversationCards(conversations); | |
| 696 | + } | |
| 697 | + | |
| 698 | + // Load patterns | |
| 699 | + const patternsResponse = await fetch(`${this.serverUrl}/patterns`); | |
| 700 | + if (patternsResponse.ok) { | |
| 701 | + const patterns = await patternsResponse.json(); | |
| 702 | + patterns.forEach(pattern => this.addPattern(pattern)); | |
| 703 | + } | |
| 704 | + } catch (error) { | |
| 705 | + console.error('Error loading initial data:', error); | |
| 706 | + } | |
| 707 | + } | |
| 708 | + | |
| 709 | + updateConversationCards(conversations) { | |
| 710 | + const grid = document.getElementById('conversationsGrid'); | |
| 711 | + grid.innerHTML = ''; | |
| 712 | + | |
| 713 | + if (conversations.length === 0) { | |
| 714 | + grid.innerHTML = ` | |
| 715 | + <div class="conversation-card"> | |
| 716 | + <div class="conversation-header"> | |
| 717 | + <span class="conversation-id">No active conversations</span> | |
| 718 | + </div> | |
| 719 | + </div> | |
| 720 | + `; | |
| 721 | + return; | |
| 722 | + } | |
| 723 | + | |
| 724 | + conversations.forEach(conv => { | |
| 725 | + this.data.conversations.set(conv.id, conv); | |
| 726 | + | |
| 727 | + const card = document.createElement('div'); | |
| 728 | + card.className = 'conversation-card'; | |
| 729 | + card.innerHTML = ` | |
| 730 | + <div class="conversation-header"> | |
| 731 | + <span class="platform-badge ${conv.platform}">${conv.platform}</span> | |
| 732 | + <span class="conversation-id">${conv.chatId}</span> | |
| 733 | + </div> | |
| 734 | + <div class="conversation-stats"> | |
| 735 | + <span>Messages: ${conv.messageCount}</span> | |
| 736 | + <span>Score: ${conv.scammerScore.toFixed(2)}</span> | |
| 737 | + <span>Duration: ${Math.round(conv.duration / 60000)}m</span> | |
| 738 | + </div> | |
| 739 | + `; | |
| 740 | + | |
| 741 | + card.addEventListener('click', () => { | |
| 742 | + this.viewConversation(conv.id); | |
| 743 | + }); | |
| 744 | + | |
| 745 | + grid.appendChild(card); | |
| 746 | + }); | |
| 747 | + | |
| 748 | + document.getElementById('activeSessions').textContent = conversations.length; | |
| 749 | + } | |
| 750 | + | |
| 751 | + viewConversation(conversationId) { | |
| 752 | + const conv = this.data.conversations.get(conversationId); | |
| 753 | + if (!conv) return; | |
| 754 | + | |
| 755 | + // Open replay modal with this conversation | |
| 756 | + document.getElementById('replayModal').classList.add('active'); | |
| 757 | + document.getElementById('conversationSelect').value = conversationId; | |
| 758 | + this.loadConversationMessages(conversationId); | |
| 759 | + } | |
| 760 | + | |
| 761 | + loadConversationMessages(conversationId) { | |
| 762 | + const conv = this.data.conversations.get(conversationId); | |
| 763 | + if (!conv) return; | |
| 764 | + | |
| 765 | + const container = document.getElementById('replayMessages'); | |
| 766 | + container.innerHTML = ''; | |
| 767 | + | |
| 768 | + conv.messages.forEach(msg => { | |
| 769 | + const msgDiv = document.createElement('div'); | |
| 770 | + msgDiv.className = `replay-message ${msg.sender}`; | |
| 771 | + msgDiv.innerHTML = ` | |
| 772 | + <div class="message-time">${new Date(msg.timestamp).toLocaleTimeString()}</div> | |
| 773 | + <div class="message-content">${msg.content}</div> | |
| 774 | + `; | |
| 775 | + container.appendChild(msgDiv); | |
| 776 | + }); | |
| 777 | + | |
| 778 | + // Update stats | |
| 779 | + document.getElementById('replayStats').innerHTML = ` | |
| 780 | + <span>Messages: ${conv.messages.length}</span> | |
| 781 | + <span>Duration: ${Math.round(conv.duration / 60000)}m</span> | |
| 782 | + <span>Scammer Score: ${conv.scammerScore.toFixed(2)}</span> | |
| 783 | + `; | |
| 784 | + } | |
| 785 | + | |
| 786 | + showNotification(message, type = 'info') { | |
| 787 | + // Create notification element | |
| 788 | + const notification = document.createElement('div'); | |
| 789 | + notification.className = `notification ${type}`; | |
| 790 | + notification.textContent = message; | |
| 791 | + notification.style.cssText = ` | |
| 792 | + position: fixed; | |
| 793 | + top: 80px; | |
| 794 | + right: 20px; | |
| 795 | + padding: 1rem 1.5rem; | |
| 796 | + border-radius: 8px; | |
| 797 | + z-index: 2000; | |
| 798 | + animation: slideIn 0.3s ease; | |
| 799 | + `; | |
| 800 | + | |
| 801 | + // Style based on type | |
| 802 | + const colors = { | |
| 803 | + info: '#3b82f6', | |
| 804 | + success: '#10b981', | |
| 805 | + warning: '#f59e0b', | |
| 806 | + error: '#ef4444' | |
| 807 | + }; | |
| 808 | + | |
| 809 | + notification.style.background = colors[type] || colors.info; | |
| 810 | + notification.style.color = 'white'; | |
| 811 | + | |
| 812 | + document.body.appendChild(notification); | |
| 813 | + | |
| 814 | + // Auto remove after 5 seconds | |
| 815 | + setTimeout(() => { | |
| 816 | + notification.style.animation = 'slideOut 0.3s ease'; | |
| 817 | + setTimeout(() => notification.remove(), 300); | |
| 818 | + }, 5000); | |
| 819 | + } | |
| 820 | +} | |
| 821 | + | |
| 822 | +// Initialize dashboard when DOM is ready | |
| 823 | +document.addEventListener('DOMContentLoaded', () => { | |
| 824 | + window.dashboard = new LooseCannonDashboard(); | |
| 825 | +}); | |
dashboard/index.htmladded@@ -0,0 +1,243 @@ | ||
| 1 | +<!DOCTYPE html> | |
| 2 | +<html lang="en"> | |
| 3 | +<head> | |
| 4 | + <meta charset="UTF-8"> | |
| 5 | + <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| 6 | + <title>LooseCannon Analytics Dashboard</title> | |
| 7 | + <link rel="stylesheet" href="styles.css"> | |
| 8 | + <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script> | |
| 9 | +</head> | |
| 10 | +<body> | |
| 11 | + <div class="dashboard"> | |
| 12 | + <!-- Header --> | |
| 13 | + <header class="dashboard-header"> | |
| 14 | + <div class="header-content"> | |
| 15 | + <h1>LooseCannon Command Center</h1> | |
| 16 | + <div class="server-status"> | |
| 17 | + <span class="status-indicator" id="serverStatus"></span> | |
| 18 | + <span>Server: <span id="serverUrl">localhost:8765</span></span> | |
| 19 | + </div> | |
| 20 | + </div> | |
| 21 | + </header> | |
| 22 | + | |
| 23 | + <!-- Main Grid --> | |
| 24 | + <main class="dashboard-main"> | |
| 25 | + <!-- Stats Cards --> | |
| 26 | + <section class="stats-grid"> | |
| 27 | + <div class="stat-card"> | |
| 28 | + <div class="stat-value" id="totalMessages">0</div> | |
| 29 | + <div class="stat-label">Messages Processed</div> | |
| 30 | + <div class="stat-trend" id="messageTrend">+0%</div> | |
| 31 | + </div> | |
| 32 | + | |
| 33 | + <div class="stat-card danger"> | |
| 34 | + <div class="stat-value" id="scammersDetected">0</div> | |
| 35 | + <div class="stat-label">Scammers Detected</div> | |
| 36 | + <div class="stat-trend" id="scammerTrend">+0%</div> | |
| 37 | + </div> | |
| 38 | + | |
| 39 | + <div class="stat-card success"> | |
| 40 | + <div class="stat-value" id="activeSessions">0</div> | |
| 41 | + <div class="stat-label">Active Sessions</div> | |
| 42 | + <div class="stat-indicator"> | |
| 43 | + <span class="pulse"></span> | |
| 44 | + Live | |
| 45 | + </div> | |
| 46 | + </div> | |
| 47 | + | |
| 48 | + <div class="stat-card info"> | |
| 49 | + <div class="stat-value" id="responseTime">0ms</div> | |
| 50 | + <div class="stat-label">Avg Response Time</div> | |
| 51 | + <div class="stat-trend" id="responseTrend">↓ 0%</div> | |
| 52 | + </div> | |
| 53 | + </section> | |
| 54 | + | |
| 55 | + <!-- Real-time Activity Feed --> | |
| 56 | + <section class="activity-section"> | |
| 57 | + <h2>Live Activity</h2> | |
| 58 | + <div class="activity-feed" id="activityFeed"> | |
| 59 | + <div class="activity-item"> | |
| 60 | + <span class="activity-time">Waiting for activity...</span> | |
| 61 | + <span class="activity-message">System ready</span> | |
| 62 | + </div> | |
| 63 | + </div> | |
| 64 | + </section> | |
| 65 | + | |
| 66 | + <!-- Charts Section --> | |
| 67 | + <section class="charts-section"> | |
| 68 | + <div class="chart-container"> | |
| 69 | + <h3>Message Volume (24h)</h3> | |
| 70 | + <canvas id="volumeChart"></canvas> | |
| 71 | + </div> | |
| 72 | + | |
| 73 | + <div class="chart-container"> | |
| 74 | + <h3>Scammer Detection Rate</h3> | |
| 75 | + <canvas id="detectionChart"></canvas> | |
| 76 | + </div> | |
| 77 | + | |
| 78 | + <div class="chart-container"> | |
| 79 | + <h3>Platform Distribution</h3> | |
| 80 | + <canvas id="platformChart"></canvas> | |
| 81 | + </div> | |
| 82 | + | |
| 83 | + <div class="chart-container"> | |
| 84 | + <h3>Response Times</h3> | |
| 85 | + <canvas id="responseChart"></canvas> | |
| 86 | + </div> | |
| 87 | + </section> | |
| 88 | + | |
| 89 | + <!-- Active Conversations --> | |
| 90 | + <section class="conversations-section"> | |
| 91 | + <h2>Active Conversations</h2> | |
| 92 | + <div class="conversations-grid" id="conversationsGrid"> | |
| 93 | + <div class="conversation-card"> | |
| 94 | + <div class="conversation-header"> | |
| 95 | + <span class="platform-badge whatsapp">WhatsApp</span> | |
| 96 | + <span class="conversation-id">No active conversations</span> | |
| 97 | + </div> | |
| 98 | + <div class="conversation-stats"> | |
| 99 | + <span>Messages: 0</span> | |
| 100 | + <span>Score: 0.0</span> | |
| 101 | + <span>Duration: 0m</span> | |
| 102 | + </div> | |
| 103 | + </div> | |
| 104 | + </div> | |
| 105 | + </section> | |
| 106 | + | |
| 107 | + <!-- Screenshot Evidence --> | |
| 108 | + <section class="evidence-section"> | |
| 109 | + <h2>Recent Evidence</h2> | |
| 110 | + <div class="evidence-grid" id="evidenceGrid"> | |
| 111 | + <div class="evidence-placeholder"> | |
| 112 | + No screenshots captured yet | |
| 113 | + </div> | |
| 114 | + </div> | |
| 115 | + </section> | |
| 116 | + | |
| 117 | + <!-- Pattern Learning --> | |
| 118 | + <section class="patterns-section"> | |
| 119 | + <h2>Learned Patterns</h2> | |
| 120 | + <div class="patterns-container"> | |
| 121 | + <div class="pattern-stats"> | |
| 122 | + <div class="pattern-stat"> | |
| 123 | + <span class="pattern-value" id="totalPatterns">0</span> | |
| 124 | + <span class="pattern-label">Patterns Identified</span> | |
| 125 | + </div> | |
| 126 | + <div class="pattern-stat"> | |
| 127 | + <span class="pattern-value" id="patternAccuracy">0%</span> | |
| 128 | + <span class="pattern-label">Detection Accuracy</span> | |
| 129 | + </div> | |
| 130 | + </div> | |
| 131 | + <div class="patterns-list" id="patternsList"> | |
| 132 | + <div class="pattern-item"> | |
| 133 | + <span class="pattern-type">No patterns learned yet</span> | |
| 134 | + <span class="pattern-count">0 occurrences</span> | |
| 135 | + </div> | |
| 136 | + </div> | |
| 137 | + </div> | |
| 138 | + </section> | |
| 139 | + | |
| 140 | + <!-- Controls --> | |
| 141 | + <section class="controls-section"> | |
| 142 | + <h2>System Controls</h2> | |
| 143 | + <div class="controls-grid"> | |
| 144 | + <button class="control-btn primary" id="exportBtn"> | |
| 145 | + Export Data | |
| 146 | + </button> | |
| 147 | + <button class="control-btn danger" id="emergencyStop"> | |
| 148 | + Emergency Stop All | |
| 149 | + </button> | |
| 150 | + <button class="control-btn" id="clearDataBtn"> | |
| 151 | + Clear Old Data | |
| 152 | + </button> | |
| 153 | + <button class="control-btn" id="syncBtn"> | |
| 154 | + Sync Patterns | |
| 155 | + </button> | |
| 156 | + <button class="control-btn success" id="personalityBtn"> | |
| 157 | + Personality Builder | |
| 158 | + </button> | |
| 159 | + <button class="control-btn info" id="replayBtn"> | |
| 160 | + Conversation Replay | |
| 161 | + </button> | |
| 162 | + </div> | |
| 163 | + </section> | |
| 164 | + </main> | |
| 165 | + | |
| 166 | + <!-- Footer --> | |
| 167 | + <footer class="dashboard-footer"> | |
| 168 | + <div class="footer-content"> | |
| 169 | + <span>LooseCannon v0.3.0</span> | |
| 170 | + <span>Uptime: <span id="uptime">0h 0m</span></span> | |
| 171 | + <span>Last Updated: <span id="lastUpdated">Never</span></span> | |
| 172 | + </div> | |
| 173 | + </footer> | |
| 174 | + </div> | |
| 175 | + | |
| 176 | + <!-- Personality Builder Modal --> | |
| 177 | + <div class="modal" id="personalityModal"> | |
| 178 | + <div class="modal-content"> | |
| 179 | + <div class="modal-header"> | |
| 180 | + <h2>Personality Builder</h2> | |
| 181 | + <button class="modal-close" id="modalClose">×</button> | |
| 182 | + </div> | |
| 183 | + <div class="modal-body"> | |
| 184 | + <form id="personalityForm"> | |
| 185 | + <div class="form-group"> | |
| 186 | + <label>Personality Name</label> | |
| 187 | + <input type="text" id="personalityName" placeholder="e.g., Overly Helpful"> | |
| 188 | + </div> | |
| 189 | + <div class="form-group"> | |
| 190 | + <label>System Prompt</label> | |
| 191 | + <textarea id="systemPrompt" rows="6" placeholder="Describe the personality traits and behavior..."></textarea> | |
| 192 | + </div> | |
| 193 | + <div class="form-group"> | |
| 194 | + <label>Temperature (0.1 - 1.0)</label> | |
| 195 | + <input type="range" id="temperature" min="0.1" max="1" step="0.1" value="0.8"> | |
| 196 | + <span id="tempValue">0.8</span> | |
| 197 | + </div> | |
| 198 | + <div class="form-group"> | |
| 199 | + <label>Example Responses</label> | |
| 200 | + <textarea id="examples" rows="4" placeholder="One example per line..."></textarea> | |
| 201 | + </div> | |
| 202 | + <div class="form-buttons"> | |
| 203 | + <button type="button" class="btn secondary" id="testPersonality">Test</button> | |
| 204 | + <button type="submit" class="btn primary">Save Personality</button> | |
| 205 | + </div> | |
| 206 | + </form> | |
| 207 | + <div class="test-output" id="testOutput"></div> | |
| 208 | + </div> | |
| 209 | + </div> | |
| 210 | + </div> | |
| 211 | + | |
| 212 | + <!-- Conversation Replay Modal --> | |
| 213 | + <div class="modal" id="replayModal"> | |
| 214 | + <div class="modal-content"> | |
| 215 | + <div class="modal-header"> | |
| 216 | + <h2>Conversation Replay</h2> | |
| 217 | + <button class="modal-close" id="replayClose">×</button> | |
| 218 | + </div> | |
| 219 | + <div class="modal-body"> | |
| 220 | + <select id="conversationSelect"> | |
| 221 | + <option>Select a conversation...</option> | |
| 222 | + </select> | |
| 223 | + <div class="replay-container" id="replayContainer"> | |
| 224 | + <div class="replay-controls"> | |
| 225 | + <button id="playBtn">▶ Play</button> | |
| 226 | + <button id="pauseBtn">⏸ Pause</button> | |
| 227 | + <button id="speedBtn">Speed: 1x</button> | |
| 228 | + <input type="range" id="replayProgress" min="0" max="100" value="0"> | |
| 229 | + </div> | |
| 230 | + <div class="replay-messages" id="replayMessages"></div> | |
| 231 | + <div class="replay-stats" id="replayStats"> | |
| 232 | + <span>Messages: 0</span> | |
| 233 | + <span>Duration: 0m</span> | |
| 234 | + <span>Scammer Score: 0.0</span> | |
| 235 | + </div> | |
| 236 | + </div> | |
| 237 | + </div> | |
| 238 | + </div> | |
| 239 | + </div> | |
| 240 | + | |
| 241 | + <script src="dashboard.js" type="module"></script> | |
| 242 | +</body> | |
| 243 | +</html> | |
dashboard/styles.cssadded@@ -0,0 +1,673 @@ | ||
| 1 | +/* LooseCannon Analytics Dashboard Styles */ | |
| 2 | + | |
| 3 | +:root { | |
| 4 | + --bg-primary: #0f0f0f; | |
| 5 | + --bg-secondary: #1a1a1a; | |
| 6 | + --bg-card: #242424; | |
| 7 | + --text-primary: #e0e0e0; | |
| 8 | + --text-secondary: #888; | |
| 9 | + --accent: #667eea; | |
| 10 | + --success: #10b981; | |
| 11 | + --danger: #ef4444; | |
| 12 | + --warning: #f59e0b; | |
| 13 | + --info: #3b82f6; | |
| 14 | + --border: #333; | |
| 15 | +} | |
| 16 | + | |
| 17 | +* { | |
| 18 | + margin: 0; | |
| 19 | + padding: 0; | |
| 20 | + box-sizing: border-box; | |
| 21 | +} | |
| 22 | + | |
| 23 | +body { | |
| 24 | + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, monospace; | |
| 25 | + background: var(--bg-primary); | |
| 26 | + color: var(--text-primary); | |
| 27 | + line-height: 1.6; | |
| 28 | +} | |
| 29 | + | |
| 30 | +/* Dashboard Layout */ | |
| 31 | +.dashboard { | |
| 32 | + min-height: 100vh; | |
| 33 | + display: flex; | |
| 34 | + flex-direction: column; | |
| 35 | +} | |
| 36 | + | |
| 37 | +/* Header */ | |
| 38 | +.dashboard-header { | |
| 39 | + background: var(--bg-secondary); | |
| 40 | + border-bottom: 1px solid var(--border); | |
| 41 | + padding: 1.5rem 2rem; | |
| 42 | + position: sticky; | |
| 43 | + top: 0; | |
| 44 | + z-index: 100; | |
| 45 | +} | |
| 46 | + | |
| 47 | +.header-content { | |
| 48 | + display: flex; | |
| 49 | + justify-content: space-between; | |
| 50 | + align-items: center; | |
| 51 | + max-width: 1400px; | |
| 52 | + margin: 0 auto; | |
| 53 | +} | |
| 54 | + | |
| 55 | +.dashboard-header h1 { | |
| 56 | + font-size: 1.8rem; | |
| 57 | + font-weight: 600; | |
| 58 | + background: linear-gradient(135deg, var(--accent), #764ba2); | |
| 59 | + -webkit-background-clip: text; | |
| 60 | + -webkit-text-fill-color: transparent; | |
| 61 | +} | |
| 62 | + | |
| 63 | +.server-status { | |
| 64 | + display: flex; | |
| 65 | + align-items: center; | |
| 66 | + gap: 0.5rem; | |
| 67 | + font-size: 0.9rem; | |
| 68 | + color: var(--text-secondary); | |
| 69 | +} | |
| 70 | + | |
| 71 | +.status-indicator { | |
| 72 | + width: 10px; | |
| 73 | + height: 10px; | |
| 74 | + border-radius: 50%; | |
| 75 | + background: var(--danger); | |
| 76 | + animation: pulse 2s infinite; | |
| 77 | +} | |
| 78 | + | |
| 79 | +.status-indicator.connected { | |
| 80 | + background: var(--success); | |
| 81 | +} | |
| 82 | + | |
| 83 | +/* Main Content */ | |
| 84 | +.dashboard-main { | |
| 85 | + flex: 1; | |
| 86 | + padding: 2rem; | |
| 87 | + max-width: 1400px; | |
| 88 | + margin: 0 auto; | |
| 89 | + width: 100%; | |
| 90 | +} | |
| 91 | + | |
| 92 | +/* Stats Grid */ | |
| 93 | +.stats-grid { | |
| 94 | + display: grid; | |
| 95 | + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); | |
| 96 | + gap: 1.5rem; | |
| 97 | + margin-bottom: 2rem; | |
| 98 | +} | |
| 99 | + | |
| 100 | +.stat-card { | |
| 101 | + background: var(--bg-card); | |
| 102 | + border: 1px solid var(--border); | |
| 103 | + border-radius: 12px; | |
| 104 | + padding: 1.5rem; | |
| 105 | + transition: transform 0.2s, border-color 0.2s; | |
| 106 | +} | |
| 107 | + | |
| 108 | +.stat-card:hover { | |
| 109 | + transform: translateY(-2px); | |
| 110 | + border-color: var(--accent); | |
| 111 | +} | |
| 112 | + | |
| 113 | +.stat-card.danger { | |
| 114 | + border-color: var(--danger); | |
| 115 | +} | |
| 116 | + | |
| 117 | +.stat-card.success { | |
| 118 | + border-color: var(--success); | |
| 119 | +} | |
| 120 | + | |
| 121 | +.stat-card.info { | |
| 122 | + border-color: var(--info); | |
| 123 | +} | |
| 124 | + | |
| 125 | +.stat-value { | |
| 126 | + font-size: 2.5rem; | |
| 127 | + font-weight: bold; | |
| 128 | + margin-bottom: 0.5rem; | |
| 129 | +} | |
| 130 | + | |
| 131 | +.stat-label { | |
| 132 | + color: var(--text-secondary); | |
| 133 | + font-size: 0.9rem; | |
| 134 | + text-transform: uppercase; | |
| 135 | + letter-spacing: 1px; | |
| 136 | +} | |
| 137 | + | |
| 138 | +.stat-trend { | |
| 139 | + margin-top: 0.5rem; | |
| 140 | + font-size: 0.85rem; | |
| 141 | + color: var(--success); | |
| 142 | +} | |
| 143 | + | |
| 144 | +.stat-trend.negative { | |
| 145 | + color: var(--danger); | |
| 146 | +} | |
| 147 | + | |
| 148 | +.stat-indicator { | |
| 149 | + display: flex; | |
| 150 | + align-items: center; | |
| 151 | + gap: 0.5rem; | |
| 152 | + margin-top: 0.5rem; | |
| 153 | + font-size: 0.85rem; | |
| 154 | +} | |
| 155 | + | |
| 156 | +.pulse { | |
| 157 | + width: 8px; | |
| 158 | + height: 8px; | |
| 159 | + background: var(--success); | |
| 160 | + border-radius: 50%; | |
| 161 | + animation: pulse 1.5s infinite; | |
| 162 | +} | |
| 163 | + | |
| 164 | +/* Activity Feed */ | |
| 165 | +.activity-section { | |
| 166 | + background: var(--bg-card); | |
| 167 | + border: 1px solid var(--border); | |
| 168 | + border-radius: 12px; | |
| 169 | + padding: 1.5rem; | |
| 170 | + margin-bottom: 2rem; | |
| 171 | +} | |
| 172 | + | |
| 173 | +.activity-section h2 { | |
| 174 | + margin-bottom: 1rem; | |
| 175 | + font-size: 1.2rem; | |
| 176 | + color: var(--text-primary); | |
| 177 | +} | |
| 178 | + | |
| 179 | +.activity-feed { | |
| 180 | + max-height: 300px; | |
| 181 | + overflow-y: auto; | |
| 182 | + space-y: 0.5rem; | |
| 183 | +} | |
| 184 | + | |
| 185 | +.activity-item { | |
| 186 | + display: flex; | |
| 187 | + gap: 1rem; | |
| 188 | + padding: 0.75rem; | |
| 189 | + background: var(--bg-secondary); | |
| 190 | + border-radius: 8px; | |
| 191 | + margin-bottom: 0.5rem; | |
| 192 | + font-size: 0.9rem; | |
| 193 | +} | |
| 194 | + | |
| 195 | +.activity-time { | |
| 196 | + color: var(--text-secondary); | |
| 197 | + min-width: 80px; | |
| 198 | +} | |
| 199 | + | |
| 200 | +.activity-message { | |
| 201 | + flex: 1; | |
| 202 | +} | |
| 203 | + | |
| 204 | +.activity-item.scammer { | |
| 205 | + border-left: 3px solid var(--danger); | |
| 206 | +} | |
| 207 | + | |
| 208 | +.activity-item.success { | |
| 209 | + border-left: 3px solid var(--success); | |
| 210 | +} | |
| 211 | + | |
| 212 | +/* Charts Section */ | |
| 213 | +.charts-section { | |
| 214 | + display: grid; | |
| 215 | + grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); | |
| 216 | + gap: 1.5rem; | |
| 217 | + margin-bottom: 2rem; | |
| 218 | +} | |
| 219 | + | |
| 220 | +.chart-container { | |
| 221 | + background: var(--bg-card); | |
| 222 | + border: 1px solid var(--border); | |
| 223 | + border-radius: 12px; | |
| 224 | + padding: 1.5rem; | |
| 225 | +} | |
| 226 | + | |
| 227 | +.chart-container h3 { | |
| 228 | + margin-bottom: 1rem; | |
| 229 | + font-size: 1rem; | |
| 230 | + color: var(--text-primary); | |
| 231 | +} | |
| 232 | + | |
| 233 | +.chart-container canvas { | |
| 234 | + max-height: 250px; | |
| 235 | +} | |
| 236 | + | |
| 237 | +/* Conversations */ | |
| 238 | +.conversations-section { | |
| 239 | + background: var(--bg-card); | |
| 240 | + border: 1px solid var(--border); | |
| 241 | + border-radius: 12px; | |
| 242 | + padding: 1.5rem; | |
| 243 | + margin-bottom: 2rem; | |
| 244 | +} | |
| 245 | + | |
| 246 | +.conversations-section h2 { | |
| 247 | + margin-bottom: 1rem; | |
| 248 | + font-size: 1.2rem; | |
| 249 | +} | |
| 250 | + | |
| 251 | +.conversations-grid { | |
| 252 | + display: grid; | |
| 253 | + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); | |
| 254 | + gap: 1rem; | |
| 255 | +} | |
| 256 | + | |
| 257 | +.conversation-card { | |
| 258 | + background: var(--bg-secondary); | |
| 259 | + border: 1px solid var(--border); | |
| 260 | + border-radius: 8px; | |
| 261 | + padding: 1rem; | |
| 262 | + transition: border-color 0.2s; | |
| 263 | +} | |
| 264 | + | |
| 265 | +.conversation-card:hover { | |
| 266 | + border-color: var(--accent); | |
| 267 | + cursor: pointer; | |
| 268 | +} | |
| 269 | + | |
| 270 | +.conversation-header { | |
| 271 | + display: flex; | |
| 272 | + justify-content: space-between; | |
| 273 | + align-items: center; | |
| 274 | + margin-bottom: 0.75rem; | |
| 275 | +} | |
| 276 | + | |
| 277 | +.platform-badge { | |
| 278 | + padding: 0.25rem 0.5rem; | |
| 279 | + border-radius: 4px; | |
| 280 | + font-size: 0.75rem; | |
| 281 | + text-transform: uppercase; | |
| 282 | + font-weight: bold; | |
| 283 | +} | |
| 284 | + | |
| 285 | +.platform-badge.whatsapp { | |
| 286 | + background: #25d366; | |
| 287 | + color: white; | |
| 288 | +} | |
| 289 | + | |
| 290 | +.platform-badge.telegram { | |
| 291 | + background: #0088cc; | |
| 292 | + color: white; | |
| 293 | +} | |
| 294 | + | |
| 295 | +.platform-badge.messenger { | |
| 296 | + background: #0084ff; | |
| 297 | + color: white; | |
| 298 | +} | |
| 299 | + | |
| 300 | +.conversation-id { | |
| 301 | + font-size: 0.85rem; | |
| 302 | + color: var(--text-secondary); | |
| 303 | +} | |
| 304 | + | |
| 305 | +.conversation-stats { | |
| 306 | + display: flex; | |
| 307 | + gap: 1rem; | |
| 308 | + font-size: 0.85rem; | |
| 309 | + color: var(--text-secondary); | |
| 310 | +} | |
| 311 | + | |
| 312 | +/* Evidence Section */ | |
| 313 | +.evidence-section { | |
| 314 | + background: var(--bg-card); | |
| 315 | + border: 1px solid var(--border); | |
| 316 | + border-radius: 12px; | |
| 317 | + padding: 1.5rem; | |
| 318 | + margin-bottom: 2rem; | |
| 319 | +} | |
| 320 | + | |
| 321 | +.evidence-section h2 { | |
| 322 | + margin-bottom: 1rem; | |
| 323 | +} | |
| 324 | + | |
| 325 | +.evidence-grid { | |
| 326 | + display: grid; | |
| 327 | + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); | |
| 328 | + gap: 1rem; | |
| 329 | +} | |
| 330 | + | |
| 331 | +.evidence-item { | |
| 332 | + position: relative; | |
| 333 | + border: 1px solid var(--border); | |
| 334 | + border-radius: 8px; | |
| 335 | + overflow: hidden; | |
| 336 | + cursor: pointer; | |
| 337 | + transition: transform 0.2s; | |
| 338 | +} | |
| 339 | + | |
| 340 | +.evidence-item:hover { | |
| 341 | + transform: scale(1.05); | |
| 342 | +} | |
| 343 | + | |
| 344 | +.evidence-item img { | |
| 345 | + width: 100%; | |
| 346 | + height: 150px; | |
| 347 | + object-fit: cover; | |
| 348 | +} | |
| 349 | + | |
| 350 | +.evidence-meta { | |
| 351 | + position: absolute; | |
| 352 | + bottom: 0; | |
| 353 | + left: 0; | |
| 354 | + right: 0; | |
| 355 | + background: rgba(0, 0, 0, 0.8); | |
| 356 | + padding: 0.5rem; | |
| 357 | + font-size: 0.75rem; | |
| 358 | + color: white; | |
| 359 | +} | |
| 360 | + | |
| 361 | +.evidence-placeholder { | |
| 362 | + grid-column: 1 / -1; | |
| 363 | + text-align: center; | |
| 364 | + color: var(--text-secondary); | |
| 365 | + padding: 2rem; | |
| 366 | +} | |
| 367 | + | |
| 368 | +/* Patterns Section */ | |
| 369 | +.patterns-section { | |
| 370 | + background: var(--bg-card); | |
| 371 | + border: 1px solid var(--border); | |
| 372 | + border-radius: 12px; | |
| 373 | + padding: 1.5rem; | |
| 374 | + margin-bottom: 2rem; | |
| 375 | +} | |
| 376 | + | |
| 377 | +.patterns-container { | |
| 378 | + display: grid; | |
| 379 | + grid-template-columns: 200px 1fr; | |
| 380 | + gap: 2rem; | |
| 381 | +} | |
| 382 | + | |
| 383 | +.pattern-stats { | |
| 384 | + display: flex; | |
| 385 | + flex-direction: column; | |
| 386 | + gap: 1rem; | |
| 387 | +} | |
| 388 | + | |
| 389 | +.pattern-stat { | |
| 390 | + text-align: center; | |
| 391 | + padding: 1rem; | |
| 392 | + background: var(--bg-secondary); | |
| 393 | + border-radius: 8px; | |
| 394 | +} | |
| 395 | + | |
| 396 | +.pattern-value { | |
| 397 | + display: block; | |
| 398 | + font-size: 2rem; | |
| 399 | + font-weight: bold; | |
| 400 | + color: var(--accent); | |
| 401 | +} | |
| 402 | + | |
| 403 | +.pattern-label { | |
| 404 | + display: block; | |
| 405 | + font-size: 0.8rem; | |
| 406 | + color: var(--text-secondary); | |
| 407 | + margin-top: 0.25rem; | |
| 408 | +} | |
| 409 | + | |
| 410 | +.patterns-list { | |
| 411 | + max-height: 200px; | |
| 412 | + overflow-y: auto; | |
| 413 | +} | |
| 414 | + | |
| 415 | +.pattern-item { | |
| 416 | + display: flex; | |
| 417 | + justify-content: space-between; | |
| 418 | + padding: 0.75rem; | |
| 419 | + background: var(--bg-secondary); | |
| 420 | + border-radius: 6px; | |
| 421 | + margin-bottom: 0.5rem; | |
| 422 | +} | |
| 423 | + | |
| 424 | +.pattern-type { | |
| 425 | + font-weight: 500; | |
| 426 | +} | |
| 427 | + | |
| 428 | +.pattern-count { | |
| 429 | + color: var(--text-secondary); | |
| 430 | + font-size: 0.9rem; | |
| 431 | +} | |
| 432 | + | |
| 433 | +/* Controls */ | |
| 434 | +.controls-section { | |
| 435 | + background: var(--bg-card); | |
| 436 | + border: 1px solid var(--border); | |
| 437 | + border-radius: 12px; | |
| 438 | + padding: 1.5rem; | |
| 439 | + margin-bottom: 2rem; | |
| 440 | +} | |
| 441 | + | |
| 442 | +.controls-grid { | |
| 443 | + display: grid; | |
| 444 | + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); | |
| 445 | + gap: 1rem; | |
| 446 | +} | |
| 447 | + | |
| 448 | +.control-btn { | |
| 449 | + padding: 0.75rem 1.5rem; | |
| 450 | + border: 1px solid var(--border); | |
| 451 | + border-radius: 8px; | |
| 452 | + background: var(--bg-secondary); | |
| 453 | + color: var(--text-primary); | |
| 454 | + font-size: 0.9rem; | |
| 455 | + cursor: pointer; | |
| 456 | + transition: all 0.2s; | |
| 457 | +} | |
| 458 | + | |
| 459 | +.control-btn:hover { | |
| 460 | + transform: translateY(-2px); | |
| 461 | + border-color: var(--accent); | |
| 462 | +} | |
| 463 | + | |
| 464 | +.control-btn.primary { | |
| 465 | + background: var(--accent); | |
| 466 | + color: white; | |
| 467 | + border-color: var(--accent); | |
| 468 | +} | |
| 469 | + | |
| 470 | +.control-btn.danger { | |
| 471 | + background: var(--danger); | |
| 472 | + color: white; | |
| 473 | + border-color: var(--danger); | |
| 474 | +} | |
| 475 | + | |
| 476 | +.control-btn.success { | |
| 477 | + background: var(--success); | |
| 478 | + color: white; | |
| 479 | + border-color: var(--success); | |
| 480 | +} | |
| 481 | + | |
| 482 | +.control-btn.info { | |
| 483 | + background: var(--info); | |
| 484 | + color: white; | |
| 485 | + border-color: var(--info); | |
| 486 | +} | |
| 487 | + | |
| 488 | +/* Modal */ | |
| 489 | +.modal { | |
| 490 | + display: none; | |
| 491 | + position: fixed; | |
| 492 | + top: 0; | |
| 493 | + left: 0; | |
| 494 | + right: 0; | |
| 495 | + bottom: 0; | |
| 496 | + background: rgba(0, 0, 0, 0.8); | |
| 497 | + z-index: 1000; | |
| 498 | + align-items: center; | |
| 499 | + justify-content: center; | |
| 500 | +} | |
| 501 | + | |
| 502 | +.modal.active { | |
| 503 | + display: flex; | |
| 504 | +} | |
| 505 | + | |
| 506 | +.modal-content { | |
| 507 | + background: var(--bg-card); | |
| 508 | + border: 1px solid var(--border); | |
| 509 | + border-radius: 12px; | |
| 510 | + width: 90%; | |
| 511 | + max-width: 600px; | |
| 512 | + max-height: 80vh; | |
| 513 | + overflow: auto; | |
| 514 | +} | |
| 515 | + | |
| 516 | +.modal-header { | |
| 517 | + display: flex; | |
| 518 | + justify-content: space-between; | |
| 519 | + align-items: center; | |
| 520 | + padding: 1.5rem; | |
| 521 | + border-bottom: 1px solid var(--border); | |
| 522 | +} | |
| 523 | + | |
| 524 | +.modal-close { | |
| 525 | + background: none; | |
| 526 | + border: none; | |
| 527 | + color: var(--text-secondary); | |
| 528 | + font-size: 1.5rem; | |
| 529 | + cursor: pointer; | |
| 530 | +} | |
| 531 | + | |
| 532 | +.modal-body { | |
| 533 | + padding: 1.5rem; | |
| 534 | +} | |
| 535 | + | |
| 536 | +/* Forms */ | |
| 537 | +.form-group { | |
| 538 | + margin-bottom: 1.5rem; | |
| 539 | +} | |
| 540 | + | |
| 541 | +.form-group label { | |
| 542 | + display: block; | |
| 543 | + margin-bottom: 0.5rem; | |
| 544 | + color: var(--text-primary); | |
| 545 | + font-size: 0.9rem; | |
| 546 | +} | |
| 547 | + | |
| 548 | +.form-group input, | |
| 549 | +.form-group textarea, | |
| 550 | +.form-group select { | |
| 551 | + width: 100%; | |
| 552 | + padding: 0.75rem; | |
| 553 | + background: var(--bg-secondary); | |
| 554 | + border: 1px solid var(--border); | |
| 555 | + border-radius: 6px; | |
| 556 | + color: var(--text-primary); | |
| 557 | + font-size: 0.9rem; | |
| 558 | +} | |
| 559 | + | |
| 560 | +.form-group input:focus, | |
| 561 | +.form-group textarea:focus { | |
| 562 | + outline: none; | |
| 563 | + border-color: var(--accent); | |
| 564 | +} | |
| 565 | + | |
| 566 | +.form-buttons { | |
| 567 | + display: flex; | |
| 568 | + gap: 1rem; | |
| 569 | + justify-content: flex-end; | |
| 570 | +} | |
| 571 | + | |
| 572 | +.btn { | |
| 573 | + padding: 0.75rem 1.5rem; | |
| 574 | + border: 1px solid var(--border); | |
| 575 | + border-radius: 6px; | |
| 576 | + background: var(--bg-secondary); | |
| 577 | + color: var(--text-primary); | |
| 578 | + cursor: pointer; | |
| 579 | + transition: all 0.2s; | |
| 580 | +} | |
| 581 | + | |
| 582 | +.btn.primary { | |
| 583 | + background: var(--accent); | |
| 584 | + color: white; | |
| 585 | + border-color: var(--accent); | |
| 586 | +} | |
| 587 | + | |
| 588 | +.btn.secondary { | |
| 589 | + background: var(--bg-secondary); | |
| 590 | + color: var(--text-primary); | |
| 591 | +} | |
| 592 | + | |
| 593 | +/* Footer */ | |
| 594 | +.dashboard-footer { | |
| 595 | + background: var(--bg-secondary); | |
| 596 | + border-top: 1px solid var(--border); | |
| 597 | + padding: 1rem 2rem; | |
| 598 | + margin-top: auto; | |
| 599 | +} | |
| 600 | + | |
| 601 | +.footer-content { | |
| 602 | + display: flex; | |
| 603 | + justify-content: space-between; | |
| 604 | + align-items: center; | |
| 605 | + max-width: 1400px; | |
| 606 | + margin: 0 auto; | |
| 607 | + font-size: 0.85rem; | |
| 608 | + color: var(--text-secondary); | |
| 609 | +} | |
| 610 | + | |
| 611 | +/* Animations */ | |
| 612 | +@keyframes pulse { | |
| 613 | + 0% { | |
| 614 | + box-shadow: 0 0 0 0 currentColor; | |
| 615 | + opacity: 1; | |
| 616 | + } | |
| 617 | + 70% { | |
| 618 | + box-shadow: 0 0 0 10px currentColor; | |
| 619 | + opacity: 0; | |
| 620 | + } | |
| 621 | + 100% { | |
| 622 | + box-shadow: 0 0 0 0 currentColor; | |
| 623 | + opacity: 0; | |
| 624 | + } | |
| 625 | +} | |
| 626 | + | |
| 627 | +/* Scrollbar */ | |
| 628 | +::-webkit-scrollbar { | |
| 629 | + width: 8px; | |
| 630 | + height: 8px; | |
| 631 | +} | |
| 632 | + | |
| 633 | +::-webkit-scrollbar-track { | |
| 634 | + background: var(--bg-secondary); | |
| 635 | +} | |
| 636 | + | |
| 637 | +::-webkit-scrollbar-thumb { | |
| 638 | + background: var(--border); | |
| 639 | + border-radius: 4px; | |
| 640 | +} | |
| 641 | + | |
| 642 | +::-webkit-scrollbar-thumb:hover { | |
| 643 | + background: var(--accent); | |
| 644 | +} | |
| 645 | + | |
| 646 | +/* Responsive */ | |
| 647 | +@media (max-width: 768px) { | |
| 648 | + .dashboard-main { | |
| 649 | + padding: 1rem; | |
| 650 | + } | |
| 651 | + | |
| 652 | + .stats-grid { | |
| 653 | + grid-template-columns: 1fr; | |
| 654 | + } | |
| 655 | + | |
| 656 | + .charts-section { | |
| 657 | + grid-template-columns: 1fr; | |
| 658 | + } | |
| 659 | + | |
| 660 | + .conversations-grid { | |
| 661 | + grid-template-columns: 1fr; | |
| 662 | + } | |
| 663 | + | |
| 664 | + .patterns-container { | |
| 665 | + grid-template-columns: 1fr; | |
| 666 | + } | |
| 667 | + | |
| 668 | + .footer-content { | |
| 669 | + flex-direction: column; | |
| 670 | + gap: 0.5rem; | |
| 671 | + text-align: center; | |
| 672 | + } | |
| 673 | +} | |
extension-chrome/background/pattern-learning.jsadded@@ -0,0 +1,505 @@ | ||
| 1 | +// Pattern Learning System for Scammer Detection | |
| 2 | +// Uses machine learning-inspired techniques to identify and learn scammer patterns | |
| 3 | + | |
| 4 | +export class PatternLearning { | |
| 5 | + constructor() { | |
| 6 | + this.patterns = new Map(); | |
| 7 | + this.weights = new Map(); | |
| 8 | + this.threshold = 0.7; | |
| 9 | + this.learningRate = 0.1; | |
| 10 | + this.decayRate = 0.95; | |
| 11 | + this.minConfidence = 0.6; | |
| 12 | + this.init(); | |
| 13 | + } | |
| 14 | + | |
| 15 | + async init() { | |
| 16 | + await this.loadPatterns(); | |
| 17 | + await this.loadWeights(); | |
| 18 | + this.startPeriodicTraining(); | |
| 19 | + } | |
| 20 | + | |
| 21 | + async loadPatterns() { | |
| 22 | + const stored = await chrome.storage.local.get('learned_patterns'); | |
| 23 | + if (stored.learned_patterns) { | |
| 24 | + stored.learned_patterns.forEach(pattern => { | |
| 25 | + this.patterns.set(pattern.id, pattern); | |
| 26 | + }); | |
| 27 | + } | |
| 28 | + | |
| 29 | + // Load default patterns | |
| 30 | + this.loadDefaultPatterns(); | |
| 31 | + } | |
| 32 | + | |
| 33 | + loadDefaultPatterns() { | |
| 34 | + const defaults = [ | |
| 35 | + { | |
| 36 | + id: 'urgent_action', | |
| 37 | + type: 'keyword_cluster', | |
| 38 | + features: ['urgent', 'immediate', 'act now', 'expire', 'limited time'], | |
| 39 | + weight: 0.8, | |
| 40 | + confidence: 0.9 | |
| 41 | + }, | |
| 42 | + { | |
| 43 | + id: 'money_request', | |
| 44 | + type: 'keyword_cluster', | |
| 45 | + features: ['send money', 'wire transfer', 'gift card', 'payment', 'fee'], | |
| 46 | + weight: 0.9, | |
| 47 | + confidence: 0.95 | |
| 48 | + }, | |
| 49 | + { | |
| 50 | + id: 'personal_info', | |
| 51 | + type: 'keyword_cluster', | |
| 52 | + features: ['social security', 'password', 'account number', 'pin', 'verification code'], | |
| 53 | + weight: 0.85, | |
| 54 | + confidence: 0.9 | |
| 55 | + }, | |
| 56 | + { | |
| 57 | + id: 'suspicious_link', | |
| 58 | + type: 'url_pattern', | |
| 59 | + features: ['bit.ly', 'tinyurl', 'short.link', 'click.here'], | |
| 60 | + weight: 0.75, | |
| 61 | + confidence: 0.8 | |
| 62 | + }, | |
| 63 | + { | |
| 64 | + id: 'impersonation', | |
| 65 | + type: 'keyword_cluster', | |
| 66 | + features: ['official', 'authorized', 'representative', 'department', 'agency'], | |
| 67 | + weight: 0.7, | |
| 68 | + confidence: 0.75 | |
| 69 | + }, | |
| 70 | + { | |
| 71 | + id: 'threat_language', | |
| 72 | + type: 'keyword_cluster', | |
| 73 | + features: ['suspended', 'terminated', 'legal action', 'arrest', 'prosecution'], | |
| 74 | + weight: 0.85, | |
| 75 | + confidence: 0.85 | |
| 76 | + } | |
| 77 | + ]; | |
| 78 | + | |
| 79 | + defaults.forEach(pattern => { | |
| 80 | + if (!this.patterns.has(pattern.id)) { | |
| 81 | + this.patterns.set(pattern.id, pattern); | |
| 82 | + this.weights.set(pattern.id, pattern.weight); | |
| 83 | + } | |
| 84 | + }); | |
| 85 | + } | |
| 86 | + | |
| 87 | + async loadWeights() { | |
| 88 | + const stored = await chrome.storage.local.get('pattern_weights'); | |
| 89 | + if (stored.pattern_weights) { | |
| 90 | + Object.entries(stored.pattern_weights).forEach(([id, weight]) => { | |
| 91 | + this.weights.set(id, weight); | |
| 92 | + }); | |
| 93 | + } | |
| 94 | + } | |
| 95 | + | |
| 96 | + async savePatterns() { | |
| 97 | + const patterns = Array.from(this.patterns.values()); | |
| 98 | + await chrome.storage.local.set({ learned_patterns: patterns }); | |
| 99 | + } | |
| 100 | + | |
| 101 | + async saveWeights() { | |
| 102 | + const weights = Object.fromEntries(this.weights); | |
| 103 | + await chrome.storage.local.set({ pattern_weights: weights }); | |
| 104 | + } | |
| 105 | + | |
| 106 | + analyzeMessage(message, metadata = {}) { | |
| 107 | + const analysis = { | |
| 108 | + score: 0, | |
| 109 | + matchedPatterns: [], | |
| 110 | + features: [], | |
| 111 | + confidence: 0, | |
| 112 | + recommendation: 'monitor' | |
| 113 | + }; | |
| 114 | + | |
| 115 | + // Extract features from message | |
| 116 | + const features = this.extractFeatures(message, metadata); | |
| 117 | + analysis.features = features; | |
| 118 | + | |
| 119 | + // Check against learned patterns | |
| 120 | + this.patterns.forEach((pattern, patternId) => { | |
| 121 | + const match = this.matchPattern(features, pattern); | |
| 122 | + if (match.score > this.minConfidence) { | |
| 123 | + analysis.matchedPatterns.push({ | |
| 124 | + id: patternId, | |
| 125 | + type: pattern.type, | |
| 126 | + score: match.score, | |
| 127 | + weight: this.weights.get(patternId) || 0.5 | |
| 128 | + }); | |
| 129 | + } | |
| 130 | + }); | |
| 131 | + | |
| 132 | + // Calculate overall score | |
| 133 | + if (analysis.matchedPatterns.length > 0) { | |
| 134 | + const weightedSum = analysis.matchedPatterns.reduce((sum, pattern) => | |
| 135 | + sum + (pattern.score * pattern.weight), 0 | |
| 136 | + ); | |
| 137 | + const totalWeight = analysis.matchedPatterns.reduce((sum, pattern) => | |
| 138 | + sum + pattern.weight, 0 | |
| 139 | + ); | |
| 140 | + | |
| 141 | + analysis.score = weightedSum / totalWeight; | |
| 142 | + analysis.confidence = this.calculateConfidence(analysis.matchedPatterns); | |
| 143 | + } | |
| 144 | + | |
| 145 | + // Determine recommendation | |
| 146 | + if (analysis.score > 0.9) { | |
| 147 | + analysis.recommendation = 'block'; | |
| 148 | + } else if (analysis.score > 0.7) { | |
| 149 | + analysis.recommendation = 'warn'; | |
| 150 | + } else if (analysis.score > 0.5) { | |
| 151 | + analysis.recommendation = 'monitor_closely'; | |
| 152 | + } | |
| 153 | + | |
| 154 | + return analysis; | |
| 155 | + } | |
| 156 | + | |
| 157 | + extractFeatures(message, metadata) { | |
| 158 | + const features = { | |
| 159 | + keywords: [], | |
| 160 | + urls: [], | |
| 161 | + patterns: [], | |
| 162 | + metrics: {}, | |
| 163 | + behavioral: [] | |
| 164 | + }; | |
| 165 | + | |
| 166 | + const text = message.content || message.text || ''; | |
| 167 | + const lowerText = text.toLowerCase(); | |
| 168 | + | |
| 169 | + // Extract keywords | |
| 170 | + features.keywords = this.extractKeywords(lowerText); | |
| 171 | + | |
| 172 | + // Extract URLs | |
| 173 | + features.urls = this.extractUrls(text); | |
| 174 | + | |
| 175 | + // Extract patterns | |
| 176 | + features.patterns = this.extractPatterns(text); | |
| 177 | + | |
| 178 | + // Calculate metrics | |
| 179 | + features.metrics = { | |
| 180 | + length: text.length, | |
| 181 | + wordCount: text.split(/\s+/).length, | |
| 182 | + uppercaseRatio: (text.match(/[A-Z]/g) || []).length / text.length, | |
| 183 | + punctuationCount: (text.match(/[!?]/g) || []).length, | |
| 184 | + numberCount: (text.match(/\d+/g) || []).length, | |
| 185 | + dollarSignCount: (text.match(/\$/g) || []).length | |
| 186 | + }; | |
| 187 | + | |
| 188 | + // Behavioral features | |
| 189 | + if (metadata.responseTime) { | |
| 190 | + features.behavioral.push({ | |
| 191 | + type: 'response_speed', | |
| 192 | + value: metadata.responseTime < 1000 ? 'instant' : 'normal' | |
| 193 | + }); | |
| 194 | + } | |
| 195 | + | |
| 196 | + if (metadata.messageCount) { | |
| 197 | + features.behavioral.push({ | |
| 198 | + type: 'message_frequency', | |
| 199 | + value: metadata.messageCount > 10 ? 'high' : 'normal' | |
| 200 | + }); | |
| 201 | + } | |
| 202 | + | |
| 203 | + return features; | |
| 204 | + } | |
| 205 | + | |
| 206 | + extractKeywords(text) { | |
| 207 | + const keywords = []; | |
| 208 | + const commonScamWords = [ | |
| 209 | + 'urgent', 'verify', 'suspended', 'confirm', 'prize', 'winner', | |
| 210 | + 'congratulations', 'claim', 'refund', 'irs', 'tax', 'arrest', | |
| 211 | + 'legal', 'bitcoin', 'investment', 'guaranteed', 'risk free' | |
| 212 | + ]; | |
| 213 | + | |
| 214 | + commonScamWords.forEach(word => { | |
| 215 | + if (text.includes(word)) { | |
| 216 | + keywords.push(word); | |
| 217 | + } | |
| 218 | + }); | |
| 219 | + | |
| 220 | + return keywords; | |
| 221 | + } | |
| 222 | + | |
| 223 | + extractUrls(text) { | |
| 224 | + const urlRegex = /(https?:\/\/[^\s]+)/g; | |
| 225 | + const urls = text.match(urlRegex) || []; | |
| 226 | + | |
| 227 | + return urls.map(url => ({ | |
| 228 | + url, | |
| 229 | + shortened: this.isShortened(url), | |
| 230 | + suspicious: this.isSuspiciousUrl(url) | |
| 231 | + })); | |
| 232 | + } | |
| 233 | + | |
| 234 | + isShortened(url) { | |
| 235 | + const shorteners = ['bit.ly', 'tinyurl.com', 'short.link', 'ow.ly', 'goo.gl']; | |
| 236 | + return shorteners.some(shortener => url.includes(shortener)); | |
| 237 | + } | |
| 238 | + | |
| 239 | + isSuspiciousUrl(url) { | |
| 240 | + // Check for typosquatting and suspicious patterns | |
| 241 | + const suspicious = [ | |
| 242 | + 'amaz0n', 'payp4l', 'mircosoft', 'goggle', | |
| 243 | + 'faceb00k', 'app1e', 'netf1ix' | |
| 244 | + ]; | |
| 245 | + | |
| 246 | + return suspicious.some(pattern => url.toLowerCase().includes(pattern)); | |
| 247 | + } | |
| 248 | + | |
| 249 | + extractPatterns(text) { | |
| 250 | + const patterns = []; | |
| 251 | + | |
| 252 | + // Phone number pattern | |
| 253 | + if (/[\+]?[(]?[0-9]{1,4}[)]?[-\s\.]?[(]?[0-9]{1,4}[)]?[-\s\.]?[0-9]{1,5}[-\s\.]?[0-9]{1,5}/.test(text)) { | |
| 254 | + patterns.push('phone_number'); | |
| 255 | + } | |
| 256 | + | |
| 257 | + // Email pattern | |
| 258 | + if (/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/.test(text)) { | |
| 259 | + patterns.push('email_address'); | |
| 260 | + } | |
| 261 | + | |
| 262 | + // Money amount pattern | |
| 263 | + if (/\$[\d,]+(\.\d{2})?/.test(text)) { | |
| 264 | + patterns.push('money_amount'); | |
| 265 | + } | |
| 266 | + | |
| 267 | + // Verification code pattern | |
| 268 | + if (/\b\d{4,6}\b/.test(text) && text.includes('code')) { | |
| 269 | + patterns.push('verification_code'); | |
| 270 | + } | |
| 271 | + | |
| 272 | + return patterns; | |
| 273 | + } | |
| 274 | + | |
| 275 | + matchPattern(features, pattern) { | |
| 276 | + let score = 0; | |
| 277 | + let matches = 0; | |
| 278 | + | |
| 279 | + if (pattern.type === 'keyword_cluster') { | |
| 280 | + // Check how many keywords from the pattern are in the message | |
| 281 | + pattern.features.forEach(keyword => { | |
| 282 | + if (features.keywords.includes(keyword.toLowerCase())) { | |
| 283 | + matches++; | |
| 284 | + } | |
| 285 | + }); | |
| 286 | + | |
| 287 | + score = matches / pattern.features.length; | |
| 288 | + } else if (pattern.type === 'url_pattern') { | |
| 289 | + // Check URLs | |
| 290 | + features.urls.forEach(urlInfo => { | |
| 291 | + pattern.features.forEach(urlPattern => { | |
| 292 | + if (urlInfo.url.includes(urlPattern)) { | |
| 293 | + matches++; | |
| 294 | + } | |
| 295 | + }); | |
| 296 | + }); | |
| 297 | + | |
| 298 | + score = matches > 0 ? 1 : 0; | |
| 299 | + } else if (pattern.type === 'behavioral') { | |
| 300 | + // Check behavioral patterns | |
| 301 | + features.behavioral.forEach(behavior => { | |
| 302 | + if (pattern.features.includes(behavior.type)) { | |
| 303 | + matches++; | |
| 304 | + } | |
| 305 | + }); | |
| 306 | + | |
| 307 | + score = matches / pattern.features.length; | |
| 308 | + } | |
| 309 | + | |
| 310 | + return { score, matches }; | |
| 311 | + } | |
| 312 | + | |
| 313 | + calculateConfidence(matchedPatterns) { | |
| 314 | + if (matchedPatterns.length === 0) return 0; | |
| 315 | + | |
| 316 | + // Confidence increases with more pattern matches | |
| 317 | + const baseConfidence = matchedPatterns.reduce((sum, p) => sum + p.score, 0) / matchedPatterns.length; | |
| 318 | + const diversityBonus = Math.min(matchedPatterns.length * 0.1, 0.3); | |
| 319 | + | |
| 320 | + return Math.min(baseConfidence + diversityBonus, 1); | |
| 321 | + } | |
| 322 | + | |
| 323 | + async addPattern(messageData, scammerScore) { | |
| 324 | + const features = this.extractFeatures(messageData); | |
| 325 | + | |
| 326 | + // Only learn from high-confidence scammer messages | |
| 327 | + if (scammerScore < this.threshold) return; | |
| 328 | + | |
| 329 | + // Update weights for matched patterns (reinforcement) | |
| 330 | + const analysis = this.analyzeMessage(messageData); | |
| 331 | + analysis.matchedPatterns.forEach(pattern => { | |
| 332 | + const currentWeight = this.weights.get(pattern.id) || 0.5; | |
| 333 | + const newWeight = currentWeight + (this.learningRate * (scammerScore - currentWeight)); | |
| 334 | + this.weights.set(pattern.id, Math.min(newWeight, 1)); | |
| 335 | + }); | |
| 336 | + | |
| 337 | + // Check if this represents a new pattern | |
| 338 | + const novelty = this.calculateNovelty(features); | |
| 339 | + if (novelty > 0.3) { | |
| 340 | + await this.createNewPattern(features, scammerScore); | |
| 341 | + } | |
| 342 | + | |
| 343 | + // Save updated weights | |
| 344 | + await this.saveWeights(); | |
| 345 | + | |
| 346 | + // Notify dashboard | |
| 347 | + this.notifyLearning(features, scammerScore); | |
| 348 | + } | |
| 349 | + | |
| 350 | + calculateNovelty(features) { | |
| 351 | + // Check how different these features are from existing patterns | |
| 352 | + let maxSimilarity = 0; | |
| 353 | + | |
| 354 | + this.patterns.forEach(pattern => { | |
| 355 | + const match = this.matchPattern(features, pattern); | |
| 356 | + maxSimilarity = Math.max(maxSimilarity, match.score); | |
| 357 | + }); | |
| 358 | + | |
| 359 | + return 1 - maxSimilarity; | |
| 360 | + } | |
| 361 | + | |
| 362 | + async createNewPattern(features, confidence) { | |
| 363 | + const id = `learned_${Date.now()}`; | |
| 364 | + | |
| 365 | + const newPattern = { | |
| 366 | + id, | |
| 367 | + type: 'learned', | |
| 368 | + features: { | |
| 369 | + keywords: features.keywords.slice(0, 10), | |
| 370 | + patterns: features.patterns, | |
| 371 | + metrics: features.metrics | |
| 372 | + }, | |
| 373 | + weight: confidence * 0.7, // Start with lower weight | |
| 374 | + confidence, | |
| 375 | + learnedAt: new Date().toISOString(), | |
| 376 | + occurrences: 1 | |
| 377 | + }; | |
| 378 | + | |
| 379 | + this.patterns.set(id, newPattern); | |
| 380 | + this.weights.set(id, newPattern.weight); | |
| 381 | + | |
| 382 | + await this.savePatterns(); | |
| 383 | + | |
| 384 | + console.log('[PatternLearning] New pattern learned:', id); | |
| 385 | + | |
| 386 | + return newPattern; | |
| 387 | + } | |
| 388 | + | |
| 389 | + async updateFromServer(serverPatterns) { | |
| 390 | + let updated = 0; | |
| 391 | + | |
| 392 | + serverPatterns.forEach(serverPattern => { | |
| 393 | + const existing = this.patterns.get(serverPattern.id); | |
| 394 | + | |
| 395 | + if (!existing || serverPattern.confidence > existing.confidence) { | |
| 396 | + this.patterns.set(serverPattern.id, serverPattern); | |
| 397 | + this.weights.set(serverPattern.id, serverPattern.weight); | |
| 398 | + updated++; | |
| 399 | + } | |
| 400 | + }); | |
| 401 | + | |
| 402 | + if (updated > 0) { | |
| 403 | + await this.savePatterns(); | |
| 404 | + await this.saveWeights(); | |
| 405 | + console.log(`[PatternLearning] Updated ${updated} patterns from server`); | |
| 406 | + } | |
| 407 | + | |
| 408 | + return updated; | |
| 409 | + } | |
| 410 | + | |
| 411 | + async getPatterns() { | |
| 412 | + return Array.from(this.patterns.values()).map(pattern => ({ | |
| 413 | + ...pattern, | |
| 414 | + weight: this.weights.get(pattern.id) || 0.5, | |
| 415 | + effectiveness: this.calculateEffectiveness(pattern.id) | |
| 416 | + })); | |
| 417 | + } | |
| 418 | + | |
| 419 | + calculateEffectiveness(patternId) { | |
| 420 | + // Track how effective each pattern is at detecting scammers | |
| 421 | + // This would be based on true/false positive rates in production | |
| 422 | + const weight = this.weights.get(patternId) || 0.5; | |
| 423 | + const pattern = this.patterns.get(patternId); | |
| 424 | + | |
| 425 | + if (!pattern) return 0; | |
| 426 | + | |
| 427 | + // Simple effectiveness score based on weight and confidence | |
| 428 | + return (weight * 0.7 + (pattern.confidence || 0.5) * 0.3); | |
| 429 | + } | |
| 430 | + | |
| 431 | + startPeriodicTraining() { | |
| 432 | + // Decay weights periodically to adapt to changing patterns | |
| 433 | + setInterval(() => { | |
| 434 | + this.decayWeights(); | |
| 435 | + }, 6 * 60 * 60 * 1000); // Every 6 hours | |
| 436 | + } | |
| 437 | + | |
| 438 | + async decayWeights() { | |
| 439 | + let decayed = 0; | |
| 440 | + | |
| 441 | + this.weights.forEach((weight, patternId) => { | |
| 442 | + // Don't decay core patterns below minimum | |
| 443 | + const pattern = this.patterns.get(patternId); | |
| 444 | + const minWeight = pattern && pattern.type !== 'learned' ? 0.5 : 0.1; | |
| 445 | + | |
| 446 | + const newWeight = Math.max(weight * this.decayRate, minWeight); | |
| 447 | + if (newWeight !== weight) { | |
| 448 | + this.weights.set(patternId, newWeight); | |
| 449 | + decayed++; | |
| 450 | + } | |
| 451 | + }); | |
| 452 | + | |
| 453 | + if (decayed > 0) { | |
| 454 | + await this.saveWeights(); | |
| 455 | + console.log(`[PatternLearning] Decayed ${decayed} pattern weights`); | |
| 456 | + } | |
| 457 | + } | |
| 458 | + | |
| 459 | + async exportPatterns() { | |
| 460 | + const patterns = await this.getPatterns(); | |
| 461 | + | |
| 462 | + return { | |
| 463 | + version: '1.0', | |
| 464 | + timestamp: new Date().toISOString(), | |
| 465 | + patterns: patterns.sort((a, b) => b.effectiveness - a.effectiveness), | |
| 466 | + statistics: { | |
| 467 | + total: patterns.length, | |
| 468 | + learned: patterns.filter(p => p.type === 'learned').length, | |
| 469 | + averageConfidence: patterns.reduce((sum, p) => sum + (p.confidence || 0), 0) / patterns.length | |
| 470 | + } | |
| 471 | + }; | |
| 472 | + } | |
| 473 | + | |
| 474 | + notifyLearning(features, score) { | |
| 475 | + // Notify dashboard about learning event | |
| 476 | + chrome.runtime.sendMessage({ | |
| 477 | + type: 'DASHBOARD_UPDATE', | |
| 478 | + data: { | |
| 479 | + event: 'pattern_learned', | |
| 480 | + features: features.keywords.slice(0, 5), | |
| 481 | + score, | |
| 482 | + timestamp: new Date().toISOString() | |
| 483 | + } | |
| 484 | + }).catch(() => { | |
| 485 | + // Dashboard might not be open | |
| 486 | + }); | |
| 487 | + } | |
| 488 | + | |
| 489 | + async resetLearning() { | |
| 490 | + // Reset to default patterns only | |
| 491 | + this.patterns.clear(); | |
| 492 | + this.weights.clear(); | |
| 493 | + this.loadDefaultPatterns(); | |
| 494 | + | |
| 495 | + await this.savePatterns(); | |
| 496 | + await this.saveWeights(); | |
| 497 | + | |
| 498 | + console.log('[PatternLearning] Reset to default patterns'); | |
| 499 | + } | |
| 500 | +} | |
| 501 | + | |
| 502 | +// Export for use in service worker | |
| 503 | +if (typeof module !== 'undefined' && module.exports) { | |
| 504 | + module.exports = PatternLearning; | |
| 505 | +} | |
extension-chrome/background/screenshot-manager.jsadded@@ -0,0 +1,408 @@ | ||
| 1 | +// Screenshot Manager for Evidence Collection | |
| 2 | +// Captures and stores screenshots when scammers are detected | |
| 3 | + | |
| 4 | +export class ScreenshotManager { | |
| 5 | + constructor() { | |
| 6 | + this.storage = 'screenshots'; | |
| 7 | + this.maxScreenshots = 100; | |
| 8 | + this.compressionQuality = 0.8; | |
| 9 | + this.init(); | |
| 10 | + } | |
| 11 | + | |
| 12 | + async init() { | |
| 13 | + // Create storage structure if doesn't exist | |
| 14 | + const stored = await chrome.storage.local.get(this.storage); | |
| 15 | + if (!stored[this.storage]) { | |
| 16 | + await chrome.storage.local.set({ [this.storage]: [] }); | |
| 17 | + } | |
| 18 | + } | |
| 19 | + | |
| 20 | + async capture(tab, metadata = {}) { | |
| 21 | + try { | |
| 22 | + // Capture visible tab | |
| 23 | + const dataUrl = await chrome.tabs.captureVisibleTab(tab.windowId, { | |
| 24 | + format: 'jpeg', | |
| 25 | + quality: Math.round(this.compressionQuality * 100) | |
| 26 | + }); | |
| 27 | + | |
| 28 | + // Generate unique ID | |
| 29 | + const id = `screenshot_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; | |
| 30 | + | |
| 31 | + // Create screenshot object | |
| 32 | + const screenshot = { | |
| 33 | + id, | |
| 34 | + timestamp: new Date().toISOString(), | |
| 35 | + url: tab.url, | |
| 36 | + title: tab.title, | |
| 37 | + platform: this.detectPlatform(tab.url), | |
| 38 | + dataUrl: await this.compressImage(dataUrl), | |
| 39 | + thumbnail: await this.createThumbnail(dataUrl), | |
| 40 | + metadata: { | |
| 41 | + ...metadata, | |
| 42 | + tabId: tab.id, | |
| 43 | + windowId: tab.windowId, | |
| 44 | + incognito: tab.incognito | |
| 45 | + } | |
| 46 | + }; | |
| 47 | + | |
| 48 | + // Store screenshot | |
| 49 | + await this.store(screenshot); | |
| 50 | + | |
| 51 | + // Notify dashboard | |
| 52 | + this.notifyDashboard(screenshot); | |
| 53 | + | |
| 54 | + return { | |
| 55 | + success: true, | |
| 56 | + id: screenshot.id, | |
| 57 | + size: this.calculateSize(screenshot.dataUrl) | |
| 58 | + }; | |
| 59 | + } catch (error) { | |
| 60 | + console.error('[ScreenshotManager] Capture error:', error); | |
| 61 | + return { | |
| 62 | + success: false, | |
| 63 | + error: error.message | |
| 64 | + }; | |
| 65 | + } | |
| 66 | + } | |
| 67 | + | |
| 68 | + detectPlatform(url) { | |
| 69 | + if (url.includes('whatsapp.com')) return 'whatsapp'; | |
| 70 | + if (url.includes('telegram.org')) return 'telegram'; | |
| 71 | + if (url.includes('messenger.com')) return 'messenger'; | |
| 72 | + return 'unknown'; | |
| 73 | + } | |
| 74 | + | |
| 75 | + async compressImage(dataUrl) { | |
| 76 | + return new Promise((resolve) => { | |
| 77 | + const img = new Image(); | |
| 78 | + img.onload = () => { | |
| 79 | + const canvas = document.createElement('canvas'); | |
| 80 | + const ctx = canvas.getContext('2d'); | |
| 81 | + | |
| 82 | + // Set max dimensions | |
| 83 | + const maxWidth = 1920; | |
| 84 | + const maxHeight = 1080; | |
| 85 | + | |
| 86 | + let width = img.width; | |
| 87 | + let height = img.height; | |
| 88 | + | |
| 89 | + // Calculate new dimensions | |
| 90 | + if (width > maxWidth || height > maxHeight) { | |
| 91 | + const ratio = Math.min(maxWidth / width, maxHeight / height); | |
| 92 | + width *= ratio; | |
| 93 | + height *= ratio; | |
| 94 | + } | |
| 95 | + | |
| 96 | + canvas.width = width; | |
| 97 | + canvas.height = height; | |
| 98 | + | |
| 99 | + // Draw and compress | |
| 100 | + ctx.drawImage(img, 0, 0, width, height); | |
| 101 | + resolve(canvas.toDataURL('image/jpeg', this.compressionQuality)); | |
| 102 | + }; | |
| 103 | + | |
| 104 | + img.src = dataUrl; | |
| 105 | + }); | |
| 106 | + } | |
| 107 | + | |
| 108 | + async createThumbnail(dataUrl) { | |
| 109 | + return new Promise((resolve) => { | |
| 110 | + const img = new Image(); | |
| 111 | + img.onload = () => { | |
| 112 | + const canvas = document.createElement('canvas'); | |
| 113 | + const ctx = canvas.getContext('2d'); | |
| 114 | + | |
| 115 | + // Thumbnail dimensions | |
| 116 | + const thumbWidth = 320; | |
| 117 | + const thumbHeight = 180; | |
| 118 | + | |
| 119 | + canvas.width = thumbWidth; | |
| 120 | + canvas.height = thumbHeight; | |
| 121 | + | |
| 122 | + // Draw thumbnail | |
| 123 | + ctx.drawImage(img, 0, 0, thumbWidth, thumbHeight); | |
| 124 | + resolve(canvas.toDataURL('image/jpeg', 0.6)); | |
| 125 | + }; | |
| 126 | + | |
| 127 | + img.src = dataUrl; | |
| 128 | + }); | |
| 129 | + } | |
| 130 | + | |
| 131 | + calculateSize(dataUrl) { | |
| 132 | + // Estimate size in bytes | |
| 133 | + const base64Length = dataUrl.length - 'data:image/jpeg;base64,'.length; | |
| 134 | + const sizeInBytes = base64Length * 0.75; | |
| 135 | + return Math.round(sizeInBytes / 1024); // Return in KB | |
| 136 | + } | |
| 137 | + | |
| 138 | + async store(screenshot) { | |
| 139 | + const stored = await chrome.storage.local.get(this.storage); | |
| 140 | + let screenshots = stored[this.storage] || []; | |
| 141 | + | |
| 142 | + // Add new screenshot | |
| 143 | + screenshots.unshift(screenshot); | |
| 144 | + | |
| 145 | + // Enforce max limit | |
| 146 | + if (screenshots.length > this.maxScreenshots) { | |
| 147 | + screenshots = screenshots.slice(0, this.maxScreenshots); | |
| 148 | + } | |
| 149 | + | |
| 150 | + // Update storage | |
| 151 | + await chrome.storage.local.set({ [this.storage]: screenshots }); | |
| 152 | + | |
| 153 | + // Also store metadata separately for quick access | |
| 154 | + await this.storeMetadata(screenshot); | |
| 155 | + | |
| 156 | + return screenshot; | |
| 157 | + } | |
| 158 | + | |
| 159 | + async storeMetadata(screenshot) { | |
| 160 | + const metaKey = `${this.storage}_metadata`; | |
| 161 | + const stored = await chrome.storage.local.get(metaKey); | |
| 162 | + let metadata = stored[metaKey] || []; | |
| 163 | + | |
| 164 | + metadata.unshift({ | |
| 165 | + id: screenshot.id, | |
| 166 | + timestamp: screenshot.timestamp, | |
| 167 | + platform: screenshot.platform, | |
| 168 | + url: screenshot.url, | |
| 169 | + scammerScore: screenshot.metadata.scammerScore || 0 | |
| 170 | + }); | |
| 171 | + | |
| 172 | + // Keep only last 500 metadata entries | |
| 173 | + if (metadata.length > 500) { | |
| 174 | + metadata = metadata.slice(0, 500); | |
| 175 | + } | |
| 176 | + | |
| 177 | + await chrome.storage.local.set({ [metaKey]: metadata }); | |
| 178 | + } | |
| 179 | + | |
| 180 | + async get(id) { | |
| 181 | + const stored = await chrome.storage.local.get(this.storage); | |
| 182 | + const screenshots = stored[this.storage] || []; | |
| 183 | + return screenshots.find(s => s.id === id); | |
| 184 | + } | |
| 185 | + | |
| 186 | + async getAll(options = {}) { | |
| 187 | + const stored = await chrome.storage.local.get(this.storage); | |
| 188 | + let screenshots = stored[this.storage] || []; | |
| 189 | + | |
| 190 | + // Apply filters | |
| 191 | + if (options.platform) { | |
| 192 | + screenshots = screenshots.filter(s => s.platform === options.platform); | |
| 193 | + } | |
| 194 | + | |
| 195 | + if (options.startDate) { | |
| 196 | + screenshots = screenshots.filter(s => | |
| 197 | + new Date(s.timestamp) >= new Date(options.startDate) | |
| 198 | + ); | |
| 199 | + } | |
| 200 | + | |
| 201 | + if (options.endDate) { | |
| 202 | + screenshots = screenshots.filter(s => | |
| 203 | + new Date(s.timestamp) <= new Date(options.endDate) | |
| 204 | + ); | |
| 205 | + } | |
| 206 | + | |
| 207 | + if (options.limit) { | |
| 208 | + screenshots = screenshots.slice(0, options.limit); | |
| 209 | + } | |
| 210 | + | |
| 211 | + return screenshots; | |
| 212 | + } | |
| 213 | + | |
| 214 | + async getMetadata() { | |
| 215 | + const metaKey = `${this.storage}_metadata`; | |
| 216 | + const stored = await chrome.storage.local.get(metaKey); | |
| 217 | + return stored[metaKey] || []; | |
| 218 | + } | |
| 219 | + | |
| 220 | + async delete(id) { | |
| 221 | + const stored = await chrome.storage.local.get(this.storage); | |
| 222 | + let screenshots = stored[this.storage] || []; | |
| 223 | + | |
| 224 | + screenshots = screenshots.filter(s => s.id !== id); | |
| 225 | + await chrome.storage.local.set({ [this.storage]: screenshots }); | |
| 226 | + | |
| 227 | + // Also remove from metadata | |
| 228 | + const metaKey = `${this.storage}_metadata`; | |
| 229 | + const metaStored = await chrome.storage.local.get(metaKey); | |
| 230 | + let metadata = metaStored[metaKey] || []; | |
| 231 | + metadata = metadata.filter(m => m.id !== id); | |
| 232 | + await chrome.storage.local.set({ [metaKey]: metadata }); | |
| 233 | + | |
| 234 | + return { success: true }; | |
| 235 | + } | |
| 236 | + | |
| 237 | + async cleanup(maxAge = 24 * 60 * 60 * 1000) { | |
| 238 | + const stored = await chrome.storage.local.get(this.storage); | |
| 239 | + let screenshots = stored[this.storage] || []; | |
| 240 | + | |
| 241 | + const cutoff = Date.now() - maxAge; | |
| 242 | + const before = screenshots.length; | |
| 243 | + | |
| 244 | + screenshots = screenshots.filter(s => | |
| 245 | + new Date(s.timestamp).getTime() > cutoff | |
| 246 | + ); | |
| 247 | + | |
| 248 | + await chrome.storage.local.set({ [this.storage]: screenshots }); | |
| 249 | + | |
| 250 | + const deleted = before - screenshots.length; | |
| 251 | + console.log(`[ScreenshotManager] Cleaned up ${deleted} old screenshots`); | |
| 252 | + | |
| 253 | + return { deleted }; | |
| 254 | + } | |
| 255 | + | |
| 256 | + async export(ids = null) { | |
| 257 | + const stored = await chrome.storage.local.get(this.storage); | |
| 258 | + let screenshots = stored[this.storage] || []; | |
| 259 | + | |
| 260 | + if (ids) { | |
| 261 | + screenshots = screenshots.filter(s => ids.includes(s.id)); | |
| 262 | + } | |
| 263 | + | |
| 264 | + // Create export data | |
| 265 | + const exportData = { | |
| 266 | + version: '1.0', | |
| 267 | + timestamp: new Date().toISOString(), | |
| 268 | + count: screenshots.length, | |
| 269 | + screenshots: screenshots.map(s => ({ | |
| 270 | + id: s.id, | |
| 271 | + timestamp: s.timestamp, | |
| 272 | + platform: s.platform, | |
| 273 | + url: s.url, | |
| 274 | + metadata: s.metadata, | |
| 275 | + thumbnail: s.thumbnail // Include thumbnail only | |
| 276 | + })) | |
| 277 | + }; | |
| 278 | + | |
| 279 | + return exportData; | |
| 280 | + } | |
| 281 | + | |
| 282 | + async getStatistics() { | |
| 283 | + const stored = await chrome.storage.local.get(this.storage); | |
| 284 | + const screenshots = stored[this.storage] || []; | |
| 285 | + | |
| 286 | + const stats = { | |
| 287 | + total: screenshots.length, | |
| 288 | + byPlatform: {}, | |
| 289 | + byDay: {}, | |
| 290 | + totalSize: 0, | |
| 291 | + oldestTimestamp: null, | |
| 292 | + newestTimestamp: null | |
| 293 | + }; | |
| 294 | + | |
| 295 | + screenshots.forEach(s => { | |
| 296 | + // By platform | |
| 297 | + stats.byPlatform[s.platform] = (stats.byPlatform[s.platform] || 0) + 1; | |
| 298 | + | |
| 299 | + // By day | |
| 300 | + const day = new Date(s.timestamp).toDateString(); | |
| 301 | + stats.byDay[day] = (stats.byDay[day] || 0) + 1; | |
| 302 | + | |
| 303 | + // Size | |
| 304 | + stats.totalSize += this.calculateSize(s.dataUrl); | |
| 305 | + | |
| 306 | + // Timestamps | |
| 307 | + const timestamp = new Date(s.timestamp).getTime(); | |
| 308 | + if (!stats.oldestTimestamp || timestamp < stats.oldestTimestamp) { | |
| 309 | + stats.oldestTimestamp = s.timestamp; | |
| 310 | + } | |
| 311 | + if (!stats.newestTimestamp || timestamp > stats.newestTimestamp) { | |
| 312 | + stats.newestTimestamp = s.timestamp; | |
| 313 | + } | |
| 314 | + }); | |
| 315 | + | |
| 316 | + return stats; | |
| 317 | + } | |
| 318 | + | |
| 319 | + notifyDashboard(screenshot) { | |
| 320 | + // Send to dashboard if open | |
| 321 | + chrome.runtime.sendMessage({ | |
| 322 | + type: 'DASHBOARD_UPDATE', | |
| 323 | + data: { | |
| 324 | + event: 'screenshot_captured', | |
| 325 | + screenshot: { | |
| 326 | + id: screenshot.id, | |
| 327 | + timestamp: screenshot.timestamp, | |
| 328 | + platform: screenshot.platform, | |
| 329 | + thumbnail: screenshot.thumbnail | |
| 330 | + } | |
| 331 | + } | |
| 332 | + }).catch(() => { | |
| 333 | + // Dashboard might not be open | |
| 334 | + }); | |
| 335 | + } | |
| 336 | + | |
| 337 | + async annotate(id, annotations) { | |
| 338 | + const screenshot = await this.get(id); | |
| 339 | + if (!screenshot) { | |
| 340 | + throw new Error('Screenshot not found'); | |
| 341 | + } | |
| 342 | + | |
| 343 | + // Add annotations | |
| 344 | + screenshot.annotations = { | |
| 345 | + ...screenshot.annotations, | |
| 346 | + ...annotations, | |
| 347 | + updatedAt: new Date().toISOString() | |
| 348 | + }; | |
| 349 | + | |
| 350 | + // Update storage | |
| 351 | + const stored = await chrome.storage.local.get(this.storage); | |
| 352 | + let screenshots = stored[this.storage] || []; | |
| 353 | + | |
| 354 | + const index = screenshots.findIndex(s => s.id === id); | |
| 355 | + if (index !== -1) { | |
| 356 | + screenshots[index] = screenshot; | |
| 357 | + await chrome.storage.local.set({ [this.storage]: screenshots }); | |
| 358 | + } | |
| 359 | + | |
| 360 | + return screenshot; | |
| 361 | + } | |
| 362 | + | |
| 363 | + async createEvidence Report(screenshotIds, conversationId) { | |
| 364 | + const screenshots = await this.getAll(); | |
| 365 | + const selected = screenshots.filter(s => screenshotIds.includes(s.id)); | |
| 366 | + | |
| 367 | + const report = { | |
| 368 | + id: `report_${Date.now()}`, | |
| 369 | + timestamp: new Date().toISOString(), | |
| 370 | + conversationId, | |
| 371 | + screenshots: selected.map(s => ({ | |
| 372 | + id: s.id, | |
| 373 | + timestamp: s.timestamp, | |
| 374 | + platform: s.platform, | |
| 375 | + url: s.url, | |
| 376 | + annotations: s.annotations | |
| 377 | + })), | |
| 378 | + summary: { | |
| 379 | + totalScreenshots: selected.length, | |
| 380 | + platforms: [...new Set(selected.map(s => s.platform))], | |
| 381 | + timeRange: { | |
| 382 | + start: selected[selected.length - 1]?.timestamp, | |
| 383 | + end: selected[0]?.timestamp | |
| 384 | + } | |
| 385 | + } | |
| 386 | + }; | |
| 387 | + | |
| 388 | + // Store report | |
| 389 | + const reportKey = 'evidence_reports'; | |
| 390 | + const stored = await chrome.storage.local.get(reportKey); | |
| 391 | + let reports = stored[reportKey] || []; | |
| 392 | + reports.unshift(report); | |
| 393 | + | |
| 394 | + // Keep only last 50 reports | |
| 395 | + if (reports.length > 50) { | |
| 396 | + reports = reports.slice(0, 50); | |
| 397 | + } | |
| 398 | + | |
| 399 | + await chrome.storage.local.set({ [reportKey]: reports }); | |
| 400 | + | |
| 401 | + return report; | |
| 402 | + } | |
| 403 | +} | |
| 404 | + | |
| 405 | +// Export for use in service worker | |
| 406 | +if (typeof module !== 'undefined' && module.exports) { | |
| 407 | + module.exports = ScreenshotManager; | |
| 408 | +} | |
extension-chrome/background/service-worker.jsadded@@ -0,0 +1,393 @@ | ||
| 1 | +// Chrome Service Worker for LooseCannon (Manifest V3) | |
| 2 | +console.log('[LooseCannon] Chrome Service Worker initialized'); | |
| 3 | + | |
| 4 | +// Import modules | |
| 5 | +import { UnifiedMessageHandler } from './unified-handler-v3.js'; | |
| 6 | +import { ScreenshotManager } from './screenshot-manager.js'; | |
| 7 | +import { PatternLearning } from './pattern-learning.js'; | |
| 8 | + | |
| 9 | +class LooseCannonServiceWorker { | |
| 10 | + constructor() { | |
| 11 | + this.messageHandler = new UnifiedMessageHandler(); | |
| 12 | + this.screenshotManager = new ScreenshotManager(); | |
| 13 | + this.patternLearning = new PatternLearning(); | |
| 14 | + this.serverUrl = 'http://localhost:8765'; | |
| 15 | + this.analytics = { | |
| 16 | + sessionsStarted: 0, | |
| 17 | + messagesProcessed: 0, | |
| 18 | + scammersDetected: 0, | |
| 19 | + screenshotsCaptured: 0 | |
| 20 | + }; | |
| 21 | + this.init(); | |
| 22 | + } | |
| 23 | + | |
| 24 | + init() { | |
| 25 | + this.setupListeners(); | |
| 26 | + this.setupAlarms(); | |
| 27 | + this.connectToServer(); | |
| 28 | + this.loadAnalytics(); | |
| 29 | + } | |
| 30 | + | |
| 31 | + setupListeners() { | |
| 32 | + // Message listener for content scripts and popup | |
| 33 | + chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { | |
| 34 | + console.log('[ServiceWorker] Received message:', message.type); | |
| 35 | + | |
| 36 | + // Handle async operations properly in Manifest V3 | |
| 37 | + this.handleMessage(message, sender).then(sendResponse); | |
| 38 | + return true; // Keep channel open for async response | |
| 39 | + }); | |
| 40 | + | |
| 41 | + // Tab events for tracking | |
| 42 | + chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { | |
| 43 | + if (changeInfo.status === 'complete') { | |
| 44 | + this.checkForSupportedSite(tab); | |
| 45 | + } | |
| 46 | + }); | |
| 47 | + | |
| 48 | + // Extension install/update events | |
| 49 | + chrome.runtime.onInstalled.addListener((details) => { | |
| 50 | + if (details.reason === 'install') { | |
| 51 | + this.onFirstInstall(); | |
| 52 | + } else if (details.reason === 'update') { | |
| 53 | + this.onUpdate(details.previousVersion); | |
| 54 | + } | |
| 55 | + }); | |
| 56 | + | |
| 57 | + // Alarm listener for periodic tasks | |
| 58 | + chrome.alarms.onAlarm.addListener((alarm) => { | |
| 59 | + this.handleAlarm(alarm); | |
| 60 | + }); | |
| 61 | + } | |
| 62 | + | |
| 63 | + async handleMessage(message, sender) { | |
| 64 | + try { | |
| 65 | + switch (message.type) { | |
| 66 | + case 'NEW_MESSAGE': | |
| 67 | + return await this.handleNewMessage(message.data, sender); | |
| 68 | + | |
| 69 | + case 'CAPTURE_SCREENSHOT': | |
| 70 | + return await this.captureScreenshot(sender.tab); | |
| 71 | + | |
| 72 | + case 'GET_ANALYTICS': | |
| 73 | + return await this.getAnalytics(); | |
| 74 | + | |
| 75 | + case 'EXPORT_DATA': | |
| 76 | + return await this.exportData(message.data); | |
| 77 | + | |
| 78 | + case 'LEARN_PATTERN': | |
| 79 | + return await this.learnPattern(message.data); | |
| 80 | + | |
| 81 | + case 'GET_SERVER_STATUS': | |
| 82 | + return await this.checkServerStatus(); | |
| 83 | + | |
| 84 | + case 'TOGGLE_ACTIVE': | |
| 85 | + return await this.handleToggleActive(message.data, sender); | |
| 86 | + | |
| 87 | + default: | |
| 88 | + return { error: 'Unknown message type' }; | |
| 89 | + } | |
| 90 | + } catch (error) { | |
| 91 | + console.error('[ServiceWorker] Error handling message:', error); | |
| 92 | + return { error: error.message }; | |
| 93 | + } | |
| 94 | + } | |
| 95 | + | |
| 96 | + async handleNewMessage(data, sender) { | |
| 97 | + this.analytics.messagesProcessed++; | |
| 98 | + | |
| 99 | + // Add Chrome-specific tab info | |
| 100 | + data.tabId = sender.tab.id; | |
| 101 | + data.url = sender.url; | |
| 102 | + | |
| 103 | + // Process through unified handler | |
| 104 | + const response = await this.messageHandler.processMessage(data); | |
| 105 | + | |
| 106 | + // If scammer detected, capture screenshot | |
| 107 | + if (response.scammerScore > 0.7) { | |
| 108 | + this.analytics.scammersDetected++; | |
| 109 | + | |
| 110 | + // Capture evidence | |
| 111 | + const screenshot = await this.captureScreenshot(sender.tab); | |
| 112 | + if (screenshot.success) { | |
| 113 | + response.evidenceId = screenshot.id; | |
| 114 | + } | |
| 115 | + | |
| 116 | + // Learn from pattern | |
| 117 | + await this.patternLearning.addPattern(data, response.scammerScore); | |
| 118 | + } | |
| 119 | + | |
| 120 | + // Update analytics | |
| 121 | + await this.saveAnalytics(); | |
| 122 | + | |
| 123 | + return response; | |
| 124 | + } | |
| 125 | + | |
| 126 | + async captureScreenshot(tab) { | |
| 127 | + if (!tab) return { success: false, error: 'No tab provided' }; | |
| 128 | + | |
| 129 | + try { | |
| 130 | + // Chrome-specific screenshot API | |
| 131 | + const dataUrl = await chrome.tabs.captureVisibleTab(tab.windowId, { | |
| 132 | + format: 'png', | |
| 133 | + quality: 90 | |
| 134 | + }); | |
| 135 | + | |
| 136 | + // Store screenshot | |
| 137 | + const screenshot = await this.screenshotManager.store({ | |
| 138 | + dataUrl, | |
| 139 | + tabId: tab.id, | |
| 140 | + url: tab.url, | |
| 141 | + timestamp: new Date().toISOString() | |
| 142 | + }); | |
| 143 | + | |
| 144 | + this.analytics.screenshotsCaptured++; | |
| 145 | + | |
| 146 | + return { | |
| 147 | + success: true, | |
| 148 | + id: screenshot.id, | |
| 149 | + size: screenshot.size | |
| 150 | + }; | |
| 151 | + } catch (error) { | |
| 152 | + console.error('[ServiceWorker] Screenshot error:', error); | |
| 153 | + return { success: false, error: error.message }; | |
| 154 | + } | |
| 155 | + } | |
| 156 | + | |
| 157 | + async checkForSupportedSite(tab) { | |
| 158 | + const supportedSites = [ | |
| 159 | + 'web.whatsapp.com', | |
| 160 | + 'web.telegram.org', | |
| 161 | + 'messenger.com' | |
| 162 | + ]; | |
| 163 | + | |
| 164 | + const isSupported = supportedSites.some(site => tab.url?.includes(site)); | |
| 165 | + | |
| 166 | + if (isSupported) { | |
| 167 | + // Show page action or badge | |
| 168 | + chrome.action.setBadgeText({ | |
| 169 | + text: 'LC', | |
| 170 | + tabId: tab.id | |
| 171 | + }); | |
| 172 | + | |
| 173 | + chrome.action.setBadgeBackgroundColor({ | |
| 174 | + color: '#4CAF50', | |
| 175 | + tabId: tab.id | |
| 176 | + }); | |
| 177 | + } | |
| 178 | + } | |
| 179 | + | |
| 180 | + setupAlarms() { | |
| 181 | + // Set up periodic tasks | |
| 182 | + chrome.alarms.create('analyticsSync', { periodInMinutes: 5 }); | |
| 183 | + chrome.alarms.create('patternSync', { periodInMinutes: 30 }); | |
| 184 | + chrome.alarms.create('cleanup', { periodInMinutes: 60 }); | |
| 185 | + } | |
| 186 | + | |
| 187 | + async handleAlarm(alarm) { | |
| 188 | + switch (alarm.name) { | |
| 189 | + case 'analyticsSync': | |
| 190 | + await this.syncAnalytics(); | |
| 191 | + break; | |
| 192 | + | |
| 193 | + case 'patternSync': | |
| 194 | + await this.syncPatterns(); | |
| 195 | + break; | |
| 196 | + | |
| 197 | + case 'cleanup': | |
| 198 | + await this.cleanup(); | |
| 199 | + break; | |
| 200 | + } | |
| 201 | + } | |
| 202 | + | |
| 203 | + async syncAnalytics() { | |
| 204 | + try { | |
| 205 | + // Send analytics to server | |
| 206 | + const response = await fetch(`${this.serverUrl}/analytics/sync`, { | |
| 207 | + method: 'POST', | |
| 208 | + headers: { 'Content-Type': 'application/json' }, | |
| 209 | + body: JSON.stringify({ | |
| 210 | + analytics: this.analytics, | |
| 211 | + timestamp: new Date().toISOString() | |
| 212 | + }) | |
| 213 | + }); | |
| 214 | + | |
| 215 | + if (response.ok) { | |
| 216 | + console.log('[ServiceWorker] Analytics synced'); | |
| 217 | + } | |
| 218 | + } catch (error) { | |
| 219 | + console.error('[ServiceWorker] Analytics sync failed:', error); | |
| 220 | + } | |
| 221 | + } | |
| 222 | + | |
| 223 | + async syncPatterns() { | |
| 224 | + try { | |
| 225 | + const patterns = await this.patternLearning.getPatterns(); | |
| 226 | + | |
| 227 | + // Send learned patterns to server | |
| 228 | + const response = await fetch(`${this.serverUrl}/patterns/sync`, { | |
| 229 | + method: 'POST', | |
| 230 | + headers: { 'Content-Type': 'application/json' }, | |
| 231 | + body: JSON.stringify({ patterns }) | |
| 232 | + }); | |
| 233 | + | |
| 234 | + if (response.ok) { | |
| 235 | + const serverPatterns = await response.json(); | |
| 236 | + await this.patternLearning.updateFromServer(serverPatterns); | |
| 237 | + console.log('[ServiceWorker] Patterns synced'); | |
| 238 | + } | |
| 239 | + } catch (error) { | |
| 240 | + console.error('[ServiceWorker] Pattern sync failed:', error); | |
| 241 | + } | |
| 242 | + } | |
| 243 | + | |
| 244 | + async cleanup() { | |
| 245 | + // Clean up old screenshots | |
| 246 | + await this.screenshotManager.cleanup(24 * 60 * 60 * 1000); // 24 hours | |
| 247 | + | |
| 248 | + // Clean up old conversation data | |
| 249 | + const storage = await chrome.storage.local.get(); | |
| 250 | + const now = Date.now(); | |
| 251 | + const maxAge = 7 * 24 * 60 * 60 * 1000; // 7 days | |
| 252 | + | |
| 253 | + for (const key in storage) { | |
| 254 | + if (key.startsWith('conversation_')) { | |
| 255 | + const data = storage[key]; | |
| 256 | + if (data.timestamp && (now - new Date(data.timestamp).getTime() > maxAge)) { | |
| 257 | + await chrome.storage.local.remove(key); | |
| 258 | + } | |
| 259 | + } | |
| 260 | + } | |
| 261 | + | |
| 262 | + console.log('[ServiceWorker] Cleanup completed'); | |
| 263 | + } | |
| 264 | + | |
| 265 | + async connectToServer() { | |
| 266 | + try { | |
| 267 | + const response = await fetch(`${this.serverUrl}/status`); | |
| 268 | + if (response.ok) { | |
| 269 | + console.log('[ServiceWorker] Server connected'); | |
| 270 | + chrome.action.setTitle({ title: 'LooseCannon - Connected' }); | |
| 271 | + } else { | |
| 272 | + throw new Error('Server not responding'); | |
| 273 | + } | |
| 274 | + } catch (error) { | |
| 275 | + console.error('[ServiceWorker] Server connection failed:', error); | |
| 276 | + chrome.action.setTitle({ title: 'LooseCannon - Server Offline' }); | |
| 277 | + | |
| 278 | + // Retry in 30 seconds | |
| 279 | + setTimeout(() => this.connectToServer(), 30000); | |
| 280 | + } | |
| 281 | + } | |
| 282 | + | |
| 283 | + async getAnalytics() { | |
| 284 | + const stored = await chrome.storage.local.get('analytics'); | |
| 285 | + return { | |
| 286 | + ...this.analytics, | |
| 287 | + ...stored.analytics, | |
| 288 | + uptime: Date.now() - this.startTime | |
| 289 | + }; | |
| 290 | + } | |
| 291 | + | |
| 292 | + async saveAnalytics() { | |
| 293 | + await chrome.storage.local.set({ analytics: this.analytics }); | |
| 294 | + } | |
| 295 | + | |
| 296 | + async loadAnalytics() { | |
| 297 | + const stored = await chrome.storage.local.get('analytics'); | |
| 298 | + if (stored.analytics) { | |
| 299 | + this.analytics = { ...this.analytics, ...stored.analytics }; | |
| 300 | + } | |
| 301 | + this.startTime = Date.now(); | |
| 302 | + } | |
| 303 | + | |
| 304 | + async exportData(options = {}) { | |
| 305 | + const data = { | |
| 306 | + version: '0.3.0', | |
| 307 | + timestamp: new Date().toISOString(), | |
| 308 | + analytics: this.analytics, | |
| 309 | + patterns: await this.patternLearning.getPatterns(), | |
| 310 | + screenshots: await this.screenshotManager.getAll() | |
| 311 | + }; | |
| 312 | + | |
| 313 | + if (options.includeConversations) { | |
| 314 | + const storage = await chrome.storage.local.get(); | |
| 315 | + data.conversations = Object.entries(storage) | |
| 316 | + .filter(([key]) => key.startsWith('conversation_')) | |
| 317 | + .map(([key, value]) => value); | |
| 318 | + } | |
| 319 | + | |
| 320 | + // Create download | |
| 321 | + const blob = new Blob([JSON.stringify(data, null, 2)], { | |
| 322 | + type: 'application/json' | |
| 323 | + }); | |
| 324 | + | |
| 325 | + const url = URL.createObjectURL(blob); | |
| 326 | + const filename = `loosecannon-export-${Date.now()}.json`; | |
| 327 | + | |
| 328 | + await chrome.downloads.download({ | |
| 329 | + url, | |
| 330 | + filename, | |
| 331 | + saveAs: true | |
| 332 | + }); | |
| 333 | + | |
| 334 | + return { success: true, filename }; | |
| 335 | + } | |
| 336 | + | |
| 337 | + async handleToggleActive(data, sender) { | |
| 338 | + this.analytics.sessionsStarted++; | |
| 339 | + await this.saveAnalytics(); | |
| 340 | + | |
| 341 | + // Notify dashboard if open | |
| 342 | + chrome.runtime.sendMessage({ | |
| 343 | + type: 'DASHBOARD_UPDATE', | |
| 344 | + data: { | |
| 345 | + event: 'session_toggle', | |
| 346 | + active: data.isActive, | |
| 347 | + platform: data.platform, | |
| 348 | + tabId: sender.tab?.id | |
| 349 | + } | |
| 350 | + }).catch(() => { | |
| 351 | + // Dashboard might not be open | |
| 352 | + }); | |
| 353 | + | |
| 354 | + return { success: true }; | |
| 355 | + } | |
| 356 | + | |
| 357 | + async checkServerStatus() { | |
| 358 | + try { | |
| 359 | + const response = await fetch(`${this.serverUrl}/status`); | |
| 360 | + const data = await response.json(); | |
| 361 | + return { connected: true, ...data }; | |
| 362 | + } catch (error) { | |
| 363 | + return { connected: false, error: error.message }; | |
| 364 | + } | |
| 365 | + } | |
| 366 | + | |
| 367 | + onFirstInstall() { | |
| 368 | + // Open welcome page | |
| 369 | + chrome.tabs.create({ | |
| 370 | + url: chrome.runtime.getURL('dashboard/welcome.html') | |
| 371 | + }); | |
| 372 | + } | |
| 373 | + | |
| 374 | + onUpdate(previousVersion) { | |
| 375 | + console.log(`[ServiceWorker] Updated from ${previousVersion} to 0.3.0`); | |
| 376 | + | |
| 377 | + // Show update notification | |
| 378 | + chrome.notifications.create({ | |
| 379 | + type: 'basic', | |
| 380 | + iconUrl: 'icons/icon-128.png', | |
| 381 | + title: 'LooseCannon Updated', | |
| 382 | + message: 'New features: Chrome support, Analytics Dashboard, and more!' | |
| 383 | + }); | |
| 384 | + } | |
| 385 | +} | |
| 386 | + | |
| 387 | +// Initialize service worker | |
| 388 | +const serviceWorker = new LooseCannonServiceWorker(); | |
| 389 | + | |
| 390 | +// Export for testing | |
| 391 | +if (typeof module !== 'undefined' && module.exports) { | |
| 392 | + module.exports = LooseCannonServiceWorker; | |
| 393 | +} | |
extension-chrome/background/unified-handler-v3.jsadded@@ -0,0 +1,293 @@ | ||
| 1 | +// Unified Message Handler for Chrome Manifest V3 | |
| 2 | +// Handles messages from all supported platforms | |
| 3 | + | |
| 4 | +export class UnifiedMessageHandler { | |
| 5 | + constructor() { | |
| 6 | + this.serverUrl = 'http://localhost:8765'; | |
| 7 | + this.activeConversations = new Map(); | |
| 8 | + this.responseQueue = []; | |
| 9 | + this.isProcessing = false; | |
| 10 | + this.settings = { | |
| 11 | + autoActivateOnScammer: true, | |
| 12 | + scammerThreshold: 0.7, | |
| 13 | + maxResponseDelay: 15000, | |
| 14 | + minResponseDelay: 2000, | |
| 15 | + personalityRotation: false | |
| 16 | + }; | |
| 17 | + } | |
| 18 | + | |
| 19 | + async processMessage(data) { | |
| 20 | + try { | |
| 21 | + // Add to conversation on server | |
| 22 | + const contextResponse = await fetch(`${this.serverUrl}/conversation/add`, { | |
| 23 | + method: 'POST', | |
| 24 | + headers: { 'Content-Type': 'application/json' }, | |
| 25 | + body: JSON.stringify({ | |
| 26 | + chatId: data.chatId, | |
| 27 | + platform: data.platform || this.detectPlatform(data.url), | |
| 28 | + message: data | |
| 29 | + }) | |
| 30 | + }); | |
| 31 | + | |
| 32 | + const context = await contextResponse.json(); | |
| 33 | + | |
| 34 | + // Check for auto-activation | |
| 35 | + if (this.settings.autoActivateOnScammer && | |
| 36 | + context.scammerScore > this.settings.scammerThreshold && | |
| 37 | + !this.isConversationActive(data.chatId, data.platform)) { | |
| 38 | + | |
| 39 | + this.activateConversation(data.chatId, data.platform, data.tabId); | |
| 40 | + | |
| 41 | + // Notify content script | |
| 42 | + chrome.tabs.sendMessage(data.tabId, { | |
| 43 | + type: 'SCAMMER_DETECTED', | |
| 44 | + data: { | |
| 45 | + score: context.scammerScore, | |
| 46 | + autoActivated: true | |
| 47 | + } | |
| 48 | + }); | |
| 49 | + } | |
| 50 | + | |
| 51 | + // Only generate response if active | |
| 52 | + if (!this.isConversationActive(data.chatId, data.platform)) { | |
| 53 | + return { | |
| 54 | + processed: false, | |
| 55 | + reason: 'Conversation not active', | |
| 56 | + scammerScore: context.scammerScore | |
| 57 | + }; | |
| 58 | + } | |
| 59 | + | |
| 60 | + // Get response suggestions | |
| 61 | + const suggestionsResponse = await fetch(`${this.serverUrl}/suggestions`, { | |
| 62 | + method: 'POST', | |
| 63 | + headers: { 'Content-Type': 'application/json' }, | |
| 64 | + body: JSON.stringify({ | |
| 65 | + chatId: data.chatId, | |
| 66 | + platform: data.platform, | |
| 67 | + context | |
| 68 | + }) | |
| 69 | + }); | |
| 70 | + | |
| 71 | + const suggestions = await suggestionsResponse.json(); | |
| 72 | + | |
| 73 | + // Select personality | |
| 74 | + const personality = await this.selectPersonality(data.chatId, context); | |
| 75 | + | |
| 76 | + // Generate response | |
| 77 | + const generateResponse = await fetch(`${this.serverUrl}/generate`, { | |
| 78 | + method: 'POST', | |
| 79 | + headers: { 'Content-Type': 'application/json' }, | |
| 80 | + body: JSON.stringify({ | |
| 81 | + message: data.content || data.text, | |
| 82 | + personality, | |
| 83 | + chatId: data.chatId, | |
| 84 | + platform: data.platform || this.detectPlatform(data.url), | |
| 85 | + context, | |
| 86 | + suggestions, | |
| 87 | + timestamp: data.timestamp | |
| 88 | + }) | |
| 89 | + }); | |
| 90 | + | |
| 91 | + const result = await generateResponse.json(); | |
| 92 | + | |
| 93 | + // Calculate delay | |
| 94 | + const delay = this.calculateResponseDelay(result.reply, context); | |
| 95 | + | |
| 96 | + // Queue response | |
| 97 | + await this.queueResponse({ | |
| 98 | + tabId: data.tabId, | |
| 99 | + platform: data.platform || this.detectPlatform(data.url), | |
| 100 | + chatId: data.chatId, | |
| 101 | + reply: result.reply, | |
| 102 | + delay, | |
| 103 | + personality | |
| 104 | + }); | |
| 105 | + | |
| 106 | + return { | |
| 107 | + queued: true, | |
| 108 | + reply: result.reply, | |
| 109 | + delay, | |
| 110 | + personality, | |
| 111 | + scammerScore: context.scammerScore | |
| 112 | + }; | |
| 113 | + | |
| 114 | + } catch (error) { | |
| 115 | + console.error('[UnifiedHandler] Error:', error); | |
| 116 | + | |
| 117 | + const fallback = this.getFallbackResponse(data.platform); | |
| 118 | + return { | |
| 119 | + reply: fallback, | |
| 120 | + delay: 3000, | |
| 121 | + error: error.message | |
| 122 | + }; | |
| 123 | + } | |
| 124 | + } | |
| 125 | + | |
| 126 | + detectPlatform(url) { | |
| 127 | + if (!url) return 'unknown'; | |
| 128 | + | |
| 129 | + if (url.includes('whatsapp.com')) return 'whatsapp'; | |
| 130 | + if (url.includes('telegram.org')) return 'telegram'; | |
| 131 | + if (url.includes('messenger.com')) return 'messenger'; | |
| 132 | + | |
| 133 | + return 'unknown'; | |
| 134 | + } | |
| 135 | + | |
| 136 | + async selectPersonality(chatId, context) { | |
| 137 | + if (!this.settings.personalityRotation) { | |
| 138 | + return context.personality || 'default'; | |
| 139 | + } | |
| 140 | + | |
| 141 | + // Load available personalities | |
| 142 | + const stored = await chrome.storage.local.get('personalities'); | |
| 143 | + const personalities = stored.personalities || ['default']; | |
| 144 | + | |
| 145 | + // Rotate based on message count | |
| 146 | + const messageCount = context.messageCount || 0; | |
| 147 | + const index = Math.floor(messageCount / 5) % personalities.length; | |
| 148 | + | |
| 149 | + return personalities[index]; | |
| 150 | + } | |
| 151 | + | |
| 152 | + calculateResponseDelay(text, context) { | |
| 153 | + const wordCount = text.split(' ').length; | |
| 154 | + const baseDelay = this.settings.minResponseDelay; | |
| 155 | + const perWordDelay = 150; | |
| 156 | + | |
| 157 | + let delay = baseDelay + (wordCount * perWordDelay); | |
| 158 | + | |
| 159 | + // Adjust based on context | |
| 160 | + if (context.conversationTone === 'urgent') { | |
| 161 | + delay *= 1.5; // Be slower for urgent scammers | |
| 162 | + } | |
| 163 | + | |
| 164 | + // Add random variation | |
| 165 | + const variation = 0.3; | |
| 166 | + const randomFactor = 1 + (Math.random() * 2 * variation - variation); | |
| 167 | + delay *= randomFactor; | |
| 168 | + | |
| 169 | + return Math.min(delay, this.settings.maxResponseDelay); | |
| 170 | + } | |
| 171 | + | |
| 172 | + async queueResponse(response) { | |
| 173 | + this.responseQueue.push({ | |
| 174 | + ...response, | |
| 175 | + queuedAt: Date.now() | |
| 176 | + }); | |
| 177 | + | |
| 178 | + if (!this.isProcessing) { | |
| 179 | + this.processQueue(); | |
| 180 | + } | |
| 181 | + } | |
| 182 | + | |
| 183 | + async processQueue() { | |
| 184 | + if (this.responseQueue.length === 0) { | |
| 185 | + this.isProcessing = false; | |
| 186 | + return; | |
| 187 | + } | |
| 188 | + | |
| 189 | + this.isProcessing = true; | |
| 190 | + const response = this.responseQueue.shift(); | |
| 191 | + | |
| 192 | + // Calculate remaining delay | |
| 193 | + const elapsed = Date.now() - response.queuedAt; | |
| 194 | + const remainingDelay = Math.max(0, response.delay - elapsed); | |
| 195 | + | |
| 196 | + // Wait | |
| 197 | + await this.sleep(remainingDelay); | |
| 198 | + | |
| 199 | + // Send response | |
| 200 | + try { | |
| 201 | + await chrome.tabs.sendMessage(response.tabId, { | |
| 202 | + type: 'SEND_MESSAGE', | |
| 203 | + data: { | |
| 204 | + text: response.reply, | |
| 205 | + delay: 0 | |
| 206 | + } | |
| 207 | + }); | |
| 208 | + | |
| 209 | + console.log(`[UnifiedHandler] Sent response to ${response.platform}:${response.chatId}`); | |
| 210 | + } catch (error) { | |
| 211 | + console.error('[UnifiedHandler] Failed to send response:', error); | |
| 212 | + } | |
| 213 | + | |
| 214 | + // Process next | |
| 215 | + this.processQueue(); | |
| 216 | + } | |
| 217 | + | |
| 218 | + activateConversation(chatId, platform, tabId) { | |
| 219 | + const key = `${platform}:${chatId}`; | |
| 220 | + this.activeConversations.set(key, { | |
| 221 | + chatId, | |
| 222 | + platform, | |
| 223 | + tabId, | |
| 224 | + activatedAt: Date.now(), | |
| 225 | + messageCount: 0 | |
| 226 | + }); | |
| 227 | + | |
| 228 | + console.log(`[UnifiedHandler] Activated ${key}`); | |
| 229 | + } | |
| 230 | + | |
| 231 | + deactivateConversation(chatId, platform) { | |
| 232 | + const key = `${platform}:${chatId}`; | |
| 233 | + this.activeConversations.delete(key); | |
| 234 | + console.log(`[UnifiedHandler] Deactivated ${key}`); | |
| 235 | + } | |
| 236 | + | |
| 237 | + isConversationActive(chatId, platform) { | |
| 238 | + const key = `${platform}:${chatId}`; | |
| 239 | + return this.activeConversations.has(key); | |
| 240 | + } | |
| 241 | + | |
| 242 | + getFallbackResponse(platform) { | |
| 243 | + const fallbacks = { | |
| 244 | + whatsapp: [ | |
| 245 | + "Sorry, what was that? My WhatsApp is acting strange.", | |
| 246 | + "Can you repeat? The message came through garbled.", | |
| 247 | + "Hold on, my phone is being slow..." | |
| 248 | + ], | |
| 249 | + telegram: [ | |
| 250 | + "Telegram is glitching for me, one second...", | |
| 251 | + "Strange, I'm getting errors. What did you say?", | |
| 252 | + "My Telegram is updating, please wait..." | |
| 253 | + ], | |
| 254 | + messenger: [ | |
| 255 | + "Facebook is being weird, can you resend?", | |
| 256 | + "Messenger crashed, what were you saying?", | |
| 257 | + "Sorry, Facebook is slow today..." | |
| 258 | + ], | |
| 259 | + default: [ | |
| 260 | + "I didn't catch that, can you repeat?", | |
| 261 | + "Sorry, technical difficulties...", | |
| 262 | + "One moment please..." | |
| 263 | + ] | |
| 264 | + }; | |
| 265 | + | |
| 266 | + const platformFallbacks = fallbacks[platform] || fallbacks.default; | |
| 267 | + return platformFallbacks[Math.floor(Math.random() * platformFallbacks.length)]; | |
| 268 | + } | |
| 269 | + | |
| 270 | + async getStatistics() { | |
| 271 | + const stats = { | |
| 272 | + activeConversations: this.activeConversations.size, | |
| 273 | + queuedResponses: this.responseQueue.length, | |
| 274 | + platformBreakdown: {} | |
| 275 | + }; | |
| 276 | + | |
| 277 | + this.activeConversations.forEach(conv => { | |
| 278 | + stats.platformBreakdown[conv.platform] = | |
| 279 | + (stats.platformBreakdown[conv.platform] || 0) + 1; | |
| 280 | + }); | |
| 281 | + | |
| 282 | + return stats; | |
| 283 | + } | |
| 284 | + | |
| 285 | + sleep(ms) { | |
| 286 | + return new Promise(resolve => setTimeout(resolve, ms)); | |
| 287 | + } | |
| 288 | +} | |
| 289 | + | |
| 290 | +// Export for use in service worker | |
| 291 | +if (typeof module !== 'undefined' && module.exports) { | |
| 292 | + module.exports = UnifiedMessageHandler; | |
| 293 | +} | |
extension-chrome/manifest.jsonadded@@ -0,0 +1,80 @@ | ||
| 1 | +{ | |
| 2 | + "manifest_version": 3, | |
| 3 | + "name": "LooseCannon", | |
| 4 | + "version": "0.3.0", | |
| 5 | + "description": "Automated scambaiting assistant - Now with Chrome support", | |
| 6 | + | |
| 7 | + "permissions": [ | |
| 8 | + "storage", | |
| 9 | + "tabs", | |
| 10 | + "scripting", | |
| 11 | + "webRequest", | |
| 12 | + "declarativeNetRequest" | |
| 13 | + ], | |
| 14 | + | |
| 15 | + "host_permissions": [ | |
| 16 | + "*://web.whatsapp.com/*", | |
| 17 | + "*://web.telegram.org/*", | |
| 18 | + "*://webk.telegram.org/*", | |
| 19 | + "*://webz.telegram.org/*", | |
| 20 | + "*://www.messenger.com/*", | |
| 21 | + "*://messenger.com/*", | |
| 22 | + "http://localhost:8765/*" | |
| 23 | + ], | |
| 24 | + | |
| 25 | + "background": { | |
| 26 | + "service_worker": "background/service-worker.js", | |
| 27 | + "type": "module" | |
| 28 | + }, | |
| 29 | + | |
| 30 | + "content_scripts": [ | |
| 31 | + { | |
| 32 | + "matches": ["*://web.whatsapp.com/*"], | |
| 33 | + "js": ["content-scripts/whatsapp-enhanced.js"], | |
| 34 | + "css": ["content-scripts/whatsapp.css"], | |
| 35 | + "run_at": "document_idle" | |
| 36 | + }, | |
| 37 | + { | |
| 38 | + "matches": [ | |
| 39 | + "*://web.telegram.org/*", | |
| 40 | + "*://webk.telegram.org/*", | |
| 41 | + "*://webz.telegram.org/*" | |
| 42 | + ], | |
| 43 | + "js": ["content-scripts/telegram.js"], | |
| 44 | + "run_at": "document_idle" | |
| 45 | + }, | |
| 46 | + { | |
| 47 | + "matches": [ | |
| 48 | + "*://www.messenger.com/*", | |
| 49 | + "*://messenger.com/*" | |
| 50 | + ], | |
| 51 | + "js": ["content-scripts/messenger.js"], | |
| 52 | + "run_at": "document_idle" | |
| 53 | + } | |
| 54 | + ], | |
| 55 | + | |
| 56 | + "action": { | |
| 57 | + "default_popup": "popup/popup.html", | |
| 58 | + "default_icon": { | |
| 59 | + "16": "icons/icon-16.png", | |
| 60 | + "48": "icons/icon-48.png", | |
| 61 | + "128": "icons/icon-128.png" | |
| 62 | + } | |
| 63 | + }, | |
| 64 | + | |
| 65 | + "icons": { | |
| 66 | + "16": "icons/icon-16.png", | |
| 67 | + "48": "icons/icon-48.png", | |
| 68 | + "128": "icons/icon-128.png" | |
| 69 | + }, | |
| 70 | + | |
| 71 | + "web_accessible_resources": [ | |
| 72 | + { | |
| 73 | + "resources": [ | |
| 74 | + "icons/*", | |
| 75 | + "content-scripts/*.css" | |
| 76 | + ], | |
| 77 | + "matches": ["<all_urls>"] | |
| 78 | + } | |
| 79 | + ] | |
| 80 | +} | |