JavaScript · 7817 bytes Raw Blame History
1 // WhatsApp Web Content Script for LooseCannon
2 console.log('[LooseCannon] WhatsApp content script loaded');
3
4 class WhatsAppIntegration {
5 constructor() {
6 this.isActive = false;
7 this.currentChat = null;
8 this.messageObserver = null;
9 this.init();
10 }
11
12 init() {
13 // Wait for WhatsApp to fully load
14 this.waitForWhatsApp().then(() => {
15 console.log('[LooseCannon] WhatsApp detected and ready');
16 this.setupMessageObserver();
17 this.injectControls();
18 this.listenForCommands();
19 });
20 }
21
22 waitForWhatsApp() {
23 return new Promise((resolve) => {
24 const checkForApp = setInterval(() => {
25 // Look for WhatsApp's main app wrapper
26 const mainWrapper = document.querySelector('[data-testid="conversation-panel-wrapper"]');
27 if (mainWrapper) {
28 clearInterval(checkForApp);
29 resolve();
30 }
31 }, 1000);
32 });
33 }
34
35 setupMessageObserver() {
36 // Observe the message list for new messages
37 const messageContainer = document.querySelector('[role="application"]');
38
39 if (!messageContainer) {
40 console.error('[LooseCannon] Could not find message container');
41 return;
42 }
43
44 const config = {
45 childList: true,
46 subtree: true,
47 characterData: true
48 };
49
50 this.messageObserver = new MutationObserver((mutations) => {
51 if (!this.isActive) return;
52
53 mutations.forEach((mutation) => {
54 // Check if new message nodes were added
55 if (mutation.type === 'childList') {
56 mutation.addedNodes.forEach((node) => {
57 if (this.isIncomingMessage(node)) {
58 this.handleIncomingMessage(node);
59 }
60 });
61 }
62 });
63 });
64
65 this.messageObserver.observe(messageContainer, config);
66 console.log('[LooseCannon] Message observer setup complete');
67 }
68
69 isIncomingMessage(node) {
70 // Check if this is an incoming message element
71 // WhatsApp uses specific classes for incoming vs outgoing messages
72 if (!node.querySelector) return false;
73
74 const messageElement = node.querySelector('[data-testid^="msg-"]');
75 if (!messageElement) return false;
76
77 // Incoming messages don't have the "message-out" class
78 return !messageElement.classList.contains('message-out');
79 }
80
81 handleIncomingMessage(node) {
82 const messageText = this.extractMessageText(node);
83 const timestamp = new Date().toISOString();
84
85 console.log('[LooseCannon] New message detected:', messageText);
86
87 // Send to background script for processing
88 browser.runtime.sendMessage({
89 type: 'NEW_MESSAGE',
90 data: {
91 text: messageText,
92 timestamp: timestamp,
93 chatId: this.getCurrentChatId()
94 }
95 }).then(response => {
96 if (response && response.reply) {
97 this.sendMessage(response.reply);
98 }
99 });
100 }
101
102 extractMessageText(node) {
103 // Extract text from WhatsApp message element
104 const textElement = node.querySelector('[data-testid="msg-container"] .selectable-text');
105 return textElement ? textElement.textContent : '';
106 }
107
108 getCurrentChatId() {
109 // Get unique identifier for current chat
110 const headerElement = document.querySelector('header [data-testid="conversation-header"]');
111 if (headerElement) {
112 const titleElement = headerElement.querySelector('span[title]');
113 return titleElement ? titleElement.title : 'unknown';
114 }
115 return 'unknown';
116 }
117
118 sendMessage(text) {
119 // Find the message input field
120 const inputElement = document.querySelector('[data-testid="conversation-compose-box-input"]');
121
122 if (!inputElement) {
123 console.error('[LooseCannon] Could not find message input');
124 return;
125 }
126
127 // Focus the input
128 inputElement.focus();
129
130 // Set the message text
131 inputElement.textContent = text;
132
133 // Trigger input event to update WhatsApp's state
134 const inputEvent = new InputEvent('input', {
135 bubbles: true,
136 cancelable: true,
137 });
138 inputElement.dispatchEvent(inputEvent);
139
140 // Find and click send button
141 setTimeout(() => {
142 const sendButton = document.querySelector('[data-testid="compose-btn-send"]');
143 if (sendButton) {
144 sendButton.click();
145 console.log('[LooseCannon] Message sent:', text);
146 }
147 }, 100);
148 }
149
150 injectControls() {
151 // Add toggle button to WhatsApp interface
152 const style = document.createElement('style');
153 style.textContent = `
154 .loosecannon-toggle {
155 position: fixed;
156 bottom: 20px;
157 right: 20px;
158 z-index: 9999;
159 background: #ff4444;
160 color: white;
161 border: none;
162 border-radius: 50px;
163 padding: 12px 20px;
164 cursor: pointer;
165 font-weight: bold;
166 box-shadow: 0 2px 10px rgba(0,0,0,0.3);
167 transition: all 0.3s;
168 }
169
170 .loosecannon-toggle.active {
171 background: #44ff44;
172 }
173
174 .loosecannon-toggle:hover {
175 transform: scale(1.05);
176 }
177
178 .loosecannon-indicator {
179 position: fixed;
180 top: 70px;
181 right: 20px;
182 background: rgba(255, 68, 68, 0.9);
183 color: white;
184 padding: 8px 15px;
185 border-radius: 20px;
186 font-size: 12px;
187 z-index: 9999;
188 display: none;
189 }
190
191 .loosecannon-indicator.active {
192 display: block;
193 background: rgba(68, 255, 68, 0.9);
194 }
195 `;
196 document.head.appendChild(style);
197
198 const toggleButton = document.createElement('button');
199 toggleButton.className = 'loosecannon-toggle';
200 toggleButton.textContent = 'LC: OFF';
201 toggleButton.onclick = () => this.toggleActive();
202 document.body.appendChild(toggleButton);
203
204 const indicator = document.createElement('div');
205 indicator.className = 'loosecannon-indicator';
206 indicator.textContent = 'LooseCannon Active';
207 document.body.appendChild(indicator);
208 }
209
210 toggleActive() {
211 this.isActive = !this.isActive;
212 const button = document.querySelector('.loosecannon-toggle');
213 const indicator = document.querySelector('.loosecannon-indicator');
214
215 if (this.isActive) {
216 button.classList.add('active');
217 button.textContent = 'LC: ON';
218 indicator.classList.add('active');
219 console.log('[LooseCannon] Activated for current chat');
220 } else {
221 button.classList.remove('active');
222 button.textContent = 'LC: OFF';
223 indicator.classList.remove('active');
224 console.log('[LooseCannon] Deactivated');
225 }
226
227 // Notify background script
228 browser.runtime.sendMessage({
229 type: 'TOGGLE_ACTIVE',
230 data: { isActive: this.isActive }
231 });
232 }
233
234 listenForCommands() {
235 // Listen for commands from popup or background script
236 browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
237 switch (message.type) {
238 case 'GET_STATUS':
239 sendResponse({ isActive: this.isActive });
240 break;
241 case 'SET_ACTIVE':
242 this.isActive = message.data.isActive;
243 this.updateUI();
244 break;
245 case 'SEND_MESSAGE':
246 this.sendMessage(message.data.text);
247 break;
248 }
249 });
250 }
251
252 updateUI() {
253 const button = document.querySelector('.loosecannon-toggle');
254 const indicator = document.querySelector('.loosecannon-indicator');
255
256 if (this.isActive) {
257 button?.classList.add('active');
258 if (button) button.textContent = 'LC: ON';
259 indicator?.classList.add('active');
260 } else {
261 button?.classList.remove('active');
262 if (button) button.textContent = 'LC: OFF';
263 indicator?.classList.remove('active');
264 }
265 }
266 }
267
268 // Initialize when DOM is ready
269 if (document.readyState === 'loading') {
270 document.addEventListener('DOMContentLoaded', () => new WhatsAppIntegration());
271 } else {
272 new WhatsAppIntegration();
273 }