From b93913834945e126ba3771c995b59aefe1376eeb Mon Sep 17 00:00:00 2001 From: "Rodrigo Rodriguez (Pragmatismo)" Date: Tue, 13 May 2025 14:50:15 -0300 Subject: [PATCH] Add initial implementation of General Bots Chrome extension with settings and UI enhancements --- .gitignore | 2 + .vscode/launch.json | 33 ++++++ LICENSE | 17 ++++ README.md | 50 +++++++++ background.js | 18 ++++ content.js | 239 ++++++++++++++++++++++++++++++++++++++++++++ icons/icon128.png | Bin 0 -> 339 bytes icons/icon16.png | Bin 0 -> 339 bytes icons/icon48.png | Bin 0 -> 339 bytes manifest.json | 37 +++++++ popup.css | 168 +++++++++++++++++++++++++++++++ popup.html | 47 +++++++++ popup.js | 50 +++++++++ styles.css | 32 ++++++ 14 files changed, 693 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/launch.json create mode 100644 LICENSE create mode 100644 README.md create mode 100644 background.js create mode 100644 content.js create mode 100644 icons/icon128.png create mode 100644 icons/icon16.png create mode 100644 icons/icon48.png create mode 100644 manifest.json create mode 100644 popup.css create mode 100644 popup.html create mode 100644 popup.js create mode 100644 styles.css diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d88d00f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.chrome-debug-profile +*.png~ \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..87a5ba3 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,33 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "chrome", + "request": "launch", + "name": "Launch Chrome against localhost", + "url": "https://web.whatsapp.com", + "webRoot": "${workspaceFolder}", + "runtimeArgs": [ + "--load-extension=${workspaceFolder}/GeneralBots", + "--disable-extensions-except=${workspaceFolder}/GeneralBots", + "--user-data-dir=${workspaceFolder}/.chrome-debug-profile" + ], + "sourceMaps": true + }, + { + "type": "extensionHost", + "request": "launch", + "name": "Debug Chrome Extension", + "runtimeExecutable": "${execPath}", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}/GeneralBots", + "--load-extension=${workspaceFolder}/GeneralBots", + "--disable-extensions-except=${workspaceFolder}/GeneralBots" + ], + "outFiles": [ + "${workspaceFolder}/GeneralBots/**/*.js" + ], + "sourceMaps": true + } + ] + } \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5aa084f --- /dev/null +++ b/LICENSE @@ -0,0 +1,17 @@ +/* +General Bots - WhatsApp Web Enhancement Chrome Extension +Copyright (C) 2025 pragmatismo.com.br + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published +by the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..628da1b --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ +# General Bots Chrome Extension + +A professional-grade Chrome extension developed by [pragmatismo.com.br](https://pragmatismo.com.br) that enhances WhatsApp Web with server-side message processing capabilities and UI improvements. + +## Features + +- Message Interception: Captures messages before they're sent +- Server Processing: Sends message content to your server for processing +- Message Replacement: Updates the message with processed content before sending +- UI Enhancement: Option to hide the contact list for more chat space +- User-friendly Settings: Simple configuration through the extension popup + +## Installation + +### Developer Mode Installation + +1. Clone or download this repository +2. Open Chrome and navigate to `chrome://extensions/` +3. Enable "Developer mode" in the top-right corner +4. Click "Load unpacked" and select the extension directory + +### Chrome Web Store Installation + +(Coming soon) + +## Configuration + +1. Click the General Bots icon in your Chrome toolbar +2. Enter your processing server URL +3. Toggle message processing on/off +4. Toggle contact list visibility + +## Server API Requirements + +Your server endpoint should: + +1. Accept POST requests with JSON payload: `{ "text": "message content", "timestamp": 1621234567890 }` +2. Return JSON response: `{ "processedText": "updated message content" }` + +## License + +This project is licensed under the [GNU Affero General Public License](LICENSE) - see the LICENSE file for details. + +## Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. + +## Contact + +For support or questions, please contact [pragmatismo.com.br](https://pragmatismo.com.br). diff --git a/background.js b/background.js new file mode 100644 index 0000000..e688b0f --- /dev/null +++ b/background.js @@ -0,0 +1,18 @@ +chrome.runtime.onInstalled.addListener(function() { + // Set default settings on installation + chrome.storage.sync.set({ + serverUrl: 'https://api.pragmatismo.com.br/general-bots/process', + enableProcessing: true, + hideContacts: false + }); +}); + +// Listen for tab updates +chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { + if (changeInfo.status === 'complete' && tab.url && tab.url.includes('web.whatsapp.com')) { + // Inject content script + chrome.tabs.sendMessage(tabId, { + action: 'tabReady' + }); + } +}); diff --git a/content.js b/content.js new file mode 100644 index 0000000..6df09a8 --- /dev/null +++ b/content.js @@ -0,0 +1,239 @@ +// Global variables +let settings = { + serverUrl: 'https://api.pragmatismo.com.br/general-bots/process', + enableProcessing: true, + hideContacts: false +}; + +// Original message storage (before processing) +const originalMessages = new Map(); + +// Initialize the extension +function init() { + console.log('General Bots: Initializing...'); + + // Load settings + chrome.storage.sync.get({ + serverUrl: 'https://api.pragmatismo.com.br/general-bots/process', + enableProcessing: true, + hideContacts: false + }, function(items) { + settings = items; + console.log('General Bots: Settings loaded', settings); + + // Apply hide contacts if enabled + applyContactVisibility(); + + // Start monitoring the input field + setupInputListener(); + }); +} + +// Apply contact list visibility based on settings +function applyContactVisibility() { + const contactList = document.querySelector('#pane-side'); + if (contactList) { + if (settings.hideContacts) { + contactList.parentElement.classList.add('gb-hide-contacts'); + } else { + contactList.parentElement.classList.remove('gb-hide-contacts'); + } + } +} + +// Setup input field listener +function setupInputListener() { + // The main input field where users type messages + const inputSelector = 'div[contenteditable="true"][data-tab="10"]'; + + // Use MutationObserver to detect when the input field appears + const observer = new MutationObserver(mutations => { + const inputField = document.querySelector(inputSelector); + if (inputField && !inputField.getAttribute('gb-monitored')) { + setupFieldMonitoring(inputField); + inputField.setAttribute('gb-monitored', 'true'); + } + }); + + observer.observe(document.body, { + childList: true, + subtree: true + }); + + // Also check immediately in case the field is already present + const inputField = document.querySelector(inputSelector); + if (inputField && !inputField.getAttribute('gb-monitored')) { + setupFieldMonitoring(inputField); + inputField.setAttribute('gb-monitored', 'true'); + } +} + +// Setup monitoring for a specific input field +function setupFieldMonitoring(inputField) { + console.log('General Bots: Setting up input field monitoring'); + + // Listen for keydown events (Enter key) + inputField.addEventListener('keydown', async (event) => { + if (event.key === 'Enter' && !event.shiftKey && settings.enableProcessing) { + const originalText = inputField.textContent; + + // Only process if there's actual text + if (originalText.trim().length > 0) { + // Prevent default Enter behavior temporarily + event.preventDefault(); + + try { + // Process message with server + const processedText = await processMessage(originalText); + + // Replace text in the input field + inputField.textContent = processedText; + + // Store original message for reference + const timestamp = Date.now(); + originalMessages.set(timestamp, { + original: originalText, + processed: processedText + }); + + // Simulate Enter press to send the message + simulateEnterPress(inputField); + + // Track the message to update it after sending + trackSentMessage(timestamp); + } catch (error) { + console.error('General Bots: Error processing message', error); + // Let the original message be sent if there's an error + simulateEnterPress(inputField); + } + } + } + }); +} + +// Process message with server +async function processMessage(text) { + console.log('General Bots: Processing message with server'); + + try { + const response = await fetch(settings.serverUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + text: text, + timestamp: Date.now() + }) + }); + + if (!response.ok) { + throw new Error(`Server responded with status: ${response.status}`); + } + + const data = await response.json(); + return data.processedText || text; + } catch (error) { + console.error('General Bots: Failed to process message', error); + return text; // Return original text if processing fails + } +} + +// Simulate Enter key press +function simulateEnterPress(element) { + const enterEvent = new KeyboardEvent('keydown', { + key: 'Enter', + code: 'Enter', + keyCode: 13, + which: 13, + bubbles: true + }); + element.dispatchEvent(enterEvent); +} + +// Track sent message to update it after sending +function trackSentMessage(timestamp) { + // Monitor for the message to appear in the chat + const observer = new MutationObserver(mutations => { + // Look for recently sent messages + const messageContainers = document.querySelectorAll('.message-out'); + if (messageContainers.length > 0) { + // Get the last sent message + const lastMessage = messageContainers[messageContainers.length - 1]; + + // Check if this message has our data + if (!lastMessage.getAttribute('gb-processed')) { + // Get message info + const messageInfo = originalMessages.get(timestamp); + if (messageInfo) { + // Mark as processed + lastMessage.setAttribute('gb-processed', timestamp); + + // Update message text if different from original + if (messageInfo.original !== messageInfo.processed) { + updateSentMessageDisplay(lastMessage, messageInfo); + } + + // Clean up + setTimeout(() => { + originalMessages.delete(timestamp); + }, 60000); // Remove after 1 minute + + // Stop observing + observer.disconnect(); + } + } + } + }); + + // Start observing chat container + const chatContainer = document.querySelector('.copyable-area'); + if (chatContainer) { + observer.observe(chatContainer, { + childList: true, + subtree: true + }); + + // Stop observing after a reasonable timeout + setTimeout(() => { + observer.disconnect(); + }, 10000); // 10 seconds timeout + } +} + +// Update the displayed message after sending +function updateSentMessageDisplay(messageElement, messageInfo) { + const textElement = messageElement.querySelector('.selectable-text'); + if (textElement) { + // Create a small indicator that the message was processed + const indicator = document.createElement('div'); + indicator.className = 'gb-processed-indicator'; + indicator.title = `Original: "${messageInfo.original}"`; + indicator.textContent = '✓ AI processed'; + + // Add the indicator to the message + messageElement.appendChild(indicator); + + // Add tooltip behavior + messageElement.classList.add('gb-processed-message'); + } +} + +// Listen for messages from popup or background +chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + if (message.action === 'settingsUpdated') { + settings = message.settings; + applyContactVisibility(); + console.log('General Bots: Settings updated', settings); + } else if (message.action === 'tabReady') { + init(); + } + + return true; +}); + +// Initialize on load +document.addEventListener('DOMContentLoaded', init); + +// Also try to initialize now in case the page is already loaded +init(); diff --git a/icons/icon128.png b/icons/icon128.png new file mode 100644 index 0000000000000000000000000000000000000000..d5754036d39e3c4f53fa1485afe116316ab5f32f GIT binary patch literal 339 zcmV-Z0j&OsP)ioMWJDZ3RG2vrfJZ1owa{WU>L?7vJMMWU|H53(==K7*908L z!M5!?ECwndc%BE>b!o^-Pq_t-;~)$}oUakS??3HdE4+x~7*Q0_5aAQ}pNGiJNs^#x z8gyNUVHjxszYpWmv6=IWu}P`)DHjv002ovPDHLkV1gUNh^hbp literal 0 HcmV?d00001 diff --git a/icons/icon16.png b/icons/icon16.png new file mode 100644 index 0000000000000000000000000000000000000000..d5754036d39e3c4f53fa1485afe116316ab5f32f GIT binary patch literal 339 zcmV-Z0j&OsP)ioMWJDZ3RG2vrfJZ1owa{WU>L?7vJMMWU|H53(==K7*908L z!M5!?ECwndc%BE>b!o^-Pq_t-;~)$}oUakS??3HdE4+x~7*Q0_5aAQ}pNGiJNs^#x z8gyNUVHjxszYpWmv6=IWu}P`)DHjv002ovPDHLkV1gUNh^hbp literal 0 HcmV?d00001 diff --git a/icons/icon48.png b/icons/icon48.png new file mode 100644 index 0000000000000000000000000000000000000000..d5754036d39e3c4f53fa1485afe116316ab5f32f GIT binary patch literal 339 zcmV-Z0j&OsP)ioMWJDZ3RG2vrfJZ1owa{WU>L?7vJMMWU|H53(==K7*908L z!M5!?ECwndc%BE>b!o^-Pq_t-;~)$}oUakS??3HdE4+x~7*Q0_5aAQ}pNGiJNs^#x z8gyNUVHjxszYpWmv6=IWu}P`)DHjv002ovPDHLkV1gUNh^hbp literal 0 HcmV?d00001 diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..6b6fb5c --- /dev/null +++ b/manifest.json @@ -0,0 +1,37 @@ +{ + "manifest_version": 3, + "name": "General Bots", + "version": "1.0.0", + "description": "Browser server-side processing capabilities", + "author": "pragmatismo.com.br", + "icons": { + "16": "icons/icon16.png", + "48": "icons/icon48.png", + "128": "icons/icon128.png" + }, + "permissions": [ + "storage", + "tabs" + ], + "host_permissions": [ + "https://web.whatsapp.com/*" + ], + "action": { + "default_popup": "popup.html", + "default_icon": { + "16": "icons/icon16.png", + "48": "icons/icon48.png", + "128": "icons/icon128.png" + } + }, + "content_scripts": [ + { + "matches": ["https://web.whatsapp.com/*"], + "js": ["content.js"], + "css": ["styles.css"] + } + ], + "background": { + "service_worker": "background.js" + } +} diff --git a/popup.css b/popup.css new file mode 100644 index 0000000..5ee8d85 --- /dev/null +++ b/popup.css @@ -0,0 +1,168 @@ +body { + font-family: Arial, sans-serif; + margin: 0; + padding: 0; + width: 320px; + background-color: #f5f5f5; +} + +.container { + padding: 16px; +} + +.header { + display: flex; + align-items: center; + margin-bottom: 16px; + flex-wrap: wrap; +} + +.header img { + width: 32px; + height: 32px; + margin-right: 10px; +} + +.header h1 { + margin: 0; + font-size: 18px; + flex-grow: 1; +} + +.version { + color: #888; + margin: 0; + font-size: 12px; +} + +.settings { + background-color: #fff; + border-radius: 8px; + padding: 16px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.setting-item { + margin-bottom: 16px; +} + +.setting-item:last-child { + margin-bottom: 0; +} + +.setting-item label { + display: block; + margin-bottom: 6px; + font-weight: bold; + font-size: 14px; +} + +.setting-item input[type="text"] { + width: 100%; + padding: 8px; + border-radius: 4px; + border: 1px solid #ddd; + box-sizing: border-box; +} + +.toggle { + display: flex; + justify-content: space-between; + align-items: center; +} + +.toggle span { + font-size: 14px; + font-weight: bold; +} + +.switch { + position: relative; + display: inline-block; + width: 46px; + height: 24px; +} + +.switch input { + opacity: 0; + width: 0; + height: 0; +} + +.slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ccc; + transition: .3s; +} + +.slider:before { + position: absolute; + content: ""; + height: 18px; + width: 18px; + left: 3px; + bottom: 3px; + background-color: white; + transition: .3s; +} + +input:checked + .slider { + background-color: #4CAF50; +} + +input:focus + .slider { + box-shadow: 0 0 1px #4CAF50; +} + +input:checked + .slider:before { + transform: translateX(22px); +} + +.slider.round { + border-radius: 24px; +} + +.slider.round:before { + border-radius: 50%; +} + +.footer { + margin-top: 16px; + text-align: center; +} + +button { + background-color: #4CAF50; + color: white; + border: none; + padding: 10px 16px; + border-radius: 4px; + cursor: pointer; + font-weight: bold; + width: 100%; + transition: background-color 0.2s; +} + +button:hover { + background-color: #45a049; +} + +.copyright { + margin-top: 10px; + font-size: 12px; + color: #888; +} + +.copyright a { + color: #4CAF50; + text-decoration: none; +} + +.copyright a:hover { + text-decoration: underline; +} diff --git a/popup.html b/popup.html new file mode 100644 index 0000000..43e0937 --- /dev/null +++ b/popup.html @@ -0,0 +1,47 @@ + + + + + + General Bots + + + +
+
+ General Bots Logo +

