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