JavaScript · 13670 bytes Raw Blame History
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 }