General Bots

+

v1.0.0

+
+ +
+
+ + +
+ +
+ Enable Message Processing + +
+ +
+ Hide Contact Window + +
+
+ + +
+ + + diff --git a/popup.js b/popup.js new file mode 100644 index 0000000..9228123 --- /dev/null +++ b/popup.js @@ -0,0 +1,50 @@ +document.addEventListener('DOMContentLoaded', function() { + // Load saved settings + chrome.storage.sync.get({ + serverUrl: 'https://api.pragmatismo.com.br/general-bots/process', + enableProcessing: true, + hideContacts: false + }, function(items) { + document.getElementById('server-url').value = items.serverUrl; + document.getElementById('enable-processing').checked = items.enableProcessing; + document.getElementById('hide-contacts').checked = items.hideContacts; + }); + + // Save settings + document.getElementById('save-settings').addEventListener('click', function() { + const serverUrl = document.getElementById('server-url').value; + const enableProcessing = document.getElementById('enable-processing').checked; + const hideContacts = document.getElementById('hide-contacts').checked; + + chrome.storage.sync.set({ + serverUrl: serverUrl, + enableProcessing: enableProcessing, + hideContacts: hideContacts + }, function() { + // Update status to let user know settings were saved + const button = document.getElementById('save-settings'); + const originalText = button.textContent; + button.textContent = 'Settings Saved!'; + button.disabled = true; + + // Send message to content script to apply changes immediately + chrome.tabs.query({url: 'https://web.whatsapp.com/*'}, function(tabs) { + if (tabs.length > 0) { + chrome.tabs.sendMessage(tabs[0].id, { + action: 'settingsUpdated', + settings: { + serverUrl: serverUrl, + enableProcessing: enableProcessing, + hideContacts: hideContacts + } + }); + } + }); + + setTimeout(function() { + button.textContent = originalText; + button.disabled = false; + }, 1500); + }); + }); +}); diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..c7ced1e --- /dev/null +++ b/styles.css @@ -0,0 +1,32 @@ +/* Hide the contacts pane when enabled */ +.gb-hide-contacts #pane-side { + display: none !important; +} + +/* Expand the chat area when contacts are hidden */ +.gb-hide-contacts #main { + width: 100% !important; + left: 0 !important; +} + +/* Style for processed messages */ +.gb-processed-message { + position: relative; +} + +.gb-processed-indicator { + position: absolute; + bottom: -16px; + right: 8px; + font-size: 10px; + color: #53bdeb; + background-color: rgba(255, 255, 255, 0.8); + padding: 2px 4px; + border-radius: 4px; + opacity: 0.7; + cursor: help; +} + +.gb-processed-message:hover .gb-processed-indicator { + opacity: 1; +}