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