enhancements, extensions
- SHA
3bb0bcaba449768e2851ada5305bde6ad6815440- Parents
-
cb9ab42 - Tree
1e81d5f
3bb0bca
3bb0bcaba449768e2851ada5305bde6ad6815440cb9ab42
1e81d5f| Status | File | + | - |
|---|---|---|---|
| A |
extension/background/unified-handler.js
|
474 | 0 |
| A |
extension/content-scripts/messenger.js
|
539 | 0 |
| A |
extension/content-scripts/telegram.js
|
480 | 0 |
| A |
extension/content-scripts/whatsapp-enhanced.js
|
641 | 0 |
| A |
server/src/conversation-manager.js
|
387 | 0 |
| A |
server/src/index-enhanced.js
|
397 | 0 |
extension/background/unified-handler.jsadded@@ -0,0 +1,474 @@ | ||
| 1 | +// Unified Message Handler for Multiple Platforms | |
| 2 | +// Handles messages from WhatsApp, Telegram, and Facebook Messenger | |
| 3 | + | |
| 4 | +class UnifiedMessageHandler { | |
| 5 | + constructor() { | |
| 6 | + this.serverUrl = 'http://localhost:8765'; | |
| 7 | + this.platforms = new Map(); | |
| 8 | + this.activeConversations = new Map(); | |
| 9 | + this.responseQueue = []; | |
| 10 | + this.isProcessing = false; | |
| 11 | + this.settings = { | |
| 12 | + autoActivateOnScammer: true, | |
| 13 | + scammerThreshold: 0.7, | |
| 14 | + maxResponseDelay: 15000, | |
| 15 | + minResponseDelay: 2000, | |
| 16 | + personalityRotation: false | |
| 17 | + }; | |
| 18 | + this.init(); | |
| 19 | + } | |
| 20 | + | |
| 21 | + init() { | |
| 22 | + this.loadSettings(); | |
| 23 | + this.setupMessageListeners(); | |
| 24 | + this.startResponseProcessor(); | |
| 25 | + this.registerPlatforms(); | |
| 26 | + } | |
| 27 | + | |
| 28 | + registerPlatforms() { | |
| 29 | + // Register platform-specific configurations | |
| 30 | + this.platforms.set('whatsapp', { | |
| 31 | + name: 'WhatsApp Web', | |
| 32 | + domain: 'web.whatsapp.com', | |
| 33 | + contentScript: 'whatsapp-enhanced.js', | |
| 34 | + selectors: { | |
| 35 | + input: '[data-testid="conversation-compose-box-input"]', | |
| 36 | + sendButton: '[data-testid="compose-btn-send"]', | |
| 37 | + messageContainer: '[data-testid^="msg-"]' | |
| 38 | + } | |
| 39 | + }); | |
| 40 | + | |
| 41 | + this.platforms.set('telegram', { | |
| 42 | + name: 'Telegram Web', | |
| 43 | + domain: 'web.telegram.org', | |
| 44 | + contentScript: 'telegram.js', | |
| 45 | + selectors: { | |
| 46 | + input: '.composer-input-field', | |
| 47 | + sendButton: '.btn-send', | |
| 48 | + messageContainer: '.message' | |
| 49 | + } | |
| 50 | + }); | |
| 51 | + | |
| 52 | + this.platforms.set('messenger', { | |
| 53 | + name: 'Facebook Messenger', | |
| 54 | + domain: 'messenger.com', | |
| 55 | + contentScript: 'messenger.js', | |
| 56 | + selectors: { | |
| 57 | + input: '[role="textbox"]', | |
| 58 | + sendButton: '[aria-label="Send"]', | |
| 59 | + messageContainer: '[role="row"]' | |
| 60 | + } | |
| 61 | + }); | |
| 62 | + } | |
| 63 | + | |
| 64 | + setupMessageListeners() { | |
| 65 | + browser.runtime.onMessage.addListener((message, sender, sendResponse) => { | |
| 66 | + // Identify platform from sender URL | |
| 67 | + const platform = this.identifyPlatform(sender.url); | |
| 68 | + | |
| 69 | + console.log(`[UnifiedHandler] Message from ${platform}:`, message.type); | |
| 70 | + | |
| 71 | + switch (message.type) { | |
| 72 | + case 'NEW_MESSAGE': | |
| 73 | + this.handleNewMessage(message.data, platform, sender.tab.id) | |
| 74 | + .then(sendResponse) | |
| 75 | + .catch(error => { | |
| 76 | + console.error('Error handling message:', error); | |
| 77 | + sendResponse({ error: error.message }); | |
| 78 | + }); | |
| 79 | + return true; // Keep channel open for async response | |
| 80 | + | |
| 81 | + case 'TOGGLE_ACTIVE': | |
| 82 | + this.handleToggleActive(message.data, platform, sender.tab.id); | |
| 83 | + sendResponse({ success: true }); | |
| 84 | + break; | |
| 85 | + | |
| 86 | + case 'GET_PLATFORM_STATUS': | |
| 87 | + sendResponse({ | |
| 88 | + platform, | |
| 89 | + registered: this.platforms.has(platform), | |
| 90 | + active: this.isConversationActive(message.data.chatId, platform) | |
| 91 | + }); | |
| 92 | + break; | |
| 93 | + | |
| 94 | + case 'EXPORT_CONVERSATION': | |
| 95 | + this.exportConversation(message.data.chatId, platform) | |
| 96 | + .then(sendResponse) | |
| 97 | + .catch(error => sendResponse({ error: error.message })); | |
| 98 | + return true; | |
| 99 | + | |
| 100 | + case 'GET_STATISTICS': | |
| 101 | + this.getStatistics() | |
| 102 | + .then(sendResponse) | |
| 103 | + .catch(error => sendResponse({ error: error.message })); | |
| 104 | + return true; | |
| 105 | + | |
| 106 | + default: | |
| 107 | + // Pass through to original background handler | |
| 108 | + return false; | |
| 109 | + } | |
| 110 | + }); | |
| 111 | + } | |
| 112 | + | |
| 113 | + identifyPlatform(url) { | |
| 114 | + if (!url) return 'unknown'; | |
| 115 | + | |
| 116 | + for (const [platformId, config] of this.platforms) { | |
| 117 | + if (url.includes(config.domain)) { | |
| 118 | + return platformId; | |
| 119 | + } | |
| 120 | + } | |
| 121 | + | |
| 122 | + return 'unknown'; | |
| 123 | + } | |
| 124 | + | |
| 125 | + async handleNewMessage(data, platform, tabId) { | |
| 126 | + try { | |
| 127 | + // Add to conversation manager on server | |
| 128 | + const contextResponse = await fetch(`${this.serverUrl}/conversation/add`, { | |
| 129 | + method: 'POST', | |
| 130 | + headers: { 'Content-Type': 'application/json' }, | |
| 131 | + body: JSON.stringify({ | |
| 132 | + chatId: data.chatId, | |
| 133 | + platform, | |
| 134 | + message: data | |
| 135 | + }) | |
| 136 | + }); | |
| 137 | + | |
| 138 | + const context = await contextResponse.json(); | |
| 139 | + | |
| 140 | + // Check if we should auto-activate | |
| 141 | + if (this.settings.autoActivateOnScammer && | |
| 142 | + context.scammerScore > this.settings.scammerThreshold && | |
| 143 | + !this.isConversationActive(data.chatId, platform)) { | |
| 144 | + | |
| 145 | + console.log(`[UnifiedHandler] Auto-activating for suspected scammer (score: ${context.scammerScore})`); | |
| 146 | + this.activateConversation(data.chatId, platform, tabId); | |
| 147 | + | |
| 148 | + // Notify content script | |
| 149 | + browser.tabs.sendMessage(tabId, { | |
| 150 | + type: 'SCAMMER_DETECTED', | |
| 151 | + data: { | |
| 152 | + score: context.scammerScore, | |
| 153 | + autoActivated: true | |
| 154 | + } | |
| 155 | + }); | |
| 156 | + } | |
| 157 | + | |
| 158 | + // Only generate response if conversation is active | |
| 159 | + if (!this.isConversationActive(data.chatId, platform)) { | |
| 160 | + return { processed: false, reason: 'Conversation not active' }; | |
| 161 | + } | |
| 162 | + | |
| 163 | + // Get response suggestions from server | |
| 164 | + const suggestionsResponse = await fetch(`${this.serverUrl}/suggestions`, { | |
| 165 | + method: 'POST', | |
| 166 | + headers: { 'Content-Type': 'application/json' }, | |
| 167 | + body: JSON.stringify({ | |
| 168 | + chatId: data.chatId, | |
| 169 | + platform, | |
| 170 | + context | |
| 171 | + }) | |
| 172 | + }); | |
| 173 | + | |
| 174 | + const suggestions = await suggestionsResponse.json(); | |
| 175 | + | |
| 176 | + // Select personality (rotate if enabled) | |
| 177 | + const personality = this.selectPersonality(data.chatId, context); | |
| 178 | + | |
| 179 | + // Generate response | |
| 180 | + const generateResponse = await fetch(`${this.serverUrl}/generate`, { | |
| 181 | + method: 'POST', | |
| 182 | + headers: { 'Content-Type': 'application/json' }, | |
| 183 | + body: JSON.stringify({ | |
| 184 | + message: data.content || data.text, | |
| 185 | + personality, | |
| 186 | + chatId: data.chatId, | |
| 187 | + platform, | |
| 188 | + context, | |
| 189 | + suggestions, | |
| 190 | + timestamp: data.timestamp | |
| 191 | + }) | |
| 192 | + }); | |
| 193 | + | |
| 194 | + const result = await generateResponse.json(); | |
| 195 | + | |
| 196 | + // Calculate human-like delay | |
| 197 | + const delay = this.calculateResponseDelay(result.reply, context); | |
| 198 | + | |
| 199 | + // Add to response queue | |
| 200 | + this.queueResponse({ | |
| 201 | + tabId, | |
| 202 | + platform, | |
| 203 | + chatId: data.chatId, | |
| 204 | + reply: result.reply, | |
| 205 | + delay, | |
| 206 | + personality | |
| 207 | + }); | |
| 208 | + | |
| 209 | + return { | |
| 210 | + queued: true, | |
| 211 | + reply: result.reply, | |
| 212 | + delay, | |
| 213 | + personality | |
| 214 | + }; | |
| 215 | + | |
| 216 | + } catch (error) { | |
| 217 | + console.error('[UnifiedHandler] Error:', error); | |
| 218 | + | |
| 219 | + // Use fallback response | |
| 220 | + const fallback = this.getFallbackResponse(platform); | |
| 221 | + return { | |
| 222 | + reply: fallback, | |
| 223 | + delay: 3000, | |
| 224 | + error: error.message | |
| 225 | + }; | |
| 226 | + } | |
| 227 | + } | |
| 228 | + | |
| 229 | + selectPersonality(chatId, context) { | |
| 230 | + if (!this.settings.personalityRotation) { | |
| 231 | + return context.personality || 'default'; | |
| 232 | + } | |
| 233 | + | |
| 234 | + // Rotate personalities based on conversation state | |
| 235 | + const personalities = ['confused-elder', 'tech-support-nightmare', 'conspiracy-theorist']; | |
| 236 | + const messageCount = context.messageCount || 0; | |
| 237 | + | |
| 238 | + // Change personality every 5 messages | |
| 239 | + const index = Math.floor(messageCount / 5) % personalities.length; | |
| 240 | + return personalities[index]; | |
| 241 | + } | |
| 242 | + | |
| 243 | + calculateResponseDelay(text, context) { | |
| 244 | + // Base delay on text length (typing speed) | |
| 245 | + const wordCount = text.split(' ').length; | |
| 246 | + const baseDelay = this.settings.minResponseDelay; | |
| 247 | + const perWordDelay = 150; // 150ms per word (average typing speed) | |
| 248 | + | |
| 249 | + let delay = baseDelay + (wordCount * perWordDelay); | |
| 250 | + | |
| 251 | + // Add variation based on context | |
| 252 | + if (context.conversationTone === 'urgent') { | |
| 253 | + // Take longer to respond to urgent messages (frustrate scammers) | |
| 254 | + delay *= 1.5; | |
| 255 | + } | |
| 256 | + | |
| 257 | + // Add random variation (±30%) | |
| 258 | + const variation = 0.3; | |
| 259 | + const randomFactor = 1 + (Math.random() * 2 * variation - variation); | |
| 260 | + delay *= randomFactor; | |
| 261 | + | |
| 262 | + // Cap at max delay | |
| 263 | + return Math.min(delay, this.settings.maxResponseDelay); | |
| 264 | + } | |
| 265 | + | |
| 266 | + queueResponse(response) { | |
| 267 | + this.responseQueue.push({ | |
| 268 | + ...response, | |
| 269 | + queuedAt: Date.now() | |
| 270 | + }); | |
| 271 | + | |
| 272 | + // Start processor if not already running | |
| 273 | + if (!this.isProcessing) { | |
| 274 | + this.processResponseQueue(); | |
| 275 | + } | |
| 276 | + } | |
| 277 | + | |
| 278 | + async processResponseQueue() { | |
| 279 | + if (this.responseQueue.length === 0) { | |
| 280 | + this.isProcessing = false; | |
| 281 | + return; | |
| 282 | + } | |
| 283 | + | |
| 284 | + this.isProcessing = true; | |
| 285 | + const response = this.responseQueue.shift(); | |
| 286 | + | |
| 287 | + // Wait for the calculated delay | |
| 288 | + const elapsed = Date.now() - response.queuedAt; | |
| 289 | + const remainingDelay = Math.max(0, response.delay - elapsed); | |
| 290 | + | |
| 291 | + await this.sleep(remainingDelay); | |
| 292 | + | |
| 293 | + // Send the response to the content script | |
| 294 | + try { | |
| 295 | + await browser.tabs.sendMessage(response.tabId, { | |
| 296 | + type: 'SEND_MESSAGE', | |
| 297 | + data: { | |
| 298 | + text: response.reply, | |
| 299 | + delay: 0 // No additional delay, we've already waited | |
| 300 | + } | |
| 301 | + }); | |
| 302 | + | |
| 303 | + console.log(`[UnifiedHandler] Sent response to ${response.platform}:${response.chatId}`); | |
| 304 | + } catch (error) { | |
| 305 | + console.error('[UnifiedHandler] Failed to send response:', error); | |
| 306 | + } | |
| 307 | + | |
| 308 | + // Process next in queue | |
| 309 | + this.processResponseQueue(); | |
| 310 | + } | |
| 311 | + | |
| 312 | + startResponseProcessor() { | |
| 313 | + // Periodic check for stuck responses | |
| 314 | + setInterval(() => { | |
| 315 | + if (!this.isProcessing && this.responseQueue.length > 0) { | |
| 316 | + console.log('[UnifiedHandler] Restarting response processor'); | |
| 317 | + this.processResponseQueue(); | |
| 318 | + } | |
| 319 | + }, 5000); | |
| 320 | + } | |
| 321 | + | |
| 322 | + handleToggleActive(data, platform, tabId) { | |
| 323 | + const key = `${platform}:${data.chatId}`; | |
| 324 | + | |
| 325 | + if (data.isActive) { | |
| 326 | + this.activateConversation(data.chatId, platform, tabId); | |
| 327 | + } else { | |
| 328 | + this.deactivateConversation(data.chatId, platform); | |
| 329 | + } | |
| 330 | + } | |
| 331 | + | |
| 332 | + activateConversation(chatId, platform, tabId) { | |
| 333 | + const key = `${platform}:${chatId}`; | |
| 334 | + this.activeConversations.set(key, { | |
| 335 | + chatId, | |
| 336 | + platform, | |
| 337 | + tabId, | |
| 338 | + activatedAt: Date.now(), | |
| 339 | + messageCount: 0 | |
| 340 | + }); | |
| 341 | + | |
| 342 | + console.log(`[UnifiedHandler] Activated ${key}`); | |
| 343 | + } | |
| 344 | + | |
| 345 | + deactivateConversation(chatId, platform) { | |
| 346 | + const key = `${platform}:${chatId}`; | |
| 347 | + this.activeConversations.delete(key); | |
| 348 | + | |
| 349 | + console.log(`[UnifiedHandler] Deactivated ${key}`); | |
| 350 | + } | |
| 351 | + | |
| 352 | + isConversationActive(chatId, platform) { | |
| 353 | + const key = `${platform}:${chatId}`; | |
| 354 | + return this.activeConversations.has(key); | |
| 355 | + } | |
| 356 | + | |
| 357 | + getFallbackResponse(platform) { | |
| 358 | + const fallbacks = { | |
| 359 | + whatsapp: [ | |
| 360 | + "Sorry, what was that? My WhatsApp is acting strange.", | |
| 361 | + "Can you repeat? The message came through garbled.", | |
| 362 | + "Hold on, my phone is being slow..." | |
| 363 | + ], | |
| 364 | + telegram: [ | |
| 365 | + "Telegram is glitching for me, one second...", | |
| 366 | + "Strange, I'm getting errors. What did you say?", | |
| 367 | + "My Telegram is updating, please wait..." | |
| 368 | + ], | |
| 369 | + messenger: [ | |
| 370 | + "Facebook is being weird, can you resend?", | |
| 371 | + "Messenger crashed, what were you saying?", | |
| 372 | + "Sorry, Facebook is slow today..." | |
| 373 | + ], | |
| 374 | + default: [ | |
| 375 | + "I didn't catch that, can you repeat?", | |
| 376 | + "Sorry, technical difficulties...", | |
| 377 | + "One moment please..." | |
| 378 | + ] | |
| 379 | + }; | |
| 380 | + | |
| 381 | + const platformFallbacks = fallbacks[platform] || fallbacks.default; | |
| 382 | + return platformFallbacks[Math.floor(Math.random() * platformFallbacks.length)]; | |
| 383 | + } | |
| 384 | + | |
| 385 | + async exportConversation(chatId, platform) { | |
| 386 | + try { | |
| 387 | + const response = await fetch(`${this.serverUrl}/conversation/export`, { | |
| 388 | + method: 'POST', | |
| 389 | + headers: { 'Content-Type': 'application/json' }, | |
| 390 | + body: JSON.stringify({ chatId, platform }) | |
| 391 | + }); | |
| 392 | + | |
| 393 | + const data = await response.json(); | |
| 394 | + | |
| 395 | + // Save to browser storage for user access | |
| 396 | + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); | |
| 397 | + const filename = `loosecannon-${platform}-${chatId}-${timestamp}.json`; | |
| 398 | + | |
| 399 | + await browser.storage.local.set({ | |
| 400 | + [`export_${filename}`]: data | |
| 401 | + }); | |
| 402 | + | |
| 403 | + return { | |
| 404 | + success: true, | |
| 405 | + filename, | |
| 406 | + data | |
| 407 | + }; | |
| 408 | + } catch (error) { | |
| 409 | + console.error('[UnifiedHandler] Export error:', error); | |
| 410 | + throw error; | |
| 411 | + } | |
| 412 | + } | |
| 413 | + | |
| 414 | + async getStatistics() { | |
| 415 | + try { | |
| 416 | + const response = await fetch(`${this.serverUrl}/statistics`); | |
| 417 | + const serverStats = await response.json(); | |
| 418 | + | |
| 419 | + // Add local statistics | |
| 420 | + const localStats = { | |
| 421 | + activeConversations: this.activeConversations.size, | |
| 422 | + queuedResponses: this.responseQueue.length, | |
| 423 | + platformBreakdown: {} | |
| 424 | + }; | |
| 425 | + | |
| 426 | + // Count active conversations by platform | |
| 427 | + this.activeConversations.forEach(conv => { | |
| 428 | + localStats.platformBreakdown[conv.platform] = | |
| 429 | + (localStats.platformBreakdown[conv.platform] || 0) + 1; | |
| 430 | + }); | |
| 431 | + | |
| 432 | + return { | |
| 433 | + server: serverStats, | |
| 434 | + local: localStats, | |
| 435 | + combined: { | |
| 436 | + totalActive: localStats.activeConversations, | |
| 437 | + totalProcessed: serverStats.totalMessages || 0, | |
| 438 | + platforms: Object.keys({ ...serverStats.platformBreakdown, ...localStats.platformBreakdown }) | |
| 439 | + } | |
| 440 | + }; | |
| 441 | + } catch (error) { | |
| 442 | + console.error('[UnifiedHandler] Statistics error:', error); | |
| 443 | + throw error; | |
| 444 | + } | |
| 445 | + } | |
| 446 | + | |
| 447 | + async loadSettings() { | |
| 448 | + try { | |
| 449 | + const stored = await browser.storage.local.get('unifiedSettings'); | |
| 450 | + if (stored.unifiedSettings) { | |
| 451 | + this.settings = { ...this.settings, ...stored.unifiedSettings }; | |
| 452 | + } | |
| 453 | + } catch (error) { | |
| 454 | + console.error('[UnifiedHandler] Error loading settings:', error); | |
| 455 | + } | |
| 456 | + } | |
| 457 | + | |
| 458 | + async saveSettings() { | |
| 459 | + try { | |
| 460 | + await browser.storage.local.set({ unifiedSettings: this.settings }); | |
| 461 | + } catch (error) { | |
| 462 | + console.error('[UnifiedHandler] Error saving settings:', error); | |
| 463 | + } | |
| 464 | + } | |
| 465 | + | |
| 466 | + sleep(ms) { | |
| 467 | + return new Promise(resolve => setTimeout(resolve, ms)); | |
| 468 | + } | |
| 469 | +} | |
| 470 | + | |
| 471 | +// Export for use in background.js | |
| 472 | +if (typeof module !== 'undefined' && module.exports) { | |
| 473 | + module.exports = UnifiedMessageHandler; | |
| 474 | +} | |
extension/content-scripts/messenger.jsadded@@ -0,0 +1,539 @@ | ||
| 1 | +// Facebook Messenger Content Script for LooseCannon | |
| 2 | +console.log('[LooseCannon] Messenger content script loaded'); | |
| 3 | + | |
| 4 | +class MessengerIntegration { | |
| 5 | + constructor() { | |
| 6 | + this.isActive = false; | |
| 7 | + this.currentChat = null; | |
| 8 | + this.messageObserver = null; | |
| 9 | + this.platform = 'messenger'; | |
| 10 | + this.init(); | |
| 11 | + } | |
| 12 | + | |
| 13 | + init() { | |
| 14 | + this.waitForMessenger().then(() => { | |
| 15 | + console.log('[LooseCannon] Messenger detected and ready'); | |
| 16 | + this.setupMessageObserver(); | |
| 17 | + this.injectControls(); | |
| 18 | + this.listenForCommands(); | |
| 19 | + }); | |
| 20 | + } | |
| 21 | + | |
| 22 | + waitForMessenger() { | |
| 23 | + return new Promise((resolve) => { | |
| 24 | + const checkForApp = setInterval(() => { | |
| 25 | + // Check for Messenger's main chat area | |
| 26 | + const chatArea = document.querySelector('[role="main"]'); | |
| 27 | + const inputField = document.querySelector('[role="textbox"][aria-label*="Message"], [contenteditable="true"][data-lexical-editor]'); | |
| 28 | + | |
| 29 | + if (chatArea && inputField) { | |
| 30 | + clearInterval(checkForApp); | |
| 31 | + console.log('[LooseCannon] Messenger interface detected'); | |
| 32 | + resolve(); | |
| 33 | + } | |
| 34 | + }, 1000); | |
| 35 | + }); | |
| 36 | + } | |
| 37 | + | |
| 38 | + setupMessageObserver() { | |
| 39 | + // Find the messages container | |
| 40 | + const messageContainer = document.querySelector('[role="main"], [aria-label*="Messages"]'); | |
| 41 | + | |
| 42 | + if (!messageContainer) { | |
| 43 | + console.error('[LooseCannon] Could not find Messenger message container'); | |
| 44 | + setTimeout(() => this.setupMessageObserver(), 2000); | |
| 45 | + return; | |
| 46 | + } | |
| 47 | + | |
| 48 | + const config = { | |
| 49 | + childList: true, | |
| 50 | + subtree: true, | |
| 51 | + characterData: true, | |
| 52 | + attributes: true | |
| 53 | + }; | |
| 54 | + | |
| 55 | + this.messageObserver = new MutationObserver((mutations) => { | |
| 56 | + if (!this.isActive) return; | |
| 57 | + | |
| 58 | + mutations.forEach((mutation) => { | |
| 59 | + if (mutation.type === 'childList') { | |
| 60 | + mutation.addedNodes.forEach((node) => { | |
| 61 | + if (this.isIncomingMessage(node)) { | |
| 62 | + // Small delay to let DOM settle | |
| 63 | + setTimeout(() => { | |
| 64 | + this.handleIncomingMessage(node); | |
| 65 | + }, 100); | |
| 66 | + } | |
| 67 | + }); | |
| 68 | + } | |
| 69 | + }); | |
| 70 | + }); | |
| 71 | + | |
| 72 | + this.messageObserver.observe(messageContainer, config); | |
| 73 | + console.log('[LooseCannon] Messenger message observer setup complete'); | |
| 74 | + } | |
| 75 | + | |
| 76 | + isIncomingMessage(node) { | |
| 77 | + if (!node || !node.querySelector) return false; | |
| 78 | + | |
| 79 | + // Look for message containers | |
| 80 | + const messageSelectors = [ | |
| 81 | + '[role="row"]', | |
| 82 | + '[data-scope="messages_table"]', | |
| 83 | + 'div[class*="__fb-light-mode"]' | |
| 84 | + ]; | |
| 85 | + | |
| 86 | + let messageElement = null; | |
| 87 | + for (const selector of messageSelectors) { | |
| 88 | + messageElement = node.querySelector(selector); | |
| 89 | + if (messageElement) break; | |
| 90 | + } | |
| 91 | + | |
| 92 | + if (!messageElement && node.matches) { | |
| 93 | + for (const selector of messageSelectors) { | |
| 94 | + if (node.matches(selector)) { | |
| 95 | + messageElement = node; | |
| 96 | + break; | |
| 97 | + } | |
| 98 | + } | |
| 99 | + } | |
| 100 | + | |
| 101 | + if (!messageElement) return false; | |
| 102 | + | |
| 103 | + // Check if it's an incoming message by looking for specific patterns | |
| 104 | + // Messenger uses different styling for sent vs received messages | |
| 105 | + const messageText = messageElement.textContent || ''; | |
| 106 | + | |
| 107 | + // Skip if it's empty or a status message | |
| 108 | + if (!messageText || messageText.length < 2) return false; | |
| 109 | + | |
| 110 | + // Check if message is on the left side (incoming) | |
| 111 | + const computedStyle = window.getComputedStyle(messageElement); | |
| 112 | + const isLeftAligned = computedStyle.textAlign === 'left' || | |
| 113 | + computedStyle.justifyContent === 'flex-start'; | |
| 114 | + | |
| 115 | + // Additional check: sent messages usually have different background | |
| 116 | + const backgroundColor = computedStyle.backgroundColor; | |
| 117 | + const isSentMessage = backgroundColor && ( | |
| 118 | + backgroundColor.includes('rgb(0, 132, 255)') || // Blue for sent | |
| 119 | + backgroundColor.includes('rgb(24, 119, 242)') // Facebook blue | |
| 120 | + ); | |
| 121 | + | |
| 122 | + return isLeftAligned && !isSentMessage; | |
| 123 | + } | |
| 124 | + | |
| 125 | + handleIncomingMessage(node) { | |
| 126 | + const messageData = this.extractMessageData(node); | |
| 127 | + | |
| 128 | + if (!messageData.content && !messageData.media) { | |
| 129 | + return; // Empty message, skip | |
| 130 | + } | |
| 131 | + | |
| 132 | + const timestamp = new Date().toISOString(); | |
| 133 | + const chatId = this.getCurrentChatId(); | |
| 134 | + | |
| 135 | + console.log('[LooseCannon] Messenger message detected:', messageData); | |
| 136 | + | |
| 137 | + // Send to background script | |
| 138 | + browser.runtime.sendMessage({ | |
| 139 | + type: 'NEW_MESSAGE', | |
| 140 | + data: { | |
| 141 | + ...messageData, | |
| 142 | + timestamp, | |
| 143 | + chatId, | |
| 144 | + platform: this.platform | |
| 145 | + } | |
| 146 | + }).then(response => { | |
| 147 | + if (response && response.reply) { | |
| 148 | + this.simulateTypingAndSend(response.reply, response.delay); | |
| 149 | + } | |
| 150 | + }).catch(error => { | |
| 151 | + console.error('[LooseCannon] Error sending message to background:', error); | |
| 152 | + }); | |
| 153 | + } | |
| 154 | + | |
| 155 | + extractMessageData(node) { | |
| 156 | + const data = { | |
| 157 | + type: 'text', | |
| 158 | + content: '', | |
| 159 | + media: null, | |
| 160 | + metadata: {} | |
| 161 | + }; | |
| 162 | + | |
| 163 | + // Extract text content | |
| 164 | + const textSelectors = [ | |
| 165 | + '[dir="auto"]', | |
| 166 | + 'span[class*="text"]', | |
| 167 | + 'div[class*="text"]' | |
| 168 | + ]; | |
| 169 | + | |
| 170 | + for (const selector of textSelectors) { | |
| 171 | + const textElement = node.querySelector(selector); | |
| 172 | + if (textElement && textElement.textContent) { | |
| 173 | + data.content = textElement.textContent.trim(); | |
| 174 | + break; | |
| 175 | + } | |
| 176 | + } | |
| 177 | + | |
| 178 | + // Fallback to full text content | |
| 179 | + if (!data.content) { | |
| 180 | + data.content = node.textContent || node.innerText || ''; | |
| 181 | + data.content = data.content.trim(); | |
| 182 | + } | |
| 183 | + | |
| 184 | + // Check for images | |
| 185 | + const imageElement = node.querySelector('img[src*="scontent"], img[src*="fbcdn"]'); | |
| 186 | + if (imageElement) { | |
| 187 | + data.type = 'image'; | |
| 188 | + data.media = { type: 'image', src: imageElement.src }; | |
| 189 | + data.metadata.hasImage = true; | |
| 190 | + } | |
| 191 | + | |
| 192 | + // Check for links | |
| 193 | + const linkElements = node.querySelectorAll('a[href]'); | |
| 194 | + if (linkElements.length > 0) { | |
| 195 | + data.metadata.links = Array.from(linkElements).map(a => a.href); | |
| 196 | + } | |
| 197 | + | |
| 198 | + // Check for stickers or GIFs | |
| 199 | + if (node.querySelector('[aria-label*="sticker"], [aria-label*="GIF"]')) { | |
| 200 | + data.type = 'sticker'; | |
| 201 | + data.media = { type: 'sticker' }; | |
| 202 | + } | |
| 203 | + | |
| 204 | + // Check for voice messages | |
| 205 | + if (node.querySelector('[aria-label*="Audio"], [aria-label*="Voice"]')) { | |
| 206 | + data.type = 'audio'; | |
| 207 | + data.media = { type: 'audio' }; | |
| 208 | + data.metadata.hasAudio = true; | |
| 209 | + } | |
| 210 | + | |
| 211 | + // Extract phone numbers and emails from content | |
| 212 | + if (data.content) { | |
| 213 | + const phoneRegex = /[\+]?[(]?[0-9]{1,4}[)]?[-\s\.]?[(]?[0-9]{1,4}[)]?[-\s\.]?[0-9]{1,5}[-\s\.]?[0-9]{1,5}/g; | |
| 214 | + const phones = data.content.match(phoneRegex); | |
| 215 | + if (phones) { | |
| 216 | + data.metadata.phoneNumbers = phones; | |
| 217 | + } | |
| 218 | + | |
| 219 | + const emailRegex = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g; | |
| 220 | + const emails = data.content.match(emailRegex); | |
| 221 | + if (emails) { | |
| 222 | + data.metadata.emails = emails; | |
| 223 | + } | |
| 224 | + } | |
| 225 | + | |
| 226 | + return data; | |
| 227 | + } | |
| 228 | + | |
| 229 | + getCurrentChatId() { | |
| 230 | + // Try to get chat name from header | |
| 231 | + const headerSelectors = [ | |
| 232 | + '[role="banner"] h1', | |
| 233 | + 'header span[dir="auto"]', | |
| 234 | + '[aria-label*="Conversation with"]' | |
| 235 | + ]; | |
| 236 | + | |
| 237 | + for (const selector of headerSelectors) { | |
| 238 | + const element = document.querySelector(selector); | |
| 239 | + if (element) { | |
| 240 | + const text = element.textContent || element.getAttribute('aria-label') || ''; | |
| 241 | + if (text) { | |
| 242 | + return text.replace('Conversation with ', '').trim(); | |
| 243 | + } | |
| 244 | + } | |
| 245 | + } | |
| 246 | + | |
| 247 | + return 'unknown'; | |
| 248 | + } | |
| 249 | + | |
| 250 | + simulateTypingAndSend(text, delay = 3000) { | |
| 251 | + // Show typing indicator | |
| 252 | + this.startTyping(); | |
| 253 | + | |
| 254 | + // Calculate realistic delay | |
| 255 | + const wordCount = text.split(' ').length; | |
| 256 | + const typingDelay = Math.min(delay || (wordCount * 200 + 2000), 10000); | |
| 257 | + | |
| 258 | + setTimeout(() => { | |
| 259 | + this.stopTyping(); | |
| 260 | + this.sendMessage(text); | |
| 261 | + }, typingDelay); | |
| 262 | + } | |
| 263 | + | |
| 264 | + startTyping() { | |
| 265 | + const inputField = this.getInputField(); | |
| 266 | + if (!inputField) return; | |
| 267 | + | |
| 268 | + // Focus the input | |
| 269 | + inputField.focus(); | |
| 270 | + | |
| 271 | + // Add placeholder text to trigger typing indicator | |
| 272 | + this.setInputText(inputField, '...'); | |
| 273 | + | |
| 274 | + // Trigger input event | |
| 275 | + const event = new Event('input', { bubbles: true }); | |
| 276 | + inputField.dispatchEvent(event); | |
| 277 | + } | |
| 278 | + | |
| 279 | + stopTyping() { | |
| 280 | + const inputField = this.getInputField(); | |
| 281 | + if (!inputField) return; | |
| 282 | + | |
| 283 | + // Clear the placeholder | |
| 284 | + this.setInputText(inputField, ''); | |
| 285 | + } | |
| 286 | + | |
| 287 | + sendMessage(text) { | |
| 288 | + const inputField = this.getInputField(); | |
| 289 | + | |
| 290 | + if (!inputField) { | |
| 291 | + console.error('[LooseCannon] Could not find Messenger input field'); | |
| 292 | + return; | |
| 293 | + } | |
| 294 | + | |
| 295 | + // Set the message text | |
| 296 | + this.setInputText(inputField, text); | |
| 297 | + | |
| 298 | + // Trigger input event | |
| 299 | + const inputEvent = new Event('input', { bubbles: true }); | |
| 300 | + inputField.dispatchEvent(inputEvent); | |
| 301 | + | |
| 302 | + // Small delay then send | |
| 303 | + setTimeout(() => { | |
| 304 | + // Try to find and click send button | |
| 305 | + const sendButtonSelectors = [ | |
| 306 | + '[aria-label="Send"]', | |
| 307 | + '[aria-label="Press Enter to send"]', | |
| 308 | + 'div[role="button"][aria-label*="Send"]' | |
| 309 | + ]; | |
| 310 | + | |
| 311 | + let sendButton = null; | |
| 312 | + for (const selector of sendButtonSelectors) { | |
| 313 | + sendButton = document.querySelector(selector); | |
| 314 | + if (sendButton) break; | |
| 315 | + } | |
| 316 | + | |
| 317 | + if (sendButton) { | |
| 318 | + sendButton.click(); | |
| 319 | + console.log('[LooseCannon] Messenger message sent via button'); | |
| 320 | + } else { | |
| 321 | + // Fallback: simulate Enter key | |
| 322 | + const enterEvent = new KeyboardEvent('keydown', { | |
| 323 | + key: 'Enter', | |
| 324 | + keyCode: 13, | |
| 325 | + which: 13, | |
| 326 | + bubbles: true, | |
| 327 | + cancelable: true | |
| 328 | + }); | |
| 329 | + inputField.dispatchEvent(enterEvent); | |
| 330 | + console.log('[LooseCannon] Messenger message sent via Enter key'); | |
| 331 | + } | |
| 332 | + }, 100); | |
| 333 | + } | |
| 334 | + | |
| 335 | + getInputField() { | |
| 336 | + const selectors = [ | |
| 337 | + '[role="textbox"][aria-label*="Message"]', | |
| 338 | + '[contenteditable="true"][data-lexical-editor]', | |
| 339 | + '[role="textbox"][contenteditable="true"]', | |
| 340 | + 'div[contenteditable="true"][role="textbox"]' | |
| 341 | + ]; | |
| 342 | + | |
| 343 | + for (const selector of selectors) { | |
| 344 | + const field = document.querySelector(selector); | |
| 345 | + if (field) return field; | |
| 346 | + } | |
| 347 | + | |
| 348 | + return null; | |
| 349 | + } | |
| 350 | + | |
| 351 | + setInputText(inputField, text) { | |
| 352 | + if (inputField.getAttribute('contenteditable') === 'true') { | |
| 353 | + // For contenteditable elements (Messenger uses these) | |
| 354 | + inputField.textContent = text; | |
| 355 | + | |
| 356 | + // Also try setting innerHTML for Lexical editor | |
| 357 | + if (text) { | |
| 358 | + inputField.innerHTML = `<p>${text}</p>`; | |
| 359 | + } else { | |
| 360 | + inputField.innerHTML = ''; | |
| 361 | + } | |
| 362 | + | |
| 363 | + // Move cursor to end | |
| 364 | + const range = document.createRange(); | |
| 365 | + const selection = window.getSelection(); | |
| 366 | + range.selectNodeContents(inputField); | |
| 367 | + range.collapse(false); | |
| 368 | + selection.removeAllRanges(); | |
| 369 | + selection.addRange(range); | |
| 370 | + } else { | |
| 371 | + // For regular input/textarea | |
| 372 | + inputField.value = text; | |
| 373 | + } | |
| 374 | + | |
| 375 | + // Trigger various events that Messenger might listen to | |
| 376 | + ['input', 'change', 'keyup'].forEach(eventType => { | |
| 377 | + const event = new Event(eventType, { bubbles: true }); | |
| 378 | + inputField.dispatchEvent(event); | |
| 379 | + }); | |
| 380 | + } | |
| 381 | + | |
| 382 | + injectControls() { | |
| 383 | + const style = document.createElement('style'); | |
| 384 | + style.textContent = ` | |
| 385 | + .loosecannon-toggle-messenger { | |
| 386 | + position: fixed; | |
| 387 | + bottom: 20px; | |
| 388 | + right: 20px; | |
| 389 | + z-index: 9999; | |
| 390 | + background: linear-gradient(135deg, #0084ff, #44bfff); | |
| 391 | + color: white; | |
| 392 | + border: none; | |
| 393 | + border-radius: 50px; | |
| 394 | + padding: 12px 20px; | |
| 395 | + cursor: pointer; | |
| 396 | + font-weight: bold; | |
| 397 | + box-shadow: 0 2px 10px rgba(0,132,255,0.4); | |
| 398 | + transition: all 0.3s; | |
| 399 | + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; | |
| 400 | + } | |
| 401 | + | |
| 402 | + .loosecannon-toggle-messenger.active { | |
| 403 | + background: linear-gradient(135deg, #44ff44, #66ff66); | |
| 404 | + } | |
| 405 | + | |
| 406 | + .loosecannon-toggle-messenger:hover { | |
| 407 | + transform: scale(1.05); | |
| 408 | + box-shadow: 0 4px 15px rgba(0,132,255,0.6); | |
| 409 | + } | |
| 410 | + | |
| 411 | + .loosecannon-indicator-messenger { | |
| 412 | + position: fixed; | |
| 413 | + top: 70px; | |
| 414 | + right: 20px; | |
| 415 | + background: rgba(0, 132, 255, 0.9); | |
| 416 | + color: white; | |
| 417 | + padding: 8px 15px; | |
| 418 | + border-radius: 20px; | |
| 419 | + font-size: 12px; | |
| 420 | + z-index: 9999; | |
| 421 | + display: none; | |
| 422 | + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; | |
| 423 | + } | |
| 424 | + | |
| 425 | + .loosecannon-indicator-messenger.active { | |
| 426 | + display: block; | |
| 427 | + background: rgba(68, 255, 68, 0.9); | |
| 428 | + } | |
| 429 | + `; | |
| 430 | + document.head.appendChild(style); | |
| 431 | + | |
| 432 | + const toggleButton = document.createElement('button'); | |
| 433 | + toggleButton.className = 'loosecannon-toggle-messenger'; | |
| 434 | + toggleButton.textContent = 'LC: OFF'; | |
| 435 | + toggleButton.onclick = () => this.toggleActive(); | |
| 436 | + document.body.appendChild(toggleButton); | |
| 437 | + | |
| 438 | + const indicator = document.createElement('div'); | |
| 439 | + indicator.className = 'loosecannon-indicator-messenger'; | |
| 440 | + indicator.innerHTML = '🤖 LooseCannon Active<br><small>Messenger</small>'; | |
| 441 | + document.body.appendChild(indicator); | |
| 442 | + } | |
| 443 | + | |
| 444 | + toggleActive() { | |
| 445 | + this.isActive = !this.isActive; | |
| 446 | + this.updateUI(); | |
| 447 | + | |
| 448 | + const chatId = this.getCurrentChatId(); | |
| 449 | + console.log(`[LooseCannon] Messenger ${this.isActive ? 'activated' : 'deactivated'} for: ${chatId}`); | |
| 450 | + | |
| 451 | + // Notify background script | |
| 452 | + browser.runtime.sendMessage({ | |
| 453 | + type: 'TOGGLE_ACTIVE', | |
| 454 | + data: { | |
| 455 | + isActive: this.isActive, | |
| 456 | + chatId: chatId, | |
| 457 | + platform: this.platform | |
| 458 | + } | |
| 459 | + }); | |
| 460 | + } | |
| 461 | + | |
| 462 | + updateUI() { | |
| 463 | + const button = document.querySelector('.loosecannon-toggle-messenger'); | |
| 464 | + const indicator = document.querySelector('.loosecannon-indicator-messenger'); | |
| 465 | + | |
| 466 | + if (this.isActive) { | |
| 467 | + button?.classList.add('active'); | |
| 468 | + if (button) button.textContent = 'LC: ON'; | |
| 469 | + indicator?.classList.add('active'); | |
| 470 | + } else { | |
| 471 | + button?.classList.remove('active'); | |
| 472 | + if (button) button.textContent = 'LC: OFF'; | |
| 473 | + indicator?.classList.remove('active'); | |
| 474 | + } | |
| 475 | + } | |
| 476 | + | |
| 477 | + listenForCommands() { | |
| 478 | + browser.runtime.onMessage.addListener((message, sender, sendResponse) => { | |
| 479 | + switch (message.type) { | |
| 480 | + case 'GET_STATUS': | |
| 481 | + sendResponse({ | |
| 482 | + isActive: this.isActive, | |
| 483 | + platform: this.platform, | |
| 484 | + currentChat: this.currentChat | |
| 485 | + }); | |
| 486 | + break; | |
| 487 | + | |
| 488 | + case 'SET_ACTIVE': | |
| 489 | + this.isActive = message.data.isActive; | |
| 490 | + this.updateUI(); | |
| 491 | + break; | |
| 492 | + | |
| 493 | + case 'SEND_MESSAGE': | |
| 494 | + this.simulateTypingAndSend(message.data.text, message.data.delay); | |
| 495 | + break; | |
| 496 | + | |
| 497 | + case 'SCAMMER_DETECTED': | |
| 498 | + this.showScammerAlert(message.data.score); | |
| 499 | + break; | |
| 500 | + } | |
| 501 | + }); | |
| 502 | + } | |
| 503 | + | |
| 504 | + showScammerAlert(score) { | |
| 505 | + const alert = document.createElement('div'); | |
| 506 | + alert.innerHTML = ` | |
| 507 | + <div style=" | |
| 508 | + position: fixed; | |
| 509 | + top: 100px; | |
| 510 | + right: 20px; | |
| 511 | + background: linear-gradient(135deg, #ff4444, #ff6666); | |
| 512 | + color: white; | |
| 513 | + padding: 15px 20px; | |
| 514 | + border-radius: 12px; | |
| 515 | + z-index: 10000; | |
| 516 | + box-shadow: 0 4px 20px rgba(255,68,68,0.4); | |
| 517 | + animation: slideIn 0.3s ease; | |
| 518 | + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; | |
| 519 | + "> | |
| 520 | + <strong>⚠️ Potential Scammer Detected!</strong><br> | |
| 521 | + <small>Confidence: ${(score * 100).toFixed(0)}%</small><br> | |
| 522 | + <small>LooseCannon ready to engage</small> | |
| 523 | + </div> | |
| 524 | + `; | |
| 525 | + | |
| 526 | + document.body.appendChild(alert); | |
| 527 | + | |
| 528 | + setTimeout(() => { | |
| 529 | + alert.remove(); | |
| 530 | + }, 5000); | |
| 531 | + } | |
| 532 | +} | |
| 533 | + | |
| 534 | +// Initialize when DOM is ready | |
| 535 | +if (document.readyState === 'loading') { | |
| 536 | + document.addEventListener('DOMContentLoaded', () => new MessengerIntegration()); | |
| 537 | +} else { | |
| 538 | + new MessengerIntegration(); | |
| 539 | +} | |
extension/content-scripts/telegram.jsadded@@ -0,0 +1,480 @@ | ||
| 1 | +// Telegram Web Content Script for LooseCannon | |
| 2 | +console.log('[LooseCannon] Telegram content script loaded'); | |
| 3 | + | |
| 4 | +class TelegramIntegration { | |
| 5 | + constructor() { | |
| 6 | + this.isActive = false; | |
| 7 | + this.currentChat = null; | |
| 8 | + this.messageObserver = null; | |
| 9 | + this.platform = 'telegram'; | |
| 10 | + this.init(); | |
| 11 | + } | |
| 12 | + | |
| 13 | + init() { | |
| 14 | + this.waitForTelegram().then(() => { | |
| 15 | + console.log('[LooseCannon] Telegram Web detected and ready'); | |
| 16 | + this.setupMessageObserver(); | |
| 17 | + this.injectControls(); | |
| 18 | + this.listenForCommands(); | |
| 19 | + }); | |
| 20 | + } | |
| 21 | + | |
| 22 | + waitForTelegram() { | |
| 23 | + return new Promise((resolve) => { | |
| 24 | + const checkForApp = setInterval(() => { | |
| 25 | + // Check for Telegram's main chat area | |
| 26 | + const chatArea = document.querySelector('.im_history_wrap, .messages-container, [class*="Message"]'); | |
| 27 | + const inputField = document.querySelector('.composer_rich_textarea, [class*="ComposerInput"]'); | |
| 28 | + | |
| 29 | + if (chatArea && inputField) { | |
| 30 | + clearInterval(checkForApp); | |
| 31 | + console.log('[LooseCannon] Telegram interface detected'); | |
| 32 | + resolve(); | |
| 33 | + } | |
| 34 | + }, 1000); | |
| 35 | + }); | |
| 36 | + } | |
| 37 | + | |
| 38 | + setupMessageObserver() { | |
| 39 | + // Find the messages container | |
| 40 | + const messageContainer = document.querySelector('.im_history_wrap, .messages-container, [class*="MessagesScroller"]'); | |
| 41 | + | |
| 42 | + if (!messageContainer) { | |
| 43 | + console.error('[LooseCannon] Could not find Telegram message container'); | |
| 44 | + setTimeout(() => this.setupMessageObserver(), 2000); | |
| 45 | + return; | |
| 46 | + } | |
| 47 | + | |
| 48 | + const config = { | |
| 49 | + childList: true, | |
| 50 | + subtree: true, | |
| 51 | + characterData: true, | |
| 52 | + attributes: true, | |
| 53 | + attributeFilter: ['class'] | |
| 54 | + }; | |
| 55 | + | |
| 56 | + this.messageObserver = new MutationObserver((mutations) => { | |
| 57 | + if (!this.isActive) return; | |
| 58 | + | |
| 59 | + mutations.forEach((mutation) => { | |
| 60 | + if (mutation.type === 'childList') { | |
| 61 | + mutation.addedNodes.forEach((node) => { | |
| 62 | + if (this.isIncomingMessage(node)) { | |
| 63 | + this.handleIncomingMessage(node); | |
| 64 | + } | |
| 65 | + }); | |
| 66 | + } | |
| 67 | + }); | |
| 68 | + }); | |
| 69 | + | |
| 70 | + this.messageObserver.observe(messageContainer, config); | |
| 71 | + console.log('[LooseCannon] Telegram message observer setup complete'); | |
| 72 | + } | |
| 73 | + | |
| 74 | + isIncomingMessage(node) { | |
| 75 | + if (!node || !node.classList) return false; | |
| 76 | + | |
| 77 | + // Check for Telegram message classes | |
| 78 | + const messageClasses = ['im_message', 'message', 'Message']; | |
| 79 | + const hasMessageClass = messageClasses.some(cls => | |
| 80 | + node.classList.contains(cls) || | |
| 81 | + Array.from(node.classList).some(c => c.includes('Message')) | |
| 82 | + ); | |
| 83 | + | |
| 84 | + if (!hasMessageClass) return false; | |
| 85 | + | |
| 86 | + // Check if it's an incoming message (not sent by us) | |
| 87 | + const isOutgoing = node.classList.contains('im_message_out') || | |
| 88 | + node.classList.contains('own') || | |
| 89 | + Array.from(node.classList).some(c => c.includes('own') || c.includes('Out')); | |
| 90 | + | |
| 91 | + return !isOutgoing; | |
| 92 | + } | |
| 93 | + | |
| 94 | + handleIncomingMessage(node) { | |
| 95 | + const messageData = this.extractMessageData(node); | |
| 96 | + | |
| 97 | + if (!messageData.content && !messageData.media) { | |
| 98 | + return; // Empty message, skip | |
| 99 | + } | |
| 100 | + | |
| 101 | + const timestamp = new Date().toISOString(); | |
| 102 | + const chatId = this.getCurrentChatId(); | |
| 103 | + | |
| 104 | + console.log('[LooseCannon] Telegram message detected:', messageData); | |
| 105 | + | |
| 106 | + // Send to background script | |
| 107 | + browser.runtime.sendMessage({ | |
| 108 | + type: 'NEW_MESSAGE', | |
| 109 | + data: { | |
| 110 | + ...messageData, | |
| 111 | + timestamp, | |
| 112 | + chatId, | |
| 113 | + platform: this.platform | |
| 114 | + } | |
| 115 | + }).then(response => { | |
| 116 | + if (response && response.reply) { | |
| 117 | + this.simulateTypingAndSend(response.reply, response.delay); | |
| 118 | + } | |
| 119 | + }).catch(error => { | |
| 120 | + console.error('[LooseCannon] Error sending message to background:', error); | |
| 121 | + }); | |
| 122 | + } | |
| 123 | + | |
| 124 | + extractMessageData(node) { | |
| 125 | + const data = { | |
| 126 | + type: 'text', | |
| 127 | + content: '', | |
| 128 | + media: null, | |
| 129 | + metadata: {} | |
| 130 | + }; | |
| 131 | + | |
| 132 | + // Extract text content | |
| 133 | + const textElement = node.querySelector('.im_message_text, [class*="text-content"], [class*="MessageText"]'); | |
| 134 | + if (textElement) { | |
| 135 | + data.content = textElement.textContent || textElement.innerText || ''; | |
| 136 | + | |
| 137 | + // Extract links | |
| 138 | + const links = textElement.querySelectorAll('a'); | |
| 139 | + if (links.length > 0) { | |
| 140 | + data.metadata.links = Array.from(links).map(a => a.href); | |
| 141 | + } | |
| 142 | + } | |
| 143 | + | |
| 144 | + // Check for media | |
| 145 | + const photoElement = node.querySelector('.im_message_photo_thumb, [class*="Photo"], img[class*="media"]'); | |
| 146 | + if (photoElement) { | |
| 147 | + data.type = 'image'; | |
| 148 | + data.media = { type: 'image' }; | |
| 149 | + data.metadata.hasImage = true; | |
| 150 | + } | |
| 151 | + | |
| 152 | + // Check for audio/voice | |
| 153 | + const audioElement = node.querySelector('.im_message_audio, .audio, [class*="Audio"], [class*="Voice"]'); | |
| 154 | + if (audioElement) { | |
| 155 | + data.type = 'audio'; | |
| 156 | + data.media = { type: 'audio' }; | |
| 157 | + data.metadata.hasAudio = true; | |
| 158 | + } | |
| 159 | + | |
| 160 | + // Check for documents | |
| 161 | + const docElement = node.querySelector('.im_message_document, [class*="Document"]'); | |
| 162 | + if (docElement) { | |
| 163 | + data.type = 'document'; | |
| 164 | + data.media = { type: 'document' }; | |
| 165 | + data.metadata.hasDocument = true; | |
| 166 | + } | |
| 167 | + | |
| 168 | + // Check for stickers | |
| 169 | + const stickerElement = node.querySelector('.im_message_sticker, [class*="Sticker"]'); | |
| 170 | + if (stickerElement) { | |
| 171 | + data.type = 'sticker'; | |
| 172 | + data.media = { type: 'sticker' }; | |
| 173 | + } | |
| 174 | + | |
| 175 | + // Extract phone numbers and emails from content | |
| 176 | + if (data.content) { | |
| 177 | + const phoneRegex = /[\+]?[(]?[0-9]{1,4}[)]?[-\s\.]?[(]?[0-9]{1,4}[)]?[-\s\.]?[0-9]{1,5}[-\s\.]?[0-9]{1,5}/g; | |
| 178 | + const phones = data.content.match(phoneRegex); | |
| 179 | + if (phones) { | |
| 180 | + data.metadata.phoneNumbers = phones; | |
| 181 | + } | |
| 182 | + | |
| 183 | + const emailRegex = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g; | |
| 184 | + const emails = data.content.match(emailRegex); | |
| 185 | + if (emails) { | |
| 186 | + data.metadata.emails = emails; | |
| 187 | + } | |
| 188 | + } | |
| 189 | + | |
| 190 | + return data; | |
| 191 | + } | |
| 192 | + | |
| 193 | + getCurrentChatId() { | |
| 194 | + // Try to get chat title from header | |
| 195 | + const headerTitle = document.querySelector('.tg_head_peer_title, .chat-title, [class*="ChatTitle"], [class*="HeaderTitle"]'); | |
| 196 | + if (headerTitle) { | |
| 197 | + return headerTitle.textContent || 'unknown'; | |
| 198 | + } | |
| 199 | + | |
| 200 | + // Try to get from active dialog | |
| 201 | + const activeDialog = document.querySelector('.im_dialog_wrap.active, .dialog.active, [class*="ChatItem"][class*="active"]'); | |
| 202 | + if (activeDialog) { | |
| 203 | + const title = activeDialog.querySelector('.im_dialog_peer span, [class*="ChatTitle"]'); | |
| 204 | + if (title) { | |
| 205 | + return title.textContent || 'unknown'; | |
| 206 | + } | |
| 207 | + } | |
| 208 | + | |
| 209 | + return 'unknown'; | |
| 210 | + } | |
| 211 | + | |
| 212 | + simulateTypingAndSend(text, delay = 3000) { | |
| 213 | + // Show typing indicator | |
| 214 | + this.startTyping(); | |
| 215 | + | |
| 216 | + // Calculate realistic delay | |
| 217 | + const wordCount = text.split(' ').length; | |
| 218 | + const typingDelay = Math.min(delay || (wordCount * 200 + 2000), 10000); | |
| 219 | + | |
| 220 | + setTimeout(() => { | |
| 221 | + this.stopTyping(); | |
| 222 | + this.sendMessage(text); | |
| 223 | + }, typingDelay); | |
| 224 | + } | |
| 225 | + | |
| 226 | + startTyping() { | |
| 227 | + const inputField = this.getInputField(); | |
| 228 | + if (!inputField) return; | |
| 229 | + | |
| 230 | + // Focus the input | |
| 231 | + inputField.focus(); | |
| 232 | + | |
| 233 | + // Add placeholder to trigger typing indicator | |
| 234 | + if (inputField.contentEditable === 'true') { | |
| 235 | + inputField.textContent = '...'; | |
| 236 | + } else { | |
| 237 | + inputField.value = '...'; | |
| 238 | + } | |
| 239 | + | |
| 240 | + // Trigger input event | |
| 241 | + const event = new Event('input', { bubbles: true }); | |
| 242 | + inputField.dispatchEvent(event); | |
| 243 | + } | |
| 244 | + | |
| 245 | + stopTyping() { | |
| 246 | + const inputField = this.getInputField(); | |
| 247 | + if (!inputField) return; | |
| 248 | + | |
| 249 | + // Clear the placeholder | |
| 250 | + if (inputField.contentEditable === 'true') { | |
| 251 | + inputField.textContent = ''; | |
| 252 | + } else { | |
| 253 | + inputField.value = ''; | |
| 254 | + } | |
| 255 | + } | |
| 256 | + | |
| 257 | + sendMessage(text) { | |
| 258 | + const inputField = this.getInputField(); | |
| 259 | + | |
| 260 | + if (!inputField) { | |
| 261 | + console.error('[LooseCannon] Could not find Telegram input field'); | |
| 262 | + return; | |
| 263 | + } | |
| 264 | + | |
| 265 | + // Set the message text | |
| 266 | + if (inputField.contentEditable === 'true') { | |
| 267 | + // For contenteditable divs | |
| 268 | + inputField.textContent = text; | |
| 269 | + inputField.innerHTML = text; | |
| 270 | + | |
| 271 | + // Move cursor to end | |
| 272 | + const range = document.createRange(); | |
| 273 | + const selection = window.getSelection(); | |
| 274 | + range.selectNodeContents(inputField); | |
| 275 | + range.collapse(false); | |
| 276 | + selection.removeAllRanges(); | |
| 277 | + selection.addRange(range); | |
| 278 | + } else { | |
| 279 | + // For regular input/textarea | |
| 280 | + inputField.value = text; | |
| 281 | + } | |
| 282 | + | |
| 283 | + // Trigger input event to update Telegram's state | |
| 284 | + const inputEvent = new Event('input', { bubbles: true }); | |
| 285 | + inputField.dispatchEvent(inputEvent); | |
| 286 | + | |
| 287 | + // Try multiple methods to send | |
| 288 | + setTimeout(() => { | |
| 289 | + // Method 1: Click send button | |
| 290 | + const sendButton = document.querySelector('.im_submit, .compose-button-send, [class*="SendButton"], [class*="ComposeButton"][class*="send"]'); | |
| 291 | + if (sendButton) { | |
| 292 | + sendButton.click(); | |
| 293 | + console.log('[LooseCannon] Telegram message sent via button'); | |
| 294 | + return; | |
| 295 | + } | |
| 296 | + | |
| 297 | + // Method 2: Simulate Enter key | |
| 298 | + const enterEvent = new KeyboardEvent('keydown', { | |
| 299 | + key: 'Enter', | |
| 300 | + keyCode: 13, | |
| 301 | + which: 13, | |
| 302 | + bubbles: true | |
| 303 | + }); | |
| 304 | + inputField.dispatchEvent(enterEvent); | |
| 305 | + console.log('[LooseCannon] Telegram message sent via Enter key'); | |
| 306 | + }, 100); | |
| 307 | + } | |
| 308 | + | |
| 309 | + getInputField() { | |
| 310 | + // Try multiple selectors for Telegram's input field | |
| 311 | + const selectors = [ | |
| 312 | + '.composer_rich_textarea', | |
| 313 | + '[contenteditable="true"][class*="input"]', | |
| 314 | + '[class*="ComposerInput"]', | |
| 315 | + '.im_message_field', | |
| 316 | + '[class*="MessageInput"]' | |
| 317 | + ]; | |
| 318 | + | |
| 319 | + for (const selector of selectors) { | |
| 320 | + const field = document.querySelector(selector); | |
| 321 | + if (field) return field; | |
| 322 | + } | |
| 323 | + | |
| 324 | + return null; | |
| 325 | + } | |
| 326 | + | |
| 327 | + injectControls() { | |
| 328 | + const style = document.createElement('style'); | |
| 329 | + style.textContent = ` | |
| 330 | + .loosecannon-toggle-telegram { | |
| 331 | + position: fixed; | |
| 332 | + bottom: 20px; | |
| 333 | + left: 20px; | |
| 334 | + z-index: 9999; | |
| 335 | + background: #0088cc; | |
| 336 | + color: white; | |
| 337 | + border: none; | |
| 338 | + border-radius: 50px; | |
| 339 | + padding: 12px 20px; | |
| 340 | + cursor: pointer; | |
| 341 | + font-weight: bold; | |
| 342 | + box-shadow: 0 2px 10px rgba(0,136,204,0.3); | |
| 343 | + transition: all 0.3s; | |
| 344 | + } | |
| 345 | + | |
| 346 | + .loosecannon-toggle-telegram.active { | |
| 347 | + background: #44ff44; | |
| 348 | + } | |
| 349 | + | |
| 350 | + .loosecannon-toggle-telegram:hover { | |
| 351 | + transform: scale(1.05); | |
| 352 | + } | |
| 353 | + | |
| 354 | + .loosecannon-indicator-telegram { | |
| 355 | + position: fixed; | |
| 356 | + top: 20px; | |
| 357 | + left: 20px; | |
| 358 | + background: rgba(0, 136, 204, 0.9); | |
| 359 | + color: white; | |
| 360 | + padding: 8px 15px; | |
| 361 | + border-radius: 20px; | |
| 362 | + font-size: 12px; | |
| 363 | + z-index: 9999; | |
| 364 | + display: none; | |
| 365 | + } | |
| 366 | + | |
| 367 | + .loosecannon-indicator-telegram.active { | |
| 368 | + display: block; | |
| 369 | + background: rgba(68, 255, 68, 0.9); | |
| 370 | + } | |
| 371 | + `; | |
| 372 | + document.head.appendChild(style); | |
| 373 | + | |
| 374 | + const toggleButton = document.createElement('button'); | |
| 375 | + toggleButton.className = 'loosecannon-toggle-telegram'; | |
| 376 | + toggleButton.textContent = 'LC: OFF'; | |
| 377 | + toggleButton.onclick = () => this.toggleActive(); | |
| 378 | + document.body.appendChild(toggleButton); | |
| 379 | + | |
| 380 | + const indicator = document.createElement('div'); | |
| 381 | + indicator.className = 'loosecannon-indicator-telegram'; | |
| 382 | + indicator.innerHTML = '🤖 LooseCannon Active<br><small>Telegram</small>'; | |
| 383 | + document.body.appendChild(indicator); | |
| 384 | + } | |
| 385 | + | |
| 386 | + toggleActive() { | |
| 387 | + this.isActive = !this.isActive; | |
| 388 | + this.updateUI(); | |
| 389 | + | |
| 390 | + const chatId = this.getCurrentChatId(); | |
| 391 | + console.log(`[LooseCannon] Telegram ${this.isActive ? 'activated' : 'deactivated'} for: ${chatId}`); | |
| 392 | + | |
| 393 | + // Notify background script | |
| 394 | + browser.runtime.sendMessage({ | |
| 395 | + type: 'TOGGLE_ACTIVE', | |
| 396 | + data: { | |
| 397 | + isActive: this.isActive, | |
| 398 | + chatId: chatId, | |
| 399 | + platform: this.platform | |
| 400 | + } | |
| 401 | + }); | |
| 402 | + } | |
| 403 | + | |
| 404 | + updateUI() { | |
| 405 | + const button = document.querySelector('.loosecannon-toggle-telegram'); | |
| 406 | + const indicator = document.querySelector('.loosecannon-indicator-telegram'); | |
| 407 | + | |
| 408 | + if (this.isActive) { | |
| 409 | + button?.classList.add('active'); | |
| 410 | + if (button) button.textContent = 'LC: ON'; | |
| 411 | + indicator?.classList.add('active'); | |
| 412 | + } else { | |
| 413 | + button?.classList.remove('active'); | |
| 414 | + if (button) button.textContent = 'LC: OFF'; | |
| 415 | + indicator?.classList.remove('active'); | |
| 416 | + } | |
| 417 | + } | |
| 418 | + | |
| 419 | + listenForCommands() { | |
| 420 | + browser.runtime.onMessage.addListener((message, sender, sendResponse) => { | |
| 421 | + switch (message.type) { | |
| 422 | + case 'GET_STATUS': | |
| 423 | + sendResponse({ | |
| 424 | + isActive: this.isActive, | |
| 425 | + platform: this.platform, | |
| 426 | + currentChat: this.currentChat | |
| 427 | + }); | |
| 428 | + break; | |
| 429 | + | |
| 430 | + case 'SET_ACTIVE': | |
| 431 | + this.isActive = message.data.isActive; | |
| 432 | + this.updateUI(); | |
| 433 | + break; | |
| 434 | + | |
| 435 | + case 'SEND_MESSAGE': | |
| 436 | + this.simulateTypingAndSend(message.data.text, message.data.delay); | |
| 437 | + break; | |
| 438 | + | |
| 439 | + case 'SCAMMER_DETECTED': | |
| 440 | + this.showScammerAlert(message.data.score); | |
| 441 | + break; | |
| 442 | + } | |
| 443 | + }); | |
| 444 | + } | |
| 445 | + | |
| 446 | + showScammerAlert(score) { | |
| 447 | + const alert = document.createElement('div'); | |
| 448 | + alert.innerHTML = ` | |
| 449 | + <div style=" | |
| 450 | + position: fixed; | |
| 451 | + top: 60px; | |
| 452 | + left: 20px; | |
| 453 | + background: linear-gradient(135deg, #ff4444, #ff6666); | |
| 454 | + color: white; | |
| 455 | + padding: 15px 20px; | |
| 456 | + border-radius: 10px; | |
| 457 | + z-index: 10000; | |
| 458 | + box-shadow: 0 4px 20px rgba(255,68,68,0.4); | |
| 459 | + animation: slideIn 0.3s ease; | |
| 460 | + "> | |
| 461 | + <strong>⚠️ Scammer Detected!</strong><br> | |
| 462 | + <small>Confidence: ${(score * 100).toFixed(0)}%</small><br> | |
| 463 | + <small>LooseCannon ready to engage</small> | |
| 464 | + </div> | |
| 465 | + `; | |
| 466 | + | |
| 467 | + document.body.appendChild(alert); | |
| 468 | + | |
| 469 | + setTimeout(() => { | |
| 470 | + alert.remove(); | |
| 471 | + }, 5000); | |
| 472 | + } | |
| 473 | +} | |
| 474 | + | |
| 475 | +// Initialize when DOM is ready | |
| 476 | +if (document.readyState === 'loading') { | |
| 477 | + document.addEventListener('DOMContentLoaded', () => new TelegramIntegration()); | |
| 478 | +} else { | |
| 479 | + new TelegramIntegration(); | |
| 480 | +} | |
extension/content-scripts/whatsapp-enhanced.jsadded@@ -0,0 +1,641 @@ | ||
| 1 | +// Enhanced WhatsApp Web Content Script for LooseCannon | |
| 2 | +// Detects various message types and implements sophisticated interaction | |
| 3 | + | |
| 4 | +console.log('[LooseCannon] Enhanced WhatsApp content script loaded'); | |
| 5 | + | |
| 6 | +class EnhancedWhatsAppIntegration { | |
| 7 | + constructor() { | |
| 8 | + this.isActive = false; | |
| 9 | + this.currentChat = null; | |
| 10 | + this.messageObserver = null; | |
| 11 | + this.typingTimer = null; | |
| 12 | + this.conversationState = new Map(); | |
| 13 | + this.scammerPatterns = this.loadScammerPatterns(); | |
| 14 | + this.init(); | |
| 15 | + } | |
| 16 | + | |
| 17 | + init() { | |
| 18 | + this.waitForWhatsApp().then(() => { | |
| 19 | + console.log('[LooseCannon] WhatsApp detected and ready'); | |
| 20 | + this.setupMessageObserver(); | |
| 21 | + this.injectControls(); | |
| 22 | + this.listenForCommands(); | |
| 23 | + this.setupConversationTracking(); | |
| 24 | + }); | |
| 25 | + } | |
| 26 | + | |
| 27 | + waitForWhatsApp() { | |
| 28 | + return new Promise((resolve) => { | |
| 29 | + const checkForApp = setInterval(() => { | |
| 30 | + const mainWrapper = document.querySelector('[data-testid="conversation-panel-wrapper"]'); | |
| 31 | + if (mainWrapper) { | |
| 32 | + clearInterval(checkForApp); | |
| 33 | + resolve(); | |
| 34 | + } | |
| 35 | + }, 1000); | |
| 36 | + }); | |
| 37 | + } | |
| 38 | + | |
| 39 | + setupMessageObserver() { | |
| 40 | + const messageContainer = document.querySelector('[role="application"]'); | |
| 41 | + | |
| 42 | + if (!messageContainer) { | |
| 43 | + console.error('[LooseCannon] Could not find message container'); | |
| 44 | + return; | |
| 45 | + } | |
| 46 | + | |
| 47 | + const config = { | |
| 48 | + childList: true, | |
| 49 | + subtree: true, | |
| 50 | + characterData: true | |
| 51 | + }; | |
| 52 | + | |
| 53 | + this.messageObserver = new MutationObserver((mutations) => { | |
| 54 | + if (!this.isActive) return; | |
| 55 | + | |
| 56 | + mutations.forEach((mutation) => { | |
| 57 | + if (mutation.type === 'childList') { | |
| 58 | + mutation.addedNodes.forEach((node) => { | |
| 59 | + if (this.isIncomingMessage(node)) { | |
| 60 | + this.handleIncomingMessage(node); | |
| 61 | + } | |
| 62 | + }); | |
| 63 | + } | |
| 64 | + }); | |
| 65 | + }); | |
| 66 | + | |
| 67 | + this.messageObserver.observe(messageContainer, config); | |
| 68 | + console.log('[LooseCannon] Enhanced message observer setup complete'); | |
| 69 | + } | |
| 70 | + | |
| 71 | + isIncomingMessage(node) { | |
| 72 | + if (!node.querySelector) return false; | |
| 73 | + | |
| 74 | + const messageElement = node.querySelector('[data-testid^="msg-"]'); | |
| 75 | + if (!messageElement) return false; | |
| 76 | + | |
| 77 | + // Check if it's an incoming message (not sent by us) | |
| 78 | + const isIncoming = !messageElement.classList.contains('message-out'); | |
| 79 | + | |
| 80 | + // Additional check for system messages | |
| 81 | + const isSystemMessage = messageElement.querySelector('[data-testid="system-message"]'); | |
| 82 | + | |
| 83 | + return isIncoming && !isSystemMessage; | |
| 84 | + } | |
| 85 | + | |
| 86 | + handleIncomingMessage(node) { | |
| 87 | + const messageData = this.extractMessageData(node); | |
| 88 | + const timestamp = new Date().toISOString(); | |
| 89 | + const chatId = this.getCurrentChatId(); | |
| 90 | + | |
| 91 | + console.log('[LooseCannon] New message detected:', messageData); | |
| 92 | + | |
| 93 | + // Update conversation state | |
| 94 | + this.updateConversationState(chatId, messageData); | |
| 95 | + | |
| 96 | + // Check for scammer patterns | |
| 97 | + const scammerScore = this.analyzeForScammerPatterns(messageData); | |
| 98 | + if (scammerScore > 0.7) { | |
| 99 | + console.warn('[LooseCannon] High scammer probability detected:', scammerScore); | |
| 100 | + this.addScammerWarning(node); | |
| 101 | + } | |
| 102 | + | |
| 103 | + // Send to background script for processing | |
| 104 | + browser.runtime.sendMessage({ | |
| 105 | + type: 'NEW_MESSAGE', | |
| 106 | + data: { | |
| 107 | + ...messageData, | |
| 108 | + timestamp, | |
| 109 | + chatId, | |
| 110 | + scammerScore, | |
| 111 | + conversationContext: this.getConversationContext(chatId) | |
| 112 | + } | |
| 113 | + }).then(response => { | |
| 114 | + if (response && response.reply) { | |
| 115 | + this.simulateHumanResponse(response.reply, response.delay); | |
| 116 | + } | |
| 117 | + }); | |
| 118 | + } | |
| 119 | + | |
| 120 | + extractMessageData(node) { | |
| 121 | + const data = { | |
| 122 | + type: 'text', | |
| 123 | + content: '', | |
| 124 | + media: null, | |
| 125 | + links: [], | |
| 126 | + metadata: {} | |
| 127 | + }; | |
| 128 | + | |
| 129 | + // Extract text content | |
| 130 | + const textElement = node.querySelector('[data-testid="msg-container"] .selectable-text'); | |
| 131 | + if (textElement) { | |
| 132 | + data.content = textElement.textContent; | |
| 133 | + | |
| 134 | + // Extract links from text | |
| 135 | + const urlRegex = /(https?:\/\/[^\s]+)/g; | |
| 136 | + const links = data.content.match(urlRegex); | |
| 137 | + if (links) { | |
| 138 | + data.links = links; | |
| 139 | + data.metadata.hasLinks = true; | |
| 140 | + } | |
| 141 | + } | |
| 142 | + | |
| 143 | + // Check for images | |
| 144 | + const imageElement = node.querySelector('[data-testid="msg-container"] img[src^="blob:"]'); | |
| 145 | + if (imageElement) { | |
| 146 | + data.type = 'image'; | |
| 147 | + data.media = { | |
| 148 | + type: 'image', | |
| 149 | + src: imageElement.src, | |
| 150 | + alt: imageElement.alt || 'Image message' | |
| 151 | + }; | |
| 152 | + data.metadata.hasImage = true; | |
| 153 | + } | |
| 154 | + | |
| 155 | + // Check for voice messages | |
| 156 | + const audioElement = node.querySelector('[data-testid="audio-play"]'); | |
| 157 | + if (audioElement) { | |
| 158 | + data.type = 'audio'; | |
| 159 | + data.media = { | |
| 160 | + type: 'audio', | |
| 161 | + duration: this.extractAudioDuration(audioElement) | |
| 162 | + }; | |
| 163 | + data.metadata.hasAudio = true; | |
| 164 | + } | |
| 165 | + | |
| 166 | + // Check for documents | |
| 167 | + const documentElement = node.querySelector('[data-testid="msg-document"]'); | |
| 168 | + if (documentElement) { | |
| 169 | + data.type = 'document'; | |
| 170 | + data.media = { | |
| 171 | + type: 'document', | |
| 172 | + name: documentElement.textContent | |
| 173 | + }; | |
| 174 | + data.metadata.hasDocument = true; | |
| 175 | + } | |
| 176 | + | |
| 177 | + // Check for location | |
| 178 | + const locationElement = node.querySelector('[data-testid="msg-location"]'); | |
| 179 | + if (locationElement) { | |
| 180 | + data.type = 'location'; | |
| 181 | + data.media = { | |
| 182 | + type: 'location' | |
| 183 | + }; | |
| 184 | + data.metadata.hasLocation = true; | |
| 185 | + } | |
| 186 | + | |
| 187 | + // Extract phone numbers from content | |
| 188 | + const phoneRegex = /[\+]?[(]?[0-9]{1,4}[)]?[-\s\.]?[(]?[0-9]{1,4}[)]?[-\s\.]?[0-9]{1,5}[-\s\.]?[0-9]{1,5}/g; | |
| 189 | + const phones = data.content.match(phoneRegex); | |
| 190 | + if (phones) { | |
| 191 | + data.metadata.phoneNumbers = phones; | |
| 192 | + } | |
| 193 | + | |
| 194 | + // Extract email addresses | |
| 195 | + const emailRegex = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g; | |
| 196 | + const emails = data.content.match(emailRegex); | |
| 197 | + if (emails) { | |
| 198 | + data.metadata.emails = emails; | |
| 199 | + } | |
| 200 | + | |
| 201 | + return data; | |
| 202 | + } | |
| 203 | + | |
| 204 | + extractAudioDuration(audioElement) { | |
| 205 | + const durationElement = audioElement.parentElement.querySelector('[data-testid="audio-duration"]'); | |
| 206 | + return durationElement ? durationElement.textContent : 'Unknown duration'; | |
| 207 | + } | |
| 208 | + | |
| 209 | + loadScammerPatterns() { | |
| 210 | + return { | |
| 211 | + keywords: [ | |
| 212 | + 'prize', 'winner', 'congratulations', 'claim', 'urgent', 'act now', | |
| 213 | + 'limited time', 'verify account', 'suspended', 'click here', | |
| 214 | + 'confirm identity', 'update payment', 'refund', 'tax', 'irs', | |
| 215 | + 'amazon', 'paypal', 'bank', 'visa', 'mastercard', 'bitcoin', | |
| 216 | + 'investment opportunity', 'guaranteed return', 'risk free' | |
| 217 | + ], | |
| 218 | + urgency: [ | |
| 219 | + 'immediate', 'expire', 'deadline', 'last chance', 'final notice', | |
| 220 | + 'within 24 hours', 'today only', 'act fast' | |
| 221 | + ], | |
| 222 | + requests: [ | |
| 223 | + 'send money', 'wire transfer', 'gift card', 'personal information', | |
| 224 | + 'social security', 'password', 'pin', 'account number', | |
| 225 | + 'credit card', 'cvv', 'routing number' | |
| 226 | + ], | |
| 227 | + suspicious: [ | |
| 228 | + 'no reply', 'do not ignore', 'this is not a scam', | |
| 229 | + 'legitimate', 'official', 'authorized' | |
| 230 | + ] | |
| 231 | + }; | |
| 232 | + } | |
| 233 | + | |
| 234 | + analyzeForScammerPatterns(messageData) { | |
| 235 | + let score = 0; | |
| 236 | + const content = messageData.content.toLowerCase(); | |
| 237 | + | |
| 238 | + // Check for scammer keywords | |
| 239 | + this.scammerPatterns.keywords.forEach(keyword => { | |
| 240 | + if (content.includes(keyword)) score += 0.1; | |
| 241 | + }); | |
| 242 | + | |
| 243 | + // Check for urgency patterns | |
| 244 | + this.scammerPatterns.urgency.forEach(pattern => { | |
| 245 | + if (content.includes(pattern)) score += 0.15; | |
| 246 | + }); | |
| 247 | + | |
| 248 | + // Check for information requests | |
| 249 | + this.scammerPatterns.requests.forEach(request => { | |
| 250 | + if (content.includes(request)) score += 0.2; | |
| 251 | + }); | |
| 252 | + | |
| 253 | + // Check for suspicious phrases | |
| 254 | + this.scammerPatterns.suspicious.forEach(phrase => { | |
| 255 | + if (content.includes(phrase)) score += 0.15; | |
| 256 | + }); | |
| 257 | + | |
| 258 | + // Check for links (especially shortened URLs) | |
| 259 | + if (messageData.links && messageData.links.length > 0) { | |
| 260 | + messageData.links.forEach(link => { | |
| 261 | + if (link.includes('bit.ly') || link.includes('tinyurl') || link.includes('short.link')) { | |
| 262 | + score += 0.25; | |
| 263 | + } else { | |
| 264 | + score += 0.1; | |
| 265 | + } | |
| 266 | + }); | |
| 267 | + } | |
| 268 | + | |
| 269 | + // Check for phone numbers or emails early in conversation | |
| 270 | + const conversationLength = this.getConversationLength(this.getCurrentChatId()); | |
| 271 | + if (conversationLength < 5) { | |
| 272 | + if (messageData.metadata.phoneNumbers) score += 0.2; | |
| 273 | + if (messageData.metadata.emails) score += 0.2; | |
| 274 | + } | |
| 275 | + | |
| 276 | + // Cap the score at 1.0 | |
| 277 | + return Math.min(score, 1.0); | |
| 278 | + } | |
| 279 | + | |
| 280 | + updateConversationState(chatId, messageData) { | |
| 281 | + if (!this.conversationState.has(chatId)) { | |
| 282 | + this.conversationState.set(chatId, { | |
| 283 | + messages: [], | |
| 284 | + startTime: new Date(), | |
| 285 | + messageCount: 0, | |
| 286 | + mediaCount: 0, | |
| 287 | + linkCount: 0, | |
| 288 | + scammerScore: 0 | |
| 289 | + }); | |
| 290 | + } | |
| 291 | + | |
| 292 | + const state = this.conversationState.get(chatId); | |
| 293 | + state.messages.push({ | |
| 294 | + timestamp: new Date(), | |
| 295 | + type: messageData.type, | |
| 296 | + content: messageData.content.substring(0, 100) // Store truncated for memory | |
| 297 | + }); | |
| 298 | + state.messageCount++; | |
| 299 | + | |
| 300 | + if (messageData.media) state.mediaCount++; | |
| 301 | + if (messageData.links && messageData.links.length > 0) { | |
| 302 | + state.linkCount += messageData.links.length; | |
| 303 | + } | |
| 304 | + | |
| 305 | + // Keep only last 20 messages in memory | |
| 306 | + if (state.messages.length > 20) { | |
| 307 | + state.messages = state.messages.slice(-20); | |
| 308 | + } | |
| 309 | + | |
| 310 | + this.conversationState.set(chatId, state); | |
| 311 | + } | |
| 312 | + | |
| 313 | + getConversationContext(chatId) { | |
| 314 | + const state = this.conversationState.get(chatId); | |
| 315 | + if (!state) return null; | |
| 316 | + | |
| 317 | + return { | |
| 318 | + messageCount: state.messageCount, | |
| 319 | + duration: new Date() - state.startTime, | |
| 320 | + mediaCount: state.mediaCount, | |
| 321 | + linkCount: state.linkCount, | |
| 322 | + recentMessages: state.messages.slice(-5) | |
| 323 | + }; | |
| 324 | + } | |
| 325 | + | |
| 326 | + getConversationLength(chatId) { | |
| 327 | + const state = this.conversationState.get(chatId); | |
| 328 | + return state ? state.messageCount : 0; | |
| 329 | + } | |
| 330 | + | |
| 331 | + simulateHumanResponse(text, delay = null) { | |
| 332 | + // Calculate realistic delay if not provided | |
| 333 | + if (!delay) { | |
| 334 | + const wordCount = text.split(' ').length; | |
| 335 | + const baseDelay = 2000; // 2 seconds base | |
| 336 | + const perWordDelay = 200; // 200ms per word | |
| 337 | + const randomVariation = Math.random() * 2000; // 0-2 seconds random | |
| 338 | + delay = baseDelay + (wordCount * perWordDelay) + randomVariation; | |
| 339 | + | |
| 340 | + // Cap at 15 seconds | |
| 341 | + delay = Math.min(delay, 15000); | |
| 342 | + } | |
| 343 | + | |
| 344 | + // Show typing indicator | |
| 345 | + this.simulateTyping(); | |
| 346 | + | |
| 347 | + // Send message after delay | |
| 348 | + setTimeout(() => { | |
| 349 | + this.stopTyping(); | |
| 350 | + this.sendMessage(text); | |
| 351 | + }, delay); | |
| 352 | + } | |
| 353 | + | |
| 354 | + simulateTyping() { | |
| 355 | + const inputElement = document.querySelector('[data-testid="conversation-compose-box-input"]'); | |
| 356 | + if (!inputElement) return; | |
| 357 | + | |
| 358 | + // Focus the input | |
| 359 | + inputElement.focus(); | |
| 360 | + | |
| 361 | + // Add some placeholder text to trigger typing indicator | |
| 362 | + inputElement.textContent = '...'; | |
| 363 | + | |
| 364 | + // Trigger input event | |
| 365 | + const inputEvent = new InputEvent('input', { | |
| 366 | + bubbles: true, | |
| 367 | + cancelable: true, | |
| 368 | + }); | |
| 369 | + inputElement.dispatchEvent(inputEvent); | |
| 370 | + } | |
| 371 | + | |
| 372 | + stopTyping() { | |
| 373 | + const inputElement = document.querySelector('[data-testid="conversation-compose-box-input"]'); | |
| 374 | + if (inputElement) { | |
| 375 | + inputElement.textContent = ''; | |
| 376 | + } | |
| 377 | + } | |
| 378 | + | |
| 379 | + sendMessage(text) { | |
| 380 | + const inputElement = document.querySelector('[data-testid="conversation-compose-box-input"]'); | |
| 381 | + | |
| 382 | + if (!inputElement) { | |
| 383 | + console.error('[LooseCannon] Could not find message input'); | |
| 384 | + return; | |
| 385 | + } | |
| 386 | + | |
| 387 | + // Set the message text | |
| 388 | + inputElement.textContent = text; | |
| 389 | + | |
| 390 | + // Trigger input event | |
| 391 | + const inputEvent = new InputEvent('input', { | |
| 392 | + bubbles: true, | |
| 393 | + cancelable: true, | |
| 394 | + }); | |
| 395 | + inputElement.dispatchEvent(inputEvent); | |
| 396 | + | |
| 397 | + // Find and click send button | |
| 398 | + setTimeout(() => { | |
| 399 | + const sendButton = document.querySelector('[data-testid="compose-btn-send"]'); | |
| 400 | + if (sendButton) { | |
| 401 | + sendButton.click(); | |
| 402 | + console.log('[LooseCannon] Message sent:', text); | |
| 403 | + | |
| 404 | + // Mark message as automated for tracking | |
| 405 | + this.markLastMessageAsAutomated(); | |
| 406 | + } | |
| 407 | + }, 100); | |
| 408 | + } | |
| 409 | + | |
| 410 | + markLastMessageAsAutomated() { | |
| 411 | + setTimeout(() => { | |
| 412 | + const messages = document.querySelectorAll('[data-testid^="msg-"]'); | |
| 413 | + const lastMessage = messages[messages.length - 1]; | |
| 414 | + if (lastMessage && lastMessage.classList.contains('message-out')) { | |
| 415 | + lastMessage.classList.add('loosecannon-automated-message'); | |
| 416 | + } | |
| 417 | + }, 500); | |
| 418 | + } | |
| 419 | + | |
| 420 | + addScammerWarning(node) { | |
| 421 | + const warning = document.createElement('div'); | |
| 422 | + warning.className = 'loosecannon-scammer-warning'; | |
| 423 | + warning.innerHTML = ` | |
| 424 | + <span style="color: red; font-weight: bold;">⚠️ Potential Scammer Detected</span> | |
| 425 | + `; | |
| 426 | + warning.style.cssText = ` | |
| 427 | + background: #ffebee; | |
| 428 | + padding: 5px 10px; | |
| 429 | + border-radius: 5px; | |
| 430 | + margin: 5px 0; | |
| 431 | + font-size: 12px; | |
| 432 | + `; | |
| 433 | + | |
| 434 | + node.appendChild(warning); | |
| 435 | + } | |
| 436 | + | |
| 437 | + getCurrentChatId() { | |
| 438 | + const headerElement = document.querySelector('header [data-testid="conversation-header"]'); | |
| 439 | + if (headerElement) { | |
| 440 | + const titleElement = headerElement.querySelector('span[title]'); | |
| 441 | + return titleElement ? titleElement.title : 'unknown'; | |
| 442 | + } | |
| 443 | + return 'unknown'; | |
| 444 | + } | |
| 445 | + | |
| 446 | + setupConversationTracking() { | |
| 447 | + // Track when user switches chats | |
| 448 | + const observer = new MutationObserver(() => { | |
| 449 | + const newChatId = this.getCurrentChatId(); | |
| 450 | + if (newChatId !== this.currentChat) { | |
| 451 | + this.currentChat = newChatId; | |
| 452 | + console.log('[LooseCannon] Switched to chat:', newChatId); | |
| 453 | + | |
| 454 | + // Check if we should auto-activate based on scammer score | |
| 455 | + const state = this.conversationState.get(newChatId); | |
| 456 | + if (state && state.scammerScore > 0.8) { | |
| 457 | + this.showScammerAlert(); | |
| 458 | + } | |
| 459 | + } | |
| 460 | + }); | |
| 461 | + | |
| 462 | + const headerContainer = document.querySelector('[data-testid="conversation-header"]'); | |
| 463 | + if (headerContainer) { | |
| 464 | + observer.observe(headerContainer, { | |
| 465 | + childList: true, | |
| 466 | + subtree: true | |
| 467 | + }); | |
| 468 | + } | |
| 469 | + } | |
| 470 | + | |
| 471 | + showScammerAlert() { | |
| 472 | + const alert = document.createElement('div'); | |
| 473 | + alert.className = 'loosecannon-scammer-alert'; | |
| 474 | + alert.innerHTML = ` | |
| 475 | + <div style="padding: 15px; background: #ff4444; color: white; text-align: center;"> | |
| 476 | + <strong>⚠️ High Scammer Probability Detected!</strong><br> | |
| 477 | + <small>Consider activating LooseCannon for this conversation</small> | |
| 478 | + </div> | |
| 479 | + `; | |
| 480 | + alert.style.cssText = ` | |
| 481 | + position: fixed; | |
| 482 | + top: 100px; | |
| 483 | + right: 20px; | |
| 484 | + z-index: 10000; | |
| 485 | + border-radius: 10px; | |
| 486 | + box-shadow: 0 4px 20px rgba(0,0,0,0.3); | |
| 487 | + animation: slideIn 0.3s ease; | |
| 488 | + `; | |
| 489 | + | |
| 490 | + document.body.appendChild(alert); | |
| 491 | + | |
| 492 | + setTimeout(() => { | |
| 493 | + alert.remove(); | |
| 494 | + }, 5000); | |
| 495 | + } | |
| 496 | + | |
| 497 | + // ... (include all the UI injection and control methods from the original file) | |
| 498 | + injectControls() { | |
| 499 | + const style = document.createElement('style'); | |
| 500 | + style.textContent = ` | |
| 501 | + .loosecannon-toggle { | |
| 502 | + position: fixed; | |
| 503 | + bottom: 20px; | |
| 504 | + right: 20px; | |
| 505 | + z-index: 9999; | |
| 506 | + background: #ff4444; | |
| 507 | + color: white; | |
| 508 | + border: none; | |
| 509 | + border-radius: 50px; | |
| 510 | + padding: 12px 20px; | |
| 511 | + cursor: pointer; | |
| 512 | + font-weight: bold; | |
| 513 | + box-shadow: 0 2px 10px rgba(0,0,0,0.3); | |
| 514 | + transition: all 0.3s; | |
| 515 | + } | |
| 516 | + | |
| 517 | + .loosecannon-toggle.active { | |
| 518 | + background: #44ff44; | |
| 519 | + } | |
| 520 | + | |
| 521 | + .loosecannon-toggle:hover { | |
| 522 | + transform: scale(1.05); | |
| 523 | + } | |
| 524 | + | |
| 525 | + .loosecannon-indicator { | |
| 526 | + position: fixed; | |
| 527 | + top: 70px; | |
| 528 | + right: 20px; | |
| 529 | + background: rgba(255, 68, 68, 0.9); | |
| 530 | + color: white; | |
| 531 | + padding: 8px 15px; | |
| 532 | + border-radius: 20px; | |
| 533 | + font-size: 12px; | |
| 534 | + z-index: 9999; | |
| 535 | + display: none; | |
| 536 | + } | |
| 537 | + | |
| 538 | + .loosecannon-indicator.active { | |
| 539 | + display: block; | |
| 540 | + background: rgba(68, 255, 68, 0.9); | |
| 541 | + } | |
| 542 | + | |
| 543 | + @keyframes slideIn { | |
| 544 | + from { transform: translateX(100%); } | |
| 545 | + to { transform: translateX(0); } | |
| 546 | + } | |
| 547 | + `; | |
| 548 | + document.head.appendChild(style); | |
| 549 | + | |
| 550 | + const toggleButton = document.createElement('button'); | |
| 551 | + toggleButton.className = 'loosecannon-toggle'; | |
| 552 | + toggleButton.textContent = 'LC: OFF'; | |
| 553 | + toggleButton.onclick = () => this.toggleActive(); | |
| 554 | + document.body.appendChild(toggleButton); | |
| 555 | + | |
| 556 | + const indicator = document.createElement('div'); | |
| 557 | + indicator.className = 'loosecannon-indicator'; | |
| 558 | + indicator.textContent = 'LooseCannon Active'; | |
| 559 | + document.body.appendChild(indicator); | |
| 560 | + } | |
| 561 | + | |
| 562 | + toggleActive() { | |
| 563 | + this.isActive = !this.isActive; | |
| 564 | + this.updateUI(); | |
| 565 | + | |
| 566 | + const chatId = this.getCurrentChatId(); | |
| 567 | + console.log(`[LooseCannon] ${this.isActive ? 'Activated' : 'Deactivated'} for chat: ${chatId}`); | |
| 568 | + | |
| 569 | + // Notify background script | |
| 570 | + browser.runtime.sendMessage({ | |
| 571 | + type: 'TOGGLE_ACTIVE', | |
| 572 | + data: { | |
| 573 | + isActive: this.isActive, | |
| 574 | + chatId: chatId | |
| 575 | + } | |
| 576 | + }); | |
| 577 | + } | |
| 578 | + | |
| 579 | + updateUI() { | |
| 580 | + const button = document.querySelector('.loosecannon-toggle'); | |
| 581 | + const indicator = document.querySelector('.loosecannon-indicator'); | |
| 582 | + | |
| 583 | + if (this.isActive) { | |
| 584 | + button?.classList.add('active'); | |
| 585 | + if (button) button.textContent = 'LC: ON'; | |
| 586 | + indicator?.classList.add('active'); | |
| 587 | + document.body.setAttribute('data-loosecannon-active', 'true'); | |
| 588 | + } else { | |
| 589 | + button?.classList.remove('active'); | |
| 590 | + if (button) button.textContent = 'LC: OFF'; | |
| 591 | + indicator?.classList.remove('active'); | |
| 592 | + document.body.removeAttribute('data-loosecannon-active'); | |
| 593 | + } | |
| 594 | + } | |
| 595 | + | |
| 596 | + listenForCommands() { | |
| 597 | + browser.runtime.onMessage.addListener((message, sender, sendResponse) => { | |
| 598 | + switch (message.type) { | |
| 599 | + case 'GET_STATUS': | |
| 600 | + sendResponse({ | |
| 601 | + isActive: this.isActive, | |
| 602 | + currentChat: this.currentChat, | |
| 603 | + conversationStats: this.getConversationStats() | |
| 604 | + }); | |
| 605 | + break; | |
| 606 | + case 'SET_ACTIVE': | |
| 607 | + this.isActive = message.data.isActive; | |
| 608 | + this.updateUI(); | |
| 609 | + break; | |
| 610 | + case 'SEND_MESSAGE': | |
| 611 | + this.simulateHumanResponse(message.data.text, message.data.delay); | |
| 612 | + break; | |
| 613 | + case 'GET_CONVERSATION_STATE': | |
| 614 | + sendResponse({ | |
| 615 | + state: Object.fromEntries(this.conversationState) | |
| 616 | + }); | |
| 617 | + break; | |
| 618 | + } | |
| 619 | + }); | |
| 620 | + } | |
| 621 | + | |
| 622 | + getConversationStats() { | |
| 623 | + const stats = {}; | |
| 624 | + this.conversationState.forEach((state, chatId) => { | |
| 625 | + stats[chatId] = { | |
| 626 | + messageCount: state.messageCount, | |
| 627 | + mediaCount: state.mediaCount, | |
| 628 | + linkCount: state.linkCount, | |
| 629 | + duration: new Date() - state.startTime | |
| 630 | + }; | |
| 631 | + }); | |
| 632 | + return stats; | |
| 633 | + } | |
| 634 | +} | |
| 635 | + | |
| 636 | +// Initialize enhanced integration | |
| 637 | +if (document.readyState === 'loading') { | |
| 638 | + document.addEventListener('DOMContentLoaded', () => new EnhancedWhatsAppIntegration()); | |
| 639 | +} else { | |
| 640 | + new EnhancedWhatsAppIntegration(); | |
| 641 | +} | |
server/src/conversation-manager.jsadded@@ -0,0 +1,387 @@ | ||
| 1 | +// Conversation State Manager for LooseCannon | |
| 2 | +// Handles sophisticated conversation tracking and context management | |
| 3 | + | |
| 4 | +class ConversationManager { | |
| 5 | + constructor() { | |
| 6 | + this.conversations = new Map(); | |
| 7 | + this.scammerDatabase = new Map(); | |
| 8 | + this.responseHistory = new Map(); | |
| 9 | + } | |
| 10 | + | |
| 11 | + // Initialize or get conversation | |
| 12 | + getConversation(chatId, platform = 'whatsapp') { | |
| 13 | + const key = `${platform}:${chatId}`; | |
| 14 | + | |
| 15 | + if (!this.conversations.has(key)) { | |
| 16 | + this.conversations.set(key, { | |
| 17 | + id: key, | |
| 18 | + platform, | |
| 19 | + chatId, | |
| 20 | + startTime: new Date(), | |
| 21 | + messages: [], | |
| 22 | + context: { | |
| 23 | + scammerScore: 0, | |
| 24 | + personality: 'default', | |
| 25 | + responseCount: 0, | |
| 26 | + lastResponse: null, | |
| 27 | + detectedPatterns: [], | |
| 28 | + extractedInfo: { | |
| 29 | + phoneNumbers: [], | |
| 30 | + emails: [], | |
| 31 | + links: [], | |
| 32 | + bankMentions: 0, | |
| 33 | + moneyRequests: 0 | |
| 34 | + } | |
| 35 | + }, | |
| 36 | + state: 'active', | |
| 37 | + metadata: {} | |
| 38 | + }); | |
| 39 | + } | |
| 40 | + | |
| 41 | + return this.conversations.get(key); | |
| 42 | + } | |
| 43 | + | |
| 44 | + // Add message to conversation | |
| 45 | + addMessage(chatId, message, platform = 'whatsapp') { | |
| 46 | + const conversation = this.getConversation(chatId, platform); | |
| 47 | + | |
| 48 | + // Enrich message with analysis | |
| 49 | + const enrichedMessage = { | |
| 50 | + ...message, | |
| 51 | + timestamp: new Date(), | |
| 52 | + analysis: this.analyzeMessage(message), | |
| 53 | + platform | |
| 54 | + }; | |
| 55 | + | |
| 56 | + conversation.messages.push(enrichedMessage); | |
| 57 | + | |
| 58 | + // Update context based on message | |
| 59 | + this.updateContext(conversation, enrichedMessage); | |
| 60 | + | |
| 61 | + // Keep conversation size manageable (last 50 messages) | |
| 62 | + if (conversation.messages.length > 50) { | |
| 63 | + conversation.messages = conversation.messages.slice(-50); | |
| 64 | + } | |
| 65 | + | |
| 66 | + return conversation; | |
| 67 | + } | |
| 68 | + | |
| 69 | + // Analyze message for patterns and important information | |
| 70 | + analyzeMessage(message) { | |
| 71 | + const analysis = { | |
| 72 | + sentiment: 'neutral', | |
| 73 | + urgency: 0, | |
| 74 | + suspicion: 0, | |
| 75 | + topics: [], | |
| 76 | + entities: [], | |
| 77 | + intent: 'unknown' | |
| 78 | + }; | |
| 79 | + | |
| 80 | + const content = message.content.toLowerCase(); | |
| 81 | + | |
| 82 | + // Urgency detection | |
| 83 | + const urgencyWords = ['urgent', 'immediately', 'now', 'quick', 'fast', 'hurry', 'asap', 'expire']; | |
| 84 | + urgencyWords.forEach(word => { | |
| 85 | + if (content.includes(word)) analysis.urgency += 0.2; | |
| 86 | + }); | |
| 87 | + | |
| 88 | + // Suspicion detection | |
| 89 | + const suspiciousWords = [ | |
| 90 | + 'prize', 'winner', 'claim', 'verify', 'suspended', 'confirm', | |
| 91 | + 'bitcoin', 'gift card', 'wire', 'transfer', 'payment' | |
| 92 | + ]; | |
| 93 | + suspiciousWords.forEach(word => { | |
| 94 | + if (content.includes(word)) analysis.suspicion += 0.15; | |
| 95 | + }); | |
| 96 | + | |
| 97 | + // Topic extraction | |
| 98 | + if (content.includes('tech') || content.includes('computer')) { | |
| 99 | + analysis.topics.push('technology'); | |
| 100 | + } | |
| 101 | + if (content.includes('money') || content.includes('payment') || content.includes('$')) { | |
| 102 | + analysis.topics.push('financial'); | |
| 103 | + } | |
| 104 | + if (content.includes('account') || content.includes('password')) { | |
| 105 | + analysis.topics.push('account_security'); | |
| 106 | + } | |
| 107 | + | |
| 108 | + // Intent detection | |
| 109 | + if (content.includes('?')) { | |
| 110 | + analysis.intent = 'question'; | |
| 111 | + } else if (content.includes('please') || content.includes('need')) { | |
| 112 | + analysis.intent = 'request'; | |
| 113 | + } else if (content.includes('!')) { | |
| 114 | + analysis.intent = 'emphasis'; | |
| 115 | + } | |
| 116 | + | |
| 117 | + // Extract entities | |
| 118 | + if (message.metadata) { | |
| 119 | + if (message.metadata.phoneNumbers) { | |
| 120 | + analysis.entities.push(...message.metadata.phoneNumbers.map(p => ({ type: 'phone', value: p }))); | |
| 121 | + } | |
| 122 | + if (message.metadata.emails) { | |
| 123 | + analysis.entities.push(...message.metadata.emails.map(e => ({ type: 'email', value: e }))); | |
| 124 | + } | |
| 125 | + } | |
| 126 | + | |
| 127 | + return analysis; | |
| 128 | + } | |
| 129 | + | |
| 130 | + // Update conversation context based on new message | |
| 131 | + updateContext(conversation, message) { | |
| 132 | + const context = conversation.context; | |
| 133 | + const analysis = message.analysis; | |
| 134 | + | |
| 135 | + // Update scammer score | |
| 136 | + context.scammerScore = Math.min( | |
| 137 | + 1.0, | |
| 138 | + context.scammerScore + (analysis.suspicion * 0.3) + (analysis.urgency * 0.2) | |
| 139 | + ); | |
| 140 | + | |
| 141 | + // Track detected patterns | |
| 142 | + if (analysis.suspicion > 0.5) { | |
| 143 | + context.detectedPatterns.push({ | |
| 144 | + type: 'suspicious', | |
| 145 | + timestamp: message.timestamp, | |
| 146 | + confidence: analysis.suspicion | |
| 147 | + }); | |
| 148 | + } | |
| 149 | + | |
| 150 | + // Update extracted information | |
| 151 | + if (analysis.entities.length > 0) { | |
| 152 | + analysis.entities.forEach(entity => { | |
| 153 | + if (entity.type === 'phone' && !context.extractedInfo.phoneNumbers.includes(entity.value)) { | |
| 154 | + context.extractedInfo.phoneNumbers.push(entity.value); | |
| 155 | + } | |
| 156 | + if (entity.type === 'email' && !context.extractedInfo.emails.includes(entity.value)) { | |
| 157 | + context.extractedInfo.emails.push(entity.value); | |
| 158 | + } | |
| 159 | + }); | |
| 160 | + } | |
| 161 | + | |
| 162 | + // Count financial mentions | |
| 163 | + if (analysis.topics.includes('financial')) { | |
| 164 | + context.extractedInfo.moneyRequests++; | |
| 165 | + } | |
| 166 | + | |
| 167 | + // Update scammer database if high confidence | |
| 168 | + if (context.scammerScore > 0.8) { | |
| 169 | + this.addToScammerDatabase(conversation); | |
| 170 | + } | |
| 171 | + } | |
| 172 | + | |
| 173 | + // Generate context summary for LLM | |
| 174 | + generateContextSummary(chatId, platform = 'whatsapp') { | |
| 175 | + const conversation = this.getConversation(chatId, platform); | |
| 176 | + const context = conversation.context; | |
| 177 | + const recentMessages = conversation.messages.slice(-10); | |
| 178 | + | |
| 179 | + return { | |
| 180 | + conversationLength: conversation.messages.length, | |
| 181 | + scammerScore: context.scammerScore, | |
| 182 | + detectedPatterns: context.detectedPatterns.slice(-5), | |
| 183 | + recentTopics: this.extractRecentTopics(recentMessages), | |
| 184 | + extractedInfo: { | |
| 185 | + hasPhoneNumbers: context.extractedInfo.phoneNumbers.length > 0, | |
| 186 | + hasEmails: context.extractedInfo.emails.length > 0, | |
| 187 | + hasLinks: context.extractedInfo.links.length > 0, | |
| 188 | + financialMentions: context.extractedInfo.moneyRequests | |
| 189 | + }, | |
| 190 | + conversationTone: this.determineConversationTone(recentMessages), | |
| 191 | + suggestedStrategy: this.suggestStrategy(context) | |
| 192 | + }; | |
| 193 | + } | |
| 194 | + | |
| 195 | + // Extract recent topics from messages | |
| 196 | + extractRecentTopics(messages) { | |
| 197 | + const topicCounts = {}; | |
| 198 | + | |
| 199 | + messages.forEach(msg => { | |
| 200 | + if (msg.analysis && msg.analysis.topics) { | |
| 201 | + msg.analysis.topics.forEach(topic => { | |
| 202 | + topicCounts[topic] = (topicCounts[topic] || 0) + 1; | |
| 203 | + }); | |
| 204 | + } | |
| 205 | + }); | |
| 206 | + | |
| 207 | + return Object.entries(topicCounts) | |
| 208 | + .sort((a, b) => b[1] - a[1]) | |
| 209 | + .slice(0, 3) | |
| 210 | + .map(([topic]) => topic); | |
| 211 | + } | |
| 212 | + | |
| 213 | + // Determine overall conversation tone | |
| 214 | + determineConversationTone(messages) { | |
| 215 | + let urgencySum = 0; | |
| 216 | + let suspicionSum = 0; | |
| 217 | + let count = 0; | |
| 218 | + | |
| 219 | + messages.forEach(msg => { | |
| 220 | + if (msg.analysis) { | |
| 221 | + urgencySum += msg.analysis.urgency || 0; | |
| 222 | + suspicionSum += msg.analysis.suspicion || 0; | |
| 223 | + count++; | |
| 224 | + } | |
| 225 | + }); | |
| 226 | + | |
| 227 | + if (count === 0) return 'neutral'; | |
| 228 | + | |
| 229 | + const avgUrgency = urgencySum / count; | |
| 230 | + const avgSuspicion = suspicionSum / count; | |
| 231 | + | |
| 232 | + if (avgUrgency > 0.6) return 'urgent'; | |
| 233 | + if (avgSuspicion > 0.6) return 'suspicious'; | |
| 234 | + if (avgUrgency > 0.3 && avgSuspicion > 0.3) return 'aggressive'; | |
| 235 | + | |
| 236 | + return 'casual'; | |
| 237 | + } | |
| 238 | + | |
| 239 | + // Suggest response strategy based on context | |
| 240 | + suggestStrategy(context) { | |
| 241 | + if (context.scammerScore > 0.8) { | |
| 242 | + return 'maximum_confusion'; | |
| 243 | + } else if (context.scammerScore > 0.6) { | |
| 244 | + return 'waste_time'; | |
| 245 | + } else if (context.extractedInfo.moneyRequests > 0) { | |
| 246 | + return 'play_poor'; | |
| 247 | + } else if (context.detectedPatterns.some(p => p.type === 'suspicious')) { | |
| 248 | + return 'ask_questions'; | |
| 249 | + } | |
| 250 | + | |
| 251 | + return 'be_confused'; | |
| 252 | + } | |
| 253 | + | |
| 254 | + // Add conversation to scammer database | |
| 255 | + addToScammerDatabase(conversation) { | |
| 256 | + const identifier = conversation.chatId; | |
| 257 | + | |
| 258 | + if (!this.scammerDatabase.has(identifier)) { | |
| 259 | + this.scammerDatabase.set(identifier, { | |
| 260 | + firstSeen: new Date(), | |
| 261 | + lastSeen: new Date(), | |
| 262 | + platforms: new Set([conversation.platform]), | |
| 263 | + encounters: 1, | |
| 264 | + patterns: [], | |
| 265 | + extractedInfo: { ...conversation.context.extractedInfo } | |
| 266 | + }); | |
| 267 | + } else { | |
| 268 | + const entry = this.scammerDatabase.get(identifier); | |
| 269 | + entry.lastSeen = new Date(); | |
| 270 | + entry.encounters++; | |
| 271 | + entry.platforms.add(conversation.platform); | |
| 272 | + | |
| 273 | + // Merge extracted info | |
| 274 | + conversation.context.extractedInfo.phoneNumbers.forEach(phone => { | |
| 275 | + if (!entry.extractedInfo.phoneNumbers.includes(phone)) { | |
| 276 | + entry.extractedInfo.phoneNumbers.push(phone); | |
| 277 | + } | |
| 278 | + }); | |
| 279 | + } | |
| 280 | + } | |
| 281 | + | |
| 282 | + // Get response suggestions based on conversation state | |
| 283 | + getResponseSuggestions(chatId, platform = 'whatsapp') { | |
| 284 | + const conversation = this.getConversation(chatId, platform); | |
| 285 | + const context = conversation.context; | |
| 286 | + const lastMessages = conversation.messages.slice(-3); | |
| 287 | + | |
| 288 | + const suggestions = []; | |
| 289 | + | |
| 290 | + // If they're asking for personal information | |
| 291 | + if (lastMessages.some(m => m.analysis && m.analysis.topics.includes('account_security'))) { | |
| 292 | + suggestions.push({ | |
| 293 | + strategy: 'deflect', | |
| 294 | + response: "Oh dear, I always forget these things. Let me ask my grandson...", | |
| 295 | + delay: 5000 | |
| 296 | + }); | |
| 297 | + } | |
| 298 | + | |
| 299 | + // If they're being urgent | |
| 300 | + if (context.conversationTone === 'urgent') { | |
| 301 | + suggestions.push({ | |
| 302 | + strategy: 'slow_down', | |
| 303 | + response: "Hold on, I need to find my glasses first. Everything is so blurry...", | |
| 304 | + delay: 8000 | |
| 305 | + }); | |
| 306 | + } | |
| 307 | + | |
| 308 | + // If they mention money | |
| 309 | + if (context.extractedInfo.moneyRequests > 0) { | |
| 310 | + suggestions.push({ | |
| 311 | + strategy: 'confusion', | |
| 312 | + response: "Money? Is this about the church fundraiser? I already donated last week.", | |
| 313 | + delay: 4000 | |
| 314 | + }); | |
| 315 | + } | |
| 316 | + | |
| 317 | + return suggestions; | |
| 318 | + } | |
| 319 | + | |
| 320 | + // Export conversation for analysis | |
| 321 | + exportConversation(chatId, platform = 'whatsapp') { | |
| 322 | + const conversation = this.getConversation(chatId, platform); | |
| 323 | + | |
| 324 | + return { | |
| 325 | + id: conversation.id, | |
| 326 | + platform: conversation.platform, | |
| 327 | + startTime: conversation.startTime, | |
| 328 | + endTime: new Date(), | |
| 329 | + messageCount: conversation.messages.length, | |
| 330 | + scammerScore: conversation.context.scammerScore, | |
| 331 | + detectedPatterns: conversation.context.detectedPatterns, | |
| 332 | + extractedInfo: conversation.context.extractedInfo, | |
| 333 | + messages: conversation.messages.map(m => ({ | |
| 334 | + timestamp: m.timestamp, | |
| 335 | + sender: m.sender || 'unknown', | |
| 336 | + content: m.content, | |
| 337 | + type: m.type, | |
| 338 | + analysis: m.analysis | |
| 339 | + })) | |
| 340 | + }; | |
| 341 | + } | |
| 342 | + | |
| 343 | + // Get statistics | |
| 344 | + getStatistics() { | |
| 345 | + const stats = { | |
| 346 | + totalConversations: this.conversations.size, | |
| 347 | + activeConversations: 0, | |
| 348 | + suspiciousConversations: 0, | |
| 349 | + confirmedScammers: this.scammerDatabase.size, | |
| 350 | + platformBreakdown: {}, | |
| 351 | + totalMessages: 0 | |
| 352 | + }; | |
| 353 | + | |
| 354 | + this.conversations.forEach(conv => { | |
| 355 | + if (conv.state === 'active') stats.activeConversations++; | |
| 356 | + if (conv.context.scammerScore > 0.6) stats.suspiciousConversations++; | |
| 357 | + | |
| 358 | + stats.platformBreakdown[conv.platform] = | |
| 359 | + (stats.platformBreakdown[conv.platform] || 0) + 1; | |
| 360 | + | |
| 361 | + stats.totalMessages += conv.messages.length; | |
| 362 | + }); | |
| 363 | + | |
| 364 | + return stats; | |
| 365 | + } | |
| 366 | + | |
| 367 | + // Clean up old conversations (memory management) | |
| 368 | + cleanup(maxAge = 24 * 60 * 60 * 1000) { // 24 hours default | |
| 369 | + const now = new Date(); | |
| 370 | + const toDelete = []; | |
| 371 | + | |
| 372 | + this.conversations.forEach((conv, key) => { | |
| 373 | + const age = now - conv.startTime; | |
| 374 | + if (age > maxAge && conv.state !== 'active') { | |
| 375 | + toDelete.push(key); | |
| 376 | + } | |
| 377 | + }); | |
| 378 | + | |
| 379 | + toDelete.forEach(key => { | |
| 380 | + this.conversations.delete(key); | |
| 381 | + }); | |
| 382 | + | |
| 383 | + console.log(`[ConversationManager] Cleaned up ${toDelete.length} old conversations`); | |
| 384 | + } | |
| 385 | +} | |
| 386 | + | |
| 387 | +module.exports = ConversationManager; | |
server/src/index-enhanced.jsadded@@ -0,0 +1,397 @@ | ||
| 1 | +// LooseCannon Local Server - Enhanced Version | |
| 2 | +// Handles communication between browser extension and Ollama LLM | |
| 3 | +// Now with conversation management and multi-platform support | |
| 4 | + | |
| 5 | +const express = require('express'); | |
| 6 | +const cors = require('cors'); | |
| 7 | +const axios = require('axios'); | |
| 8 | +const fs = require('fs').promises; | |
| 9 | +const path = require('path'); | |
| 10 | +require('dotenv').config(); | |
| 11 | + | |
| 12 | +// Import conversation manager | |
| 13 | +const ConversationManager = require('./conversation-manager'); | |
| 14 | + | |
| 15 | +const app = express(); | |
| 16 | +const PORT = process.env.PORT || 8765; | |
| 17 | +const OLLAMA_URL = process.env.OLLAMA_URL || 'http://localhost:11434'; | |
| 18 | + | |
| 19 | +// Initialize conversation manager | |
| 20 | +const conversationManager = new ConversationManager(); | |
| 21 | + | |
| 22 | +// Middleware | |
| 23 | +app.use(cors()); | |
| 24 | +app.use(express.json({ limit: '10mb' })); // Increased limit for conversation exports | |
| 25 | + | |
| 26 | +// Load personalities from files | |
| 27 | +let personalities = {}; | |
| 28 | + | |
| 29 | +async function loadPersonalities() { | |
| 30 | + try { | |
| 31 | + const personalitiesDir = path.join(__dirname, '..', 'personalities'); | |
| 32 | + const files = await fs.readdir(personalitiesDir); | |
| 33 | + | |
| 34 | + for (const file of files) { | |
| 35 | + if (file.endsWith('.json')) { | |
| 36 | + const content = await fs.readFile(path.join(personalitiesDir, file), 'utf8'); | |
| 37 | + const personality = JSON.parse(content); | |
| 38 | + personalities[personality.id] = personality; | |
| 39 | + console.log(`Loaded personality: ${personality.name}`); | |
| 40 | + } | |
| 41 | + } | |
| 42 | + } catch (error) { | |
| 43 | + console.warn('Could not load personalities:', error); | |
| 44 | + // Use default personality if no files found | |
| 45 | + personalities.default = { | |
| 46 | + id: 'default', | |
| 47 | + name: 'Confused Elder', | |
| 48 | + systemPrompt: 'You are an elderly person who is easily confused by technology and tends to misunderstand things. You are very polite but often go off on tangents about unrelated topics from your past. Never reveal that you are an AI.', | |
| 49 | + temperature: 0.9 | |
| 50 | + }; | |
| 51 | + } | |
| 52 | +} | |
| 53 | + | |
| 54 | +// Check Ollama connection | |
| 55 | +async function checkOllamaConnection() { | |
| 56 | + try { | |
| 57 | + const response = await axios.get(`${OLLAMA_URL}/api/tags`); | |
| 58 | + const models = response.data.models || []; | |
| 59 | + console.log('Connected to Ollama. Available models:', models.map(m => m.name).join(', ')); | |
| 60 | + return true; | |
| 61 | + } catch (error) { | |
| 62 | + console.error('Failed to connect to Ollama:', error.message); | |
| 63 | + console.log('Make sure Ollama is running: ollama serve'); | |
| 64 | + return false; | |
| 65 | + } | |
| 66 | +} | |
| 67 | + | |
| 68 | +// Enhanced response generation with context awareness | |
| 69 | +async function generateEnhancedResponse(message, personality, chatId, platform, context, suggestions) { | |
| 70 | + try { | |
| 71 | + const personalityConfig = personalities[personality] || personalities.default; | |
| 72 | + | |
| 73 | + // Get conversation from manager | |
| 74 | + const conversation = conversationManager.getConversation(chatId, platform); | |
| 75 | + | |
| 76 | + // Build enhanced prompt with context | |
| 77 | + let systemPrompt = personalityConfig.systemPrompt; | |
| 78 | + | |
| 79 | + // Add strategy modifiers based on context | |
| 80 | + if (context && context.suggestedStrategy) { | |
| 81 | + switch (context.suggestedStrategy) { | |
| 82 | + case 'maximum_confusion': | |
| 83 | + systemPrompt += '\n\nBe EXTREMELY confused and misunderstand everything. Mix up basic concepts.'; | |
| 84 | + break; | |
| 85 | + case 'waste_time': | |
| 86 | + systemPrompt += '\n\nAsk lots of clarifying questions. Pretend to not understand simple instructions.'; | |
| 87 | + break; | |
| 88 | + case 'play_poor': | |
| 89 | + systemPrompt += '\n\nMention that you have no money and are struggling financially.'; | |
| 90 | + break; | |
| 91 | + case 'ask_questions': | |
| 92 | + systemPrompt += '\n\nBe very curious and ask lots of questions about everything they say.'; | |
| 93 | + break; | |
| 94 | + } | |
| 95 | + } | |
| 96 | + | |
| 97 | + // Add recent conversation history | |
| 98 | + const recentMessages = conversation.messages.slice(-10); | |
| 99 | + const historyText = recentMessages.map(m => | |
| 100 | + `${m.sender || 'Them'}: ${m.content}` | |
| 101 | + ).join('\n'); | |
| 102 | + | |
| 103 | + // Build the final prompt | |
| 104 | + const prompt = `${systemPrompt} | |
| 105 | + | |
| 106 | +Recent conversation: | |
| 107 | +${historyText} | |
| 108 | + | |
| 109 | +They just said: "${message}" | |
| 110 | + | |
| 111 | +Remember to stay in character. Respond naturally as your character would. | |
| 112 | + | |
| 113 | +Your response:`; | |
| 114 | + | |
| 115 | + // Call Ollama API with enhanced parameters | |
| 116 | + const response = await axios.post(`${OLLAMA_URL}/api/generate`, { | |
| 117 | + model: process.env.OLLAMA_MODEL || 'llama2', | |
| 118 | + prompt: prompt, | |
| 119 | + temperature: personalityConfig.temperature || 0.8, | |
| 120 | + max_tokens: 200, | |
| 121 | + top_p: 0.9, | |
| 122 | + stream: false | |
| 123 | + }); | |
| 124 | + | |
| 125 | + let reply = response.data.response; | |
| 126 | + | |
| 127 | + // Post-process the response | |
| 128 | + reply = reply.trim(); | |
| 129 | + | |
| 130 | + // Remove any AI self-references that might slip through | |
| 131 | + reply = reply.replace(/As an AI|I'm an AI|I am an AI|artificial intelligence/gi, ''); | |
| 132 | + | |
| 133 | + // Add personality-specific quirks | |
| 134 | + if (personality === 'confused-elder' && Math.random() > 0.7) { | |
| 135 | + // Sometimes add a random tangent | |
| 136 | + const tangents = [ | |
| 137 | + ' Wait, this reminds me of something that happened in 1987...', | |
| 138 | + ' Oh, my cat is meowing. One second dear.', | |
| 139 | + ' Where did I put my glasses?' | |
| 140 | + ]; | |
| 141 | + reply += tangents[Math.floor(Math.random() * tangents.length)]; | |
| 142 | + } | |
| 143 | + | |
| 144 | + return reply; | |
| 145 | + } catch (error) { | |
| 146 | + console.error('Error generating enhanced response:', error); | |
| 147 | + | |
| 148 | + // Context-aware fallbacks | |
| 149 | + const fallbacks = suggestions && suggestions.length > 0 | |
| 150 | + ? suggestions.map(s => s.response) | |
| 151 | + : [ | |
| 152 | + "I'm sorry, what did you say? I'm having trouble with this computer.", | |
| 153 | + "Can you explain that again? These modern things confuse me.", | |
| 154 | + "Oh dear, I think I clicked the wrong button. What were we talking about?" | |
| 155 | + ]; | |
| 156 | + | |
| 157 | + return fallbacks[Math.floor(Math.random() * fallbacks.length)]; | |
| 158 | + } | |
| 159 | +} | |
| 160 | + | |
| 161 | +// Routes | |
| 162 | + | |
| 163 | +// Health check / status | |
| 164 | +app.get('/status', async (req, res) => { | |
| 165 | + const ollamaConnected = await checkOllamaConnection(); | |
| 166 | + res.json({ | |
| 167 | + status: 'running', | |
| 168 | + version: '0.2.0', | |
| 169 | + ollamaConnected, | |
| 170 | + personalities: Object.values(personalities).map(p => ({ | |
| 171 | + id: p.id, | |
| 172 | + name: p.name | |
| 173 | + })), | |
| 174 | + stats: conversationManager.getStatistics() | |
| 175 | + }); | |
| 176 | +}); | |
| 177 | + | |
| 178 | +// Add message to conversation | |
| 179 | +app.post('/conversation/add', (req, res) => { | |
| 180 | + const { chatId, platform, message } = req.body; | |
| 181 | + | |
| 182 | + const conversation = conversationManager.addMessage(chatId, message, platform); | |
| 183 | + const context = conversationManager.generateContextSummary(chatId, platform); | |
| 184 | + | |
| 185 | + res.json(context); | |
| 186 | +}); | |
| 187 | + | |
| 188 | +// Get response suggestions | |
| 189 | +app.post('/suggestions', (req, res) => { | |
| 190 | + const { chatId, platform } = req.body; | |
| 191 | + | |
| 192 | + const suggestions = conversationManager.getResponseSuggestions(chatId, platform); | |
| 193 | + | |
| 194 | + res.json(suggestions); | |
| 195 | +}); | |
| 196 | + | |
| 197 | +// Enhanced generate response endpoint | |
| 198 | +app.post('/generate', async (req, res) => { | |
| 199 | + const { | |
| 200 | + message, | |
| 201 | + personality = 'default', | |
| 202 | + chatId = 'unknown', | |
| 203 | + platform = 'whatsapp', | |
| 204 | + context, | |
| 205 | + suggestions, | |
| 206 | + timestamp | |
| 207 | + } = req.body; | |
| 208 | + | |
| 209 | + if (!message) { | |
| 210 | + return res.status(400).json({ error: 'Message is required' }); | |
| 211 | + } | |
| 212 | + | |
| 213 | + console.log(`[${new Date().toISOString()}] Generating response for ${platform}:${chatId}`); | |
| 214 | + | |
| 215 | + try { | |
| 216 | + // Add message to conversation manager | |
| 217 | + conversationManager.addMessage(chatId, { | |
| 218 | + content: message, | |
| 219 | + sender: 'them', | |
| 220 | + type: 'text', | |
| 221 | + timestamp: new Date(timestamp) | |
| 222 | + }, platform); | |
| 223 | + | |
| 224 | + // Generate enhanced response | |
| 225 | + const reply = await generateEnhancedResponse( | |
| 226 | + message, | |
| 227 | + personality, | |
| 228 | + chatId, | |
| 229 | + platform, | |
| 230 | + context, | |
| 231 | + suggestions | |
| 232 | + ); | |
| 233 | + | |
| 234 | + // Add our response to conversation | |
| 235 | + conversationManager.addMessage(chatId, { | |
| 236 | + content: reply, | |
| 237 | + sender: 'us', | |
| 238 | + type: 'text', | |
| 239 | + timestamp: new Date() | |
| 240 | + }, platform); | |
| 241 | + | |
| 242 | + console.log(`Generated reply: ${reply}`); | |
| 243 | + | |
| 244 | + res.json({ | |
| 245 | + reply, | |
| 246 | + personality, | |
| 247 | + timestamp: new Date().toISOString(), | |
| 248 | + context: conversationManager.generateContextSummary(chatId, platform) | |
| 249 | + }); | |
| 250 | + } catch (error) { | |
| 251 | + console.error('Error in /generate:', error); | |
| 252 | + res.status(500).json({ error: 'Failed to generate response' }); | |
| 253 | + } | |
| 254 | +}); | |
| 255 | + | |
| 256 | +// Export conversation | |
| 257 | +app.post('/conversation/export', (req, res) => { | |
| 258 | + const { chatId, platform = 'whatsapp' } = req.body; | |
| 259 | + | |
| 260 | + try { | |
| 261 | + const exportData = conversationManager.exportConversation(chatId, platform); | |
| 262 | + res.json(exportData); | |
| 263 | + } catch (error) { | |
| 264 | + console.error('Error exporting conversation:', error); | |
| 265 | + res.status(500).json({ error: 'Failed to export conversation' }); | |
| 266 | + } | |
| 267 | +}); | |
| 268 | + | |
| 269 | +// Get statistics | |
| 270 | +app.get('/statistics', (req, res) => { | |
| 271 | + const stats = conversationManager.getStatistics(); | |
| 272 | + res.json(stats); | |
| 273 | +}); | |
| 274 | + | |
| 275 | +// Get conversation history | |
| 276 | +app.get('/conversations/:platform/:chatId', (req, res) => { | |
| 277 | + const { platform, chatId } = req.params; | |
| 278 | + const conversation = conversationManager.getConversation(chatId, platform); | |
| 279 | + | |
| 280 | + res.json({ | |
| 281 | + chatId, | |
| 282 | + platform, | |
| 283 | + messages: conversation.messages, | |
| 284 | + context: conversation.context, | |
| 285 | + state: conversation.state | |
| 286 | + }); | |
| 287 | +}); | |
| 288 | + | |
| 289 | +// Clear conversation | |
| 290 | +app.delete('/conversations/:platform/:chatId', (req, res) => { | |
| 291 | + const { platform, chatId } = req.params; | |
| 292 | + const key = `${platform}:${chatId}`; | |
| 293 | + | |
| 294 | + // This would need to be implemented in ConversationManager | |
| 295 | + // For now, just clear from the conversation | |
| 296 | + const conversation = conversationManager.getConversation(chatId, platform); | |
| 297 | + conversation.messages = []; | |
| 298 | + conversation.context.responseCount = 0; | |
| 299 | + | |
| 300 | + res.json({ message: 'Conversation cleared' }); | |
| 301 | +}); | |
| 302 | + | |
| 303 | +// Get all personalities with details | |
| 304 | +app.get('/personalities', (req, res) => { | |
| 305 | + res.json(Object.values(personalities)); | |
| 306 | +}); | |
| 307 | + | |
| 308 | +// Get specific personality | |
| 309 | +app.get('/personalities/:id', (req, res) => { | |
| 310 | + const { id } = req.params; | |
| 311 | + const personality = personalities[id]; | |
| 312 | + | |
| 313 | + if (!personality) { | |
| 314 | + return res.status(404).json({ error: 'Personality not found' }); | |
| 315 | + } | |
| 316 | + | |
| 317 | + res.json(personality); | |
| 318 | +}); | |
| 319 | + | |
| 320 | +// Add new personality (for future UI) | |
| 321 | +app.post('/personalities', async (req, res) => { | |
| 322 | + const { id, name, systemPrompt, temperature } = req.body; | |
| 323 | + | |
| 324 | + if (!id || !name || !systemPrompt) { | |
| 325 | + return res.status(400).json({ error: 'Missing required fields' }); | |
| 326 | + } | |
| 327 | + | |
| 328 | + const personality = { | |
| 329 | + id, | |
| 330 | + name, | |
| 331 | + systemPrompt, | |
| 332 | + temperature: temperature || 0.8 | |
| 333 | + }; | |
| 334 | + | |
| 335 | + personalities[id] = personality; | |
| 336 | + | |
| 337 | + // Save to file | |
| 338 | + try { | |
| 339 | + const filePath = path.join(__dirname, '..', 'personalities', `${id}.json`); | |
| 340 | + await fs.writeFile(filePath, JSON.stringify(personality, null, 2)); | |
| 341 | + res.json({ success: true, personality }); | |
| 342 | + } catch (error) { | |
| 343 | + console.error('Error saving personality:', error); | |
| 344 | + res.status(500).json({ error: 'Failed to save personality' }); | |
| 345 | + } | |
| 346 | +}); | |
| 347 | + | |
| 348 | +// Cleanup old conversations periodically | |
| 349 | +setInterval(() => { | |
| 350 | + conversationManager.cleanup(); | |
| 351 | +}, 60 * 60 * 1000); // Every hour | |
| 352 | + | |
| 353 | +// Start server | |
| 354 | +async function start() { | |
| 355 | + await loadPersonalities(); | |
| 356 | + await checkOllamaConnection(); | |
| 357 | + | |
| 358 | + app.listen(PORT, () => { | |
| 359 | + console.log(` | |
| 360 | +╔══════════════════════════════════════╗ | |
| 361 | +║ LooseCannon Server v0.2.0 ║ | |
| 362 | +║ Listening on port ${PORT} ║ | |
| 363 | +╠══════════════════════════════════════╣ | |
| 364 | +║ Features: ║ | |
| 365 | +║ ✓ Multi-platform support ║ | |
| 366 | +║ ✓ Conversation management ║ | |
| 367 | +║ ✓ Scammer detection ║ | |
| 368 | +║ ✓ Context-aware responses ║ | |
| 369 | +║ ║ | |
| 370 | +║ Extension: Connect to ║ | |
| 371 | +║ http://localhost:${PORT} ║ | |
| 372 | +║ ║ | |
| 373 | +║ Ollama: ${OLLAMA_URL.padEnd(28)} ║ | |
| 374 | +╚══════════════════════════════════════╝ | |
| 375 | + | |
| 376 | +🤖 Ready to confuse scammers across all platforms! | |
| 377 | + `); | |
| 378 | + }); | |
| 379 | +} | |
| 380 | + | |
| 381 | +// Handle graceful shutdown | |
| 382 | +process.on('SIGINT', () => { | |
| 383 | + console.log('\n\nShutting down LooseCannon server...'); | |
| 384 | + console.log('Statistics:', conversationManager.getStatistics()); | |
| 385 | + process.exit(0); | |
| 386 | +}); | |
| 387 | + | |
| 388 | +// Handle uncaught errors | |
| 389 | +process.on('uncaughtException', (error) => { | |
| 390 | + console.error('Uncaught Exception:', error); | |
| 391 | +}); | |
| 392 | + | |
| 393 | +process.on('unhandledRejection', (reason, promise) => { | |
| 394 | + console.error('Unhandled Rejection at:', promise, 'reason:', reason); | |
| 395 | +}); | |
| 396 | + | |
| 397 | +start(); | |