Add initial implementation of General Bots Chrome extension with settings and UI enhancements

This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2025-05-13 14:50:15 -03:00
commit b939138349
14 changed files with 693 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
.chrome-debug-profile
*.png~

33
.vscode/launch.json vendored Normal file
View file

@ -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
}
]
}

17
LICENSE Normal file
View file

@ -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 <https://www.gnu.org/licenses/>.
*/

50
README.md Normal file
View file

@ -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).

18
background.js Normal file
View file

@ -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'
});
}
});

239
content.js Normal file
View file

@ -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();

BIN
icons/icon128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 339 B

BIN
icons/icon16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 339 B

BIN
icons/icon48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 339 B

37
manifest.json Normal file
View file

@ -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"
}
}

168
popup.css Normal file
View file

@ -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;
}

47
popup.html Normal file
View file

@ -0,0 +1,47 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>General Bots</title>
<link rel="stylesheet" href="popup.css">
</head>
<body>
<div class="container">
<div class="header">
<img src="icons/icon48.png" alt="General Bots Logo">
<h1>General Bots</h1>
<p class="version">v1.0.0</p>
</div>
<div class="settings">
<div class="setting-item">
<label for="server-url">Server URL:</label>
<input type="text" id="server-url" placeholder="https://your-server.com/api">
</div>
<div class="setting-item toggle">
<span>Enable Message Processing</span>
<label class="switch">
<input type="checkbox" id="enable-processing" checked>
<span class="slider round"></span>
</label>
</div>
<div class="setting-item toggle">
<span>Hide Contact Window</span>
<label class="switch">
<input type="checkbox" id="hide-contacts">
<span class="slider round"></span>
</label>
</div>
</div>
<div class="footer">
<button id="save-settings">Save Settings</button>
<p class="copyright">© pragmatismo.com.br • <a href="https://github.com/pragmatismo-io/GeneralBots" target="_blank">AGPL License</a></p>
</div>
</div>
<script src="popup.js"></script>
</body>
</html>

50
popup.js Normal file
View file

@ -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);
});
});
});

32
styles.css Normal file
View file

@ -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;
}