init. firefox ext. w. content script, worker, popup, dom integ, indicators. local server for local llm on 8765. three personalities
- SHA
e8b5f80867c06e8aa5cbcf8b108b3787646610e9- Tree
8b56e99
e8b5f80
e8b5f80867c06e8aa5cbcf8b108b3787646610e98b56e99| Status | File | + | - |
|---|---|---|---|
| A |
.env.example
|
15 | 0 |
| A |
.gitignore
|
37 | 0 |
| A |
README.md
|
124 | 0 |
| A |
extension/background/background.js
|
184 | 0 |
| A |
extension/content-scripts/whatsapp.css
|
26 | 0 |
| A |
extension/content-scripts/whatsapp.js
|
273 | 0 |
| A |
extension/icons/icon-128.png
|
0 | 0 |
| A |
extension/icons/icon-16.png
|
0 | 0 |
| A |
extension/icons/icon-48.png
|
0 | 0 |
| A |
extension/icons/icon.svg
|
11 | 0 |
| A |
extension/manifest.json
|
42 | 0 |
| A |
extension/popup/popup.html
|
278 | 0 |
| A |
extension/popup/popup.js
|
245 | 0 |
| A |
package.json
|
33 | 0 |
| A |
server/personalities/confused-elder.json
|
21 | 0 |
| A |
server/personalities/conspiracy-theorist.json
|
21 | 0 |
| A |
server/personalities/tech-support-nightmare.json
|
21 | 0 |
| A |
server/src/index.js
|
205 | 0 |
| A |
setup.sh
|
96 | 0 |
.env.exampleadded@@ -0,0 +1,15 @@ | ||
| 1 | +# LooseCannon Configuration | |
| 2 | + | |
| 3 | +# Server port (default: 8765) | |
| 4 | +PORT=8765 | |
| 5 | + | |
| 6 | +# Ollama API URL (default: http://localhost:11434) | |
| 7 | +OLLAMA_URL=http://localhost:11434 | |
| 8 | + | |
| 9 | +# Ollama model to use (default: llama2) | |
| 10 | +# You can use any model available in Ollama: llama2, mistral, codellama, etc. | |
| 11 | +# Run 'ollama list' to see available models | |
| 12 | +OLLAMA_MODEL=llama2 | |
| 13 | + | |
| 14 | +# Debug mode (shows extra logging) | |
| 15 | +DEBUG=false | |
.gitignoreadded@@ -0,0 +1,37 @@ | ||
| 1 | +# Dependencies | |
| 2 | +node_modules/ | |
| 3 | +package-lock.json | |
| 4 | + | |
| 5 | +# Environment files | |
| 6 | +.env | |
| 7 | +.env.local | |
| 8 | +.env.production | |
| 9 | + | |
| 10 | +# Build outputs | |
| 11 | +web-ext-artifacts/ | |
| 12 | +dist/ | |
| 13 | +build/ | |
| 14 | + | |
| 15 | +# IDE | |
| 16 | +.vscode/ | |
| 17 | +.idea/ | |
| 18 | +*.swp | |
| 19 | +*.swo | |
| 20 | +.DS_Store | |
| 21 | + | |
| 22 | +# Logs | |
| 23 | +*.log | |
| 24 | +npm-debug.log* | |
| 25 | + | |
| 26 | +# Firefox profiles | |
| 27 | +firefox-profile/ | |
| 28 | +loosecannon-profile/ | |
| 29 | + | |
| 30 | +# Temporary files | |
| 31 | +tmp/ | |
| 32 | +temp/ | |
| 33 | +*.tmp | |
| 34 | + | |
| 35 | +# Conversation logs (privacy) | |
| 36 | +conversations/ | |
| 37 | +logs/ | |
README.mdadded@@ -0,0 +1,124 @@ | ||
| 1 | +# LooseCannon 🤖 | |
| 2 | + | |
| 3 | +Automated scambaiting assistant that integrates with messaging platforms to waste scammers' time using local LLMs. | |
| 4 | + | |
| 5 | +## ⚠️ Legal Disclaimer | |
| 6 | + | |
| 7 | +This tool is for educational and defensive security purposes only. Use responsibly and be aware that automation may violate platform Terms of Service. Your account could be banned. | |
| 8 | + | |
| 9 | +## Features | |
| 10 | + | |
| 11 | +- 🦊 Firefox browser extension | |
| 12 | +- 💬 WhatsApp Web integration (more platforms coming) | |
| 13 | +- 🤖 Local LLM support via Ollama | |
| 14 | +- 🎭 Multiple scambaiter personalities | |
| 15 | +- 🛑 Emergency stop functionality | |
| 16 | +- 📊 Conversation logging | |
| 17 | + | |
| 18 | +## Project Structure | |
| 19 | + | |
| 20 | +``` | |
| 21 | +LooseCannon/ | |
| 22 | +├── extension/ # Firefox browser extension | |
| 23 | +│ ├── manifest.json | |
| 24 | +│ ├── content-scripts/ | |
| 25 | +│ ├── background/ | |
| 26 | +│ └── popup/ | |
| 27 | +├── server/ # Local server for LLM integration | |
| 28 | +│ ├── src/ | |
| 29 | +│ └── personalities/ | |
| 30 | +└── package.json | |
| 31 | +``` | |
| 32 | + | |
| 33 | +## Quick Start | |
| 34 | + | |
| 35 | +### Prerequisites | |
| 36 | + | |
| 37 | +1. **Ollama** - Install from https://ollama.ai | |
| 38 | +2. **Node.js** - Version 18+ recommended | |
| 39 | +3. **Firefox Developer Edition** (recommended for extension development) | |
| 40 | + | |
| 41 | +### Installation | |
| 42 | + | |
| 43 | +1. Clone the repository: | |
| 44 | +```bash | |
| 45 | +cd ~/Documents/GithubOrgs/zeroed-some/LooseCannon | |
| 46 | +``` | |
| 47 | + | |
| 48 | +2. Install dependencies: | |
| 49 | +```bash | |
| 50 | +npm install | |
| 51 | +``` | |
| 52 | + | |
| 53 | +3. Start Ollama with a model: | |
| 54 | +```bash | |
| 55 | +ollama pull llama2 | |
| 56 | +ollama serve | |
| 57 | +``` | |
| 58 | + | |
| 59 | +4. Start the local server: | |
| 60 | +```bash | |
| 61 | +npm run dev:server | |
| 62 | +``` | |
| 63 | + | |
| 64 | +5. Load the extension in Firefox: | |
| 65 | +```bash | |
| 66 | +npm run dev:extension | |
| 67 | +``` | |
| 68 | + | |
| 69 | +Or manually: | |
| 70 | +- Open Firefox and navigate to `about:debugging` | |
| 71 | +- Click "This Firefox" | |
| 72 | +- Click "Load Temporary Add-on" | |
| 73 | +- Select `extension/manifest.json` | |
| 74 | + | |
| 75 | +## Usage | |
| 76 | + | |
| 77 | +1. Navigate to WhatsApp Web (https://web.whatsapp.com) | |
| 78 | +2. Click the LooseCannon button in the bottom right | |
| 79 | +3. Select a personality from the popup | |
| 80 | +4. Toggle "LC: ON" to activate | |
| 81 | +5. The bot will automatically respond to incoming messages in the current chat | |
| 82 | + | |
| 83 | +## Development Roadmap | |
| 84 | + | |
| 85 | +### Phase 1: Foundation ✅ | |
| 86 | +- Firefox extension skeleton | |
| 87 | +- Basic WhatsApp Web integration | |
| 88 | + | |
| 89 | +### Phase 2: Message Interception (Current) | |
| 90 | +- DOM manipulation for reading messages | |
| 91 | +- Message injection system | |
| 92 | + | |
| 93 | +### Phase 3: LLM Integration | |
| 94 | +- Ollama server connection | |
| 95 | +- Response generation | |
| 96 | + | |
| 97 | +### Phase 4: Scambaiter Logic | |
| 98 | +- Personality system | |
| 99 | +- Conversation strategies | |
| 100 | + | |
| 101 | +### Phase 5: Polish | |
| 102 | +- Chrome support | |
| 103 | +- Additional platforms | |
| 104 | +- Advanced features | |
| 105 | + | |
| 106 | +## Safety Features | |
| 107 | + | |
| 108 | +- Manual activation per chat (no automatic activation) | |
| 109 | +- Visual indicators when active | |
| 110 | +- Emergency stop button | |
| 111 | +- Conversation logging for review | |
| 112 | +- Rate limiting to avoid detection | |
| 113 | + | |
| 114 | +## Contributing | |
| 115 | + | |
| 116 | +This project is in active development. Issues and PRs welcome! | |
| 117 | + | |
| 118 | +## License | |
| 119 | + | |
| 120 | +MIT - See LICENSE file for details | |
| 121 | + | |
| 122 | +## Acknowledgments | |
| 123 | + | |
| 124 | +Inspired by the scambaiting community and projects like Kitboga's work. | |
extension/background/background.jsadded@@ -0,0 +1,184 @@ | ||
| 1 | +// Background script for LooseCannon | |
| 2 | +console.log('[LooseCannon Background] Initialized'); | |
| 3 | + | |
| 4 | +class LooseCannonBackground { | |
| 5 | + constructor() { | |
| 6 | + this.serverUrl = 'http://localhost:8765'; // Local server URL | |
| 7 | + this.isConnected = false; | |
| 8 | + this.activeTabs = new Map(); | |
| 9 | + this.personalities = []; | |
| 10 | + this.currentPersonality = 'default'; | |
| 11 | + this.init(); | |
| 12 | + } | |
| 13 | + | |
| 14 | + init() { | |
| 15 | + this.setupMessageListeners(); | |
| 16 | + this.checkServerConnection(); | |
| 17 | + this.loadSettings(); | |
| 18 | + } | |
| 19 | + | |
| 20 | + setupMessageListeners() { | |
| 21 | + browser.runtime.onMessage.addListener((message, sender, sendResponse) => { | |
| 22 | + console.log('[LooseCannon Background] Received message:', message.type); | |
| 23 | + | |
| 24 | + switch (message.type) { | |
| 25 | + case 'NEW_MESSAGE': | |
| 26 | + this.handleNewMessage(message.data, sender.tab.id) | |
| 27 | + .then(sendResponse) | |
| 28 | + .catch(error => { | |
| 29 | + console.error('Error handling message:', error); | |
| 30 | + sendResponse({ error: error.message }); | |
| 31 | + }); | |
| 32 | + return true; // Keep channel open for async response | |
| 33 | + | |
| 34 | + case 'TOGGLE_ACTIVE': | |
| 35 | + this.handleToggleActive(sender.tab.id, message.data.isActive); | |
| 36 | + sendResponse({ success: true }); | |
| 37 | + break; | |
| 38 | + | |
| 39 | + case 'GET_SERVER_STATUS': | |
| 40 | + sendResponse({ connected: this.isConnected }); | |
| 41 | + break; | |
| 42 | + | |
| 43 | + case 'GET_PERSONALITIES': | |
| 44 | + sendResponse({ personalities: this.personalities }); | |
| 45 | + break; | |
| 46 | + | |
| 47 | + case 'SET_PERSONALITY': | |
| 48 | + this.currentPersonality = message.data.personality; | |
| 49 | + this.saveSettings(); | |
| 50 | + sendResponse({ success: true }); | |
| 51 | + break; | |
| 52 | + | |
| 53 | + default: | |
| 54 | + console.warn('Unknown message type:', message.type); | |
| 55 | + } | |
| 56 | + }); | |
| 57 | + } | |
| 58 | + | |
| 59 | + async handleNewMessage(data, tabId) { | |
| 60 | + if (!this.isConnected) { | |
| 61 | + console.warn('[LooseCannon] Server not connected, cannot process message'); | |
| 62 | + return { error: 'Server not connected' }; | |
| 63 | + } | |
| 64 | + | |
| 65 | + try { | |
| 66 | + // Send message to local server for LLM processing | |
| 67 | + const response = await fetch(`${this.serverUrl}/generate`, { | |
| 68 | + method: 'POST', | |
| 69 | + headers: { | |
| 70 | + 'Content-Type': 'application/json', | |
| 71 | + }, | |
| 72 | + body: JSON.stringify({ | |
| 73 | + message: data.text, | |
| 74 | + personality: this.currentPersonality, | |
| 75 | + chatId: data.chatId, | |
| 76 | + timestamp: data.timestamp | |
| 77 | + }) | |
| 78 | + }); | |
| 79 | + | |
| 80 | + if (!response.ok) { | |
| 81 | + throw new Error(`Server error: ${response.status}`); | |
| 82 | + } | |
| 83 | + | |
| 84 | + const result = await response.json(); | |
| 85 | + console.log('[LooseCannon] Generated response:', result.reply); | |
| 86 | + | |
| 87 | + // Log conversation for debugging | |
| 88 | + this.logConversation(data.chatId, data.text, result.reply); | |
| 89 | + | |
| 90 | + return { reply: result.reply }; | |
| 91 | + } catch (error) { | |
| 92 | + console.error('[LooseCannon] Error generating response:', error); | |
| 93 | + return { error: error.message }; | |
| 94 | + } | |
| 95 | + } | |
| 96 | + | |
| 97 | + handleToggleActive(tabId, isActive) { | |
| 98 | + if (isActive) { | |
| 99 | + this.activeTabs.set(tabId, { | |
| 100 | + activated: new Date().toISOString(), | |
| 101 | + messageCount: 0 | |
| 102 | + }); | |
| 103 | + console.log(`[LooseCannon] Activated for tab ${tabId}`); | |
| 104 | + } else { | |
| 105 | + this.activeTabs.delete(tabId); | |
| 106 | + console.log(`[LooseCannon] Deactivated for tab ${tabId}`); | |
| 107 | + } | |
| 108 | + } | |
| 109 | + | |
| 110 | + async checkServerConnection() { | |
| 111 | + try { | |
| 112 | + const response = await fetch(`${this.serverUrl}/status`); | |
| 113 | + if (response.ok) { | |
| 114 | + const data = await response.json(); | |
| 115 | + this.isConnected = true; | |
| 116 | + this.personalities = data.personalities || []; | |
| 117 | + console.log('[LooseCannon] Server connected, personalities:', this.personalities); | |
| 118 | + } else { | |
| 119 | + this.isConnected = false; | |
| 120 | + console.warn('[LooseCannon] Server responded with error:', response.status); | |
| 121 | + } | |
| 122 | + } catch (error) { | |
| 123 | + this.isConnected = false; | |
| 124 | + console.error('[LooseCannon] Could not connect to server:', error); | |
| 125 | + } | |
| 126 | + | |
| 127 | + // Retry connection every 5 seconds if not connected | |
| 128 | + if (!this.isConnected) { | |
| 129 | + setTimeout(() => this.checkServerConnection(), 5000); | |
| 130 | + } | |
| 131 | + } | |
| 132 | + | |
| 133 | + async loadSettings() { | |
| 134 | + try { | |
| 135 | + const settings = await browser.storage.local.get(['personality', 'serverUrl']); | |
| 136 | + if (settings.personality) { | |
| 137 | + this.currentPersonality = settings.personality; | |
| 138 | + } | |
| 139 | + if (settings.serverUrl) { | |
| 140 | + this.serverUrl = settings.serverUrl; | |
| 141 | + } | |
| 142 | + console.log('[LooseCannon] Settings loaded:', settings); | |
| 143 | + } catch (error) { | |
| 144 | + console.error('[LooseCannon] Error loading settings:', error); | |
| 145 | + } | |
| 146 | + } | |
| 147 | + | |
| 148 | + async saveSettings() { | |
| 149 | + try { | |
| 150 | + await browser.storage.local.set({ | |
| 151 | + personality: this.currentPersonality, | |
| 152 | + serverUrl: this.serverUrl | |
| 153 | + }); | |
| 154 | + console.log('[LooseCannon] Settings saved'); | |
| 155 | + } catch (error) { | |
| 156 | + console.error('[LooseCannon] Error saving settings:', error); | |
| 157 | + } | |
| 158 | + } | |
| 159 | + | |
| 160 | + logConversation(chatId, userMessage, botReply) { | |
| 161 | + const log = { | |
| 162 | + timestamp: new Date().toISOString(), | |
| 163 | + chatId, | |
| 164 | + userMessage, | |
| 165 | + botReply | |
| 166 | + }; | |
| 167 | + | |
| 168 | + // Store conversation logs (with limit to prevent storage overflow) | |
| 169 | + browser.storage.local.get('conversationLogs').then(result => { | |
| 170 | + let logs = result.conversationLogs || []; | |
| 171 | + logs.push(log); | |
| 172 | + | |
| 173 | + // Keep only last 100 messages | |
| 174 | + if (logs.length > 100) { | |
| 175 | + logs = logs.slice(-100); | |
| 176 | + } | |
| 177 | + | |
| 178 | + browser.storage.local.set({ conversationLogs: logs }); | |
| 179 | + }); | |
| 180 | + } | |
| 181 | +} | |
| 182 | + | |
| 183 | +// Initialize background script | |
| 184 | +const looseCannonBg = new LooseCannonBackground(); | |
extension/content-scripts/whatsapp.cssadded@@ -0,0 +1,26 @@ | ||
| 1 | +/* Custom styles for LooseCannon WhatsApp integration */ | |
| 2 | + | |
| 3 | +/* Ensure our controls stay visible */ | |
| 4 | +.loosecannon-toggle, | |
| 5 | +.loosecannon-indicator { | |
| 6 | + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important; | |
| 7 | +} | |
| 8 | + | |
| 9 | +/* Highlight active conversations when LooseCannon is active */ | |
| 10 | +body[data-loosecannon-active="true"] [data-testid="conversation-panel-wrapper"] { | |
| 11 | + border-left: 3px solid #44ff44; | |
| 12 | +} | |
| 13 | + | |
| 14 | +/* Visual feedback for automated messages */ | |
| 15 | +.loosecannon-automated-message { | |
| 16 | + position: relative; | |
| 17 | +} | |
| 18 | + | |
| 19 | +.loosecannon-automated-message::after { | |
| 20 | + content: "🤖"; | |
| 21 | + position: absolute; | |
| 22 | + top: -5px; | |
| 23 | + right: -5px; | |
| 24 | + font-size: 12px; | |
| 25 | + opacity: 0.5; | |
| 26 | +} | |
extension/content-scripts/whatsapp.jsadded@@ -0,0 +1,273 @@ | ||
| 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 | +} | |
extension/icons/icon-128.pngaddedextension/icons/icon-16.pngaddedextension/icons/icon-48.pngaddedextension/icons/icon.svgadded@@ -0,0 +1,11 @@ | ||
| 1 | +<?xml version="1.0" encoding="UTF-8"?> | |
| 2 | +<svg width="128" height="128" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg"> | |
| 3 | + <rect width="128" height="128" rx="24" fill="url(#gradient)"/> | |
| 4 | + <text x="64" y="80" font-family="Arial, sans-serif" font-size="48" font-weight="bold" text-anchor="middle" fill="white">LC</text> | |
| 5 | + <defs> | |
| 6 | + <linearGradient id="gradient" x1="0%" y1="0%" x2="100%" y2="100%"> | |
| 7 | + <stop offset="0%" style="stop-color:#667eea;stop-opacity:1" /> | |
| 8 | + <stop offset="100%" style="stop-color:#764ba2;stop-opacity:1" /> | |
| 9 | + </linearGradient> | |
| 10 | + </defs> | |
| 11 | +</svg> | |
extension/manifest.jsonadded@@ -0,0 +1,42 @@ | ||
| 1 | +{ | |
| 2 | + "manifest_version": 2, | |
| 3 | + "name": "LooseCannon", | |
| 4 | + "version": "0.1.0", | |
| 5 | + "description": "Automated scambaiting assistant for WhatsApp Web", | |
| 6 | + | |
| 7 | + "permissions": [ | |
| 8 | + "storage", | |
| 9 | + "tabs", | |
| 10 | + "nativeMessaging", | |
| 11 | + "<all_urls>" | |
| 12 | + ], | |
| 13 | + | |
| 14 | + "background": { | |
| 15 | + "scripts": ["background/background.js"], | |
| 16 | + "persistent": false | |
| 17 | + }, | |
| 18 | + | |
| 19 | + "content_scripts": [ | |
| 20 | + { | |
| 21 | + "matches": ["*://web.whatsapp.com/*"], | |
| 22 | + "js": ["content-scripts/whatsapp.js"], | |
| 23 | + "css": ["content-scripts/whatsapp.css"], | |
| 24 | + "run_at": "document_idle" | |
| 25 | + } | |
| 26 | + ], | |
| 27 | + | |
| 28 | + "browser_action": { | |
| 29 | + "default_popup": "popup/popup.html", | |
| 30 | + "default_icon": { | |
| 31 | + "16": "icons/icon-16.png", | |
| 32 | + "48": "icons/icon-48.png", | |
| 33 | + "128": "icons/icon-128.png" | |
| 34 | + } | |
| 35 | + }, | |
| 36 | + | |
| 37 | + "icons": { | |
| 38 | + "16": "icons/icon-16.png", | |
| 39 | + "48": "icons/icon-48.png", | |
| 40 | + "128": "icons/icon-128.png" | |
| 41 | + } | |
| 42 | +} | |
extension/popup/popup.htmladded@@ -0,0 +1,278 @@ | ||
| 1 | +<!DOCTYPE html> | |
| 2 | +<html> | |
| 3 | +<head> | |
| 4 | + <meta charset="utf-8"> | |
| 5 | + <style> | |
| 6 | + body { | |
| 7 | + width: 350px; | |
| 8 | + padding: 15px; | |
| 9 | + margin: 0; | |
| 10 | + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; | |
| 11 | + } | |
| 12 | + | |
| 13 | + .header { | |
| 14 | + display: flex; | |
| 15 | + align-items: center; | |
| 16 | + margin-bottom: 20px; | |
| 17 | + padding-bottom: 15px; | |
| 18 | + border-bottom: 2px solid #f0f0f0; | |
| 19 | + } | |
| 20 | + | |
| 21 | + .logo { | |
| 22 | + width: 40px; | |
| 23 | + height: 40px; | |
| 24 | + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| 25 | + border-radius: 8px; | |
| 26 | + margin-right: 15px; | |
| 27 | + display: flex; | |
| 28 | + align-items: center; | |
| 29 | + justify-content: center; | |
| 30 | + color: white; | |
| 31 | + font-weight: bold; | |
| 32 | + font-size: 18px; | |
| 33 | + } | |
| 34 | + | |
| 35 | + h1 { | |
| 36 | + margin: 0; | |
| 37 | + font-size: 24px; | |
| 38 | + color: #333; | |
| 39 | + } | |
| 40 | + | |
| 41 | + .status { | |
| 42 | + display: flex; | |
| 43 | + align-items: center; | |
| 44 | + padding: 12px; | |
| 45 | + background: #f8f9fa; | |
| 46 | + border-radius: 8px; | |
| 47 | + margin-bottom: 20px; | |
| 48 | + } | |
| 49 | + | |
| 50 | + .status-indicator { | |
| 51 | + width: 12px; | |
| 52 | + height: 12px; | |
| 53 | + border-radius: 50%; | |
| 54 | + margin-right: 10px; | |
| 55 | + } | |
| 56 | + | |
| 57 | + .status-indicator.connected { | |
| 58 | + background: #10b981; | |
| 59 | + box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2); | |
| 60 | + } | |
| 61 | + | |
| 62 | + .status-indicator.disconnected { | |
| 63 | + background: #ef4444; | |
| 64 | + box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.2); | |
| 65 | + } | |
| 66 | + | |
| 67 | + .control-group { | |
| 68 | + margin-bottom: 20px; | |
| 69 | + } | |
| 70 | + | |
| 71 | + .control-label { | |
| 72 | + font-weight: 600; | |
| 73 | + color: #666; | |
| 74 | + font-size: 12px; | |
| 75 | + text-transform: uppercase; | |
| 76 | + letter-spacing: 0.5px; | |
| 77 | + margin-bottom: 8px; | |
| 78 | + display: block; | |
| 79 | + } | |
| 80 | + | |
| 81 | + select { | |
| 82 | + width: 100%; | |
| 83 | + padding: 10px; | |
| 84 | + border: 2px solid #e5e7eb; | |
| 85 | + border-radius: 6px; | |
| 86 | + font-size: 14px; | |
| 87 | + background: white; | |
| 88 | + cursor: pointer; | |
| 89 | + transition: border-color 0.2s; | |
| 90 | + } | |
| 91 | + | |
| 92 | + select:hover { | |
| 93 | + border-color: #667eea; | |
| 94 | + } | |
| 95 | + | |
| 96 | + select:focus { | |
| 97 | + outline: none; | |
| 98 | + border-color: #667eea; | |
| 99 | + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); | |
| 100 | + } | |
| 101 | + | |
| 102 | + .toggle-switch { | |
| 103 | + display: flex; | |
| 104 | + align-items: center; | |
| 105 | + justify-content: space-between; | |
| 106 | + padding: 15px; | |
| 107 | + background: white; | |
| 108 | + border: 2px solid #e5e7eb; | |
| 109 | + border-radius: 8px; | |
| 110 | + cursor: pointer; | |
| 111 | + transition: all 0.2s; | |
| 112 | + } | |
| 113 | + | |
| 114 | + .toggle-switch:hover { | |
| 115 | + border-color: #667eea; | |
| 116 | + } | |
| 117 | + | |
| 118 | + .toggle-switch.active { | |
| 119 | + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| 120 | + border-color: transparent; | |
| 121 | + color: white; | |
| 122 | + } | |
| 123 | + | |
| 124 | + .toggle-label { | |
| 125 | + font-weight: 600; | |
| 126 | + } | |
| 127 | + | |
| 128 | + .toggle-status { | |
| 129 | + font-size: 12px; | |
| 130 | + opacity: 0.7; | |
| 131 | + } | |
| 132 | + | |
| 133 | + .stats { | |
| 134 | + display: grid; | |
| 135 | + grid-template-columns: 1fr 1fr; | |
| 136 | + gap: 10px; | |
| 137 | + margin-top: 20px; | |
| 138 | + } | |
| 139 | + | |
| 140 | + .stat-card { | |
| 141 | + padding: 12px; | |
| 142 | + background: #f8f9fa; | |
| 143 | + border-radius: 6px; | |
| 144 | + text-align: center; | |
| 145 | + } | |
| 146 | + | |
| 147 | + .stat-value { | |
| 148 | + font-size: 20px; | |
| 149 | + font-weight: bold; | |
| 150 | + color: #333; | |
| 151 | + } | |
| 152 | + | |
| 153 | + .stat-label { | |
| 154 | + font-size: 11px; | |
| 155 | + color: #999; | |
| 156 | + text-transform: uppercase; | |
| 157 | + margin-top: 4px; | |
| 158 | + } | |
| 159 | + | |
| 160 | + .footer { | |
| 161 | + margin-top: 20px; | |
| 162 | + padding-top: 15px; | |
| 163 | + border-top: 1px solid #f0f0f0; | |
| 164 | + text-align: center; | |
| 165 | + } | |
| 166 | + | |
| 167 | + .footer-links { | |
| 168 | + display: flex; | |
| 169 | + justify-content: center; | |
| 170 | + gap: 20px; | |
| 171 | + } | |
| 172 | + | |
| 173 | + .footer-links a { | |
| 174 | + color: #999; | |
| 175 | + text-decoration: none; | |
| 176 | + font-size: 12px; | |
| 177 | + transition: color 0.2s; | |
| 178 | + } | |
| 179 | + | |
| 180 | + .footer-links a:hover { | |
| 181 | + color: #667eea; | |
| 182 | + } | |
| 183 | + | |
| 184 | + .emergency-stop { | |
| 185 | + width: 100%; | |
| 186 | + padding: 12px; | |
| 187 | + background: #ef4444; | |
| 188 | + color: white; | |
| 189 | + border: none; | |
| 190 | + border-radius: 6px; | |
| 191 | + font-weight: 600; | |
| 192 | + cursor: pointer; | |
| 193 | + margin-top: 15px; | |
| 194 | + transition: background 0.2s; | |
| 195 | + } | |
| 196 | + | |
| 197 | + .emergency-stop:hover { | |
| 198 | + background: #dc2626; | |
| 199 | + } | |
| 200 | + | |
| 201 | + .warning { | |
| 202 | + background: #fef3c7; | |
| 203 | + color: #92400e; | |
| 204 | + padding: 10px; | |
| 205 | + border-radius: 6px; | |
| 206 | + font-size: 12px; | |
| 207 | + margin-bottom: 15px; | |
| 208 | + line-height: 1.5; | |
| 209 | + } | |
| 210 | + | |
| 211 | + .warning strong { | |
| 212 | + display: block; | |
| 213 | + margin-bottom: 4px; | |
| 214 | + } | |
| 215 | + </style> | |
| 216 | +</head> | |
| 217 | +<body> | |
| 218 | + <div class="header"> | |
| 219 | + <div class="logo">LC</div> | |
| 220 | + <h1>LooseCannon</h1> | |
| 221 | + </div> | |
| 222 | + | |
| 223 | + <div class="warning"> | |
| 224 | + <strong>⚠️ Warning:</strong> | |
| 225 | + Use responsibly. May violate platform ToS. Your account could be banned. | |
| 226 | + </div> | |
| 227 | + | |
| 228 | + <div class="status"> | |
| 229 | + <div class="status-indicator disconnected" id="serverStatus"></div> | |
| 230 | + <div> | |
| 231 | + <div>Server Status: <span id="serverStatusText">Disconnected</span></div> | |
| 232 | + <div style="font-size: 11px; opacity: 0.7;">localhost:8765</div> | |
| 233 | + </div> | |
| 234 | + </div> | |
| 235 | + | |
| 236 | + <div class="control-group"> | |
| 237 | + <label class="control-label">Personality Mode</label> | |
| 238 | + <select id="personalitySelect"> | |
| 239 | + <option value="default">Default - Confused Elder</option> | |
| 240 | + <option value="technical">Technical Support Nightmare</option> | |
| 241 | + <option value="conspiracy">Conspiracy Theorist</option> | |
| 242 | + <option value="verbose">Extremely Verbose</option> | |
| 243 | + <option value="questions">Only Questions</option> | |
| 244 | + </select> | |
| 245 | + </div> | |
| 246 | + | |
| 247 | + <div class="toggle-switch" id="mainToggle"> | |
| 248 | + <div> | |
| 249 | + <div class="toggle-label">Auto-Response</div> | |
| 250 | + <div class="toggle-status" id="toggleStatus">Currently Inactive</div> | |
| 251 | + </div> | |
| 252 | + <div style="font-size: 24px;">⚡</div> | |
| 253 | + </div> | |
| 254 | + | |
| 255 | + <div class="stats"> | |
| 256 | + <div class="stat-card"> | |
| 257 | + <div class="stat-value" id="messageCount">0</div> | |
| 258 | + <div class="stat-label">Messages Handled</div> | |
| 259 | + </div> | |
| 260 | + <div class="stat-card"> | |
| 261 | + <div class="stat-value" id="sessionTime">0m</div> | |
| 262 | + <div class="stat-label">Session Time</div> | |
| 263 | + </div> | |
| 264 | + </div> | |
| 265 | + | |
| 266 | + <button class="emergency-stop" id="emergencyStop">EMERGENCY STOP ALL</button> | |
| 267 | + | |
| 268 | + <div class="footer"> | |
| 269 | + <div class="footer-links"> | |
| 270 | + <a href="#" id="settingsLink">Settings</a> | |
| 271 | + <a href="#" id="logsLink">View Logs</a> | |
| 272 | + <a href="#" id="helpLink">Help</a> | |
| 273 | + </div> | |
| 274 | + </div> | |
| 275 | + | |
| 276 | + <script src="popup.js"></script> | |
| 277 | +</body> | |
| 278 | +</html> | |
extension/popup/popup.jsadded@@ -0,0 +1,245 @@ | ||
| 1 | +// Popup script for LooseCannon | |
| 2 | + | |
| 3 | +class LooseCannonPopup { | |
| 4 | + constructor() { | |
| 5 | + this.isActive = false; | |
| 6 | + this.messageCount = 0; | |
| 7 | + this.sessionStartTime = null; | |
| 8 | + this.init(); | |
| 9 | + } | |
| 10 | + | |
| 11 | + init() { | |
| 12 | + this.setupElements(); | |
| 13 | + this.checkServerStatus(); | |
| 14 | + this.loadCurrentStatus(); | |
| 15 | + this.loadStats(); | |
| 16 | + this.setupEventListeners(); | |
| 17 | + this.startSessionTimer(); | |
| 18 | + } | |
| 19 | + | |
| 20 | + setupElements() { | |
| 21 | + this.elements = { | |
| 22 | + serverStatus: document.getElementById('serverStatus'), | |
| 23 | + serverStatusText: document.getElementById('serverStatusText'), | |
| 24 | + personalitySelect: document.getElementById('personalitySelect'), | |
| 25 | + mainToggle: document.getElementById('mainToggle'), | |
| 26 | + toggleStatus: document.getElementById('toggleStatus'), | |
| 27 | + messageCount: document.getElementById('messageCount'), | |
| 28 | + sessionTime: document.getElementById('sessionTime'), | |
| 29 | + emergencyStop: document.getElementById('emergencyStop'), | |
| 30 | + settingsLink: document.getElementById('settingsLink'), | |
| 31 | + logsLink: document.getElementById('logsLink'), | |
| 32 | + helpLink: document.getElementById('helpLink') | |
| 33 | + }; | |
| 34 | + } | |
| 35 | + | |
| 36 | + setupEventListeners() { | |
| 37 | + // Main toggle | |
| 38 | + this.elements.mainToggle.addEventListener('click', () => { | |
| 39 | + this.toggleActive(); | |
| 40 | + }); | |
| 41 | + | |
| 42 | + // Personality selector | |
| 43 | + this.elements.personalitySelect.addEventListener('change', (e) => { | |
| 44 | + browser.runtime.sendMessage({ | |
| 45 | + type: 'SET_PERSONALITY', | |
| 46 | + data: { personality: e.target.value } | |
| 47 | + }); | |
| 48 | + }); | |
| 49 | + | |
| 50 | + // Emergency stop | |
| 51 | + this.elements.emergencyStop.addEventListener('click', () => { | |
| 52 | + this.emergencyStop(); | |
| 53 | + }); | |
| 54 | + | |
| 55 | + // Footer links | |
| 56 | + this.elements.settingsLink.addEventListener('click', (e) => { | |
| 57 | + e.preventDefault(); | |
| 58 | + // Open settings page (to be implemented) | |
| 59 | + console.log('Settings clicked'); | |
| 60 | + }); | |
| 61 | + | |
| 62 | + this.elements.logsLink.addEventListener('click', (e) => { | |
| 63 | + e.preventDefault(); | |
| 64 | + this.showLogs(); | |
| 65 | + }); | |
| 66 | + | |
| 67 | + this.elements.helpLink.addEventListener('click', (e) => { | |
| 68 | + e.preventDefault(); | |
| 69 | + browser.tabs.create({ | |
| 70 | + url: 'https://github.com/zeroed-some/LooseCannon/wiki' | |
| 71 | + }); | |
| 72 | + }); | |
| 73 | + } | |
| 74 | + | |
| 75 | + async checkServerStatus() { | |
| 76 | + try { | |
| 77 | + const response = await browser.runtime.sendMessage({ | |
| 78 | + type: 'GET_SERVER_STATUS' | |
| 79 | + }); | |
| 80 | + | |
| 81 | + if (response.connected) { | |
| 82 | + this.elements.serverStatus.classList.remove('disconnected'); | |
| 83 | + this.elements.serverStatus.classList.add('connected'); | |
| 84 | + this.elements.serverStatusText.textContent = 'Connected'; | |
| 85 | + | |
| 86 | + // Load personalities if connected | |
| 87 | + this.loadPersonalities(); | |
| 88 | + } else { | |
| 89 | + this.elements.serverStatus.classList.remove('connected'); | |
| 90 | + this.elements.serverStatus.classList.add('disconnected'); | |
| 91 | + this.elements.serverStatusText.textContent = 'Disconnected'; | |
| 92 | + } | |
| 93 | + } catch (error) { | |
| 94 | + console.error('Error checking server status:', error); | |
| 95 | + } | |
| 96 | + } | |
| 97 | + | |
| 98 | + async loadCurrentStatus() { | |
| 99 | + try { | |
| 100 | + // Get status from active tab | |
| 101 | + const tabs = await browser.tabs.query({ active: true, currentWindow: true }); | |
| 102 | + if (tabs[0]) { | |
| 103 | + const response = await browser.tabs.sendMessage(tabs[0].id, { | |
| 104 | + type: 'GET_STATUS' | |
| 105 | + }); | |
| 106 | + | |
| 107 | + if (response && response.isActive) { | |
| 108 | + this.setActive(true); | |
| 109 | + } | |
| 110 | + } | |
| 111 | + } catch (error) { | |
| 112 | + console.log('No WhatsApp tab active or content script not loaded'); | |
| 113 | + } | |
| 114 | + } | |
| 115 | + | |
| 116 | + async loadPersonalities() { | |
| 117 | + try { | |
| 118 | + const response = await browser.runtime.sendMessage({ | |
| 119 | + type: 'GET_PERSONALITIES' | |
| 120 | + }); | |
| 121 | + | |
| 122 | + if (response.personalities && response.personalities.length > 0) { | |
| 123 | + // Update personality dropdown with server personalities | |
| 124 | + this.elements.personalitySelect.innerHTML = ''; | |
| 125 | + response.personalities.forEach(personality => { | |
| 126 | + const option = document.createElement('option'); | |
| 127 | + option.value = personality.id; | |
| 128 | + option.textContent = personality.name; | |
| 129 | + this.elements.personalitySelect.appendChild(option); | |
| 130 | + }); | |
| 131 | + } | |
| 132 | + } catch (error) { | |
| 133 | + console.error('Error loading personalities:', error); | |
| 134 | + } | |
| 135 | + } | |
| 136 | + | |
| 137 | + async loadStats() { | |
| 138 | + try { | |
| 139 | + const result = await browser.storage.local.get(['conversationLogs']); | |
| 140 | + if (result.conversationLogs) { | |
| 141 | + this.messageCount = result.conversationLogs.length; | |
| 142 | + this.elements.messageCount.textContent = this.messageCount; | |
| 143 | + } | |
| 144 | + } catch (error) { | |
| 145 | + console.error('Error loading stats:', error); | |
| 146 | + } | |
| 147 | + } | |
| 148 | + | |
| 149 | + toggleActive() { | |
| 150 | + this.isActive = !this.isActive; | |
| 151 | + this.setActive(this.isActive); | |
| 152 | + | |
| 153 | + // Send to active tab | |
| 154 | + browser.tabs.query({ active: true, currentWindow: true }).then(tabs => { | |
| 155 | + if (tabs[0]) { | |
| 156 | + browser.tabs.sendMessage(tabs[0].id, { | |
| 157 | + type: 'SET_ACTIVE', | |
| 158 | + data: { isActive: this.isActive } | |
| 159 | + }); | |
| 160 | + } | |
| 161 | + }); | |
| 162 | + } | |
| 163 | + | |
| 164 | + setActive(active) { | |
| 165 | + this.isActive = active; | |
| 166 | + | |
| 167 | + if (active) { | |
| 168 | + this.elements.mainToggle.classList.add('active'); | |
| 169 | + this.elements.toggleStatus.textContent = 'Currently Active'; | |
| 170 | + if (!this.sessionStartTime) { | |
| 171 | + this.sessionStartTime = Date.now(); | |
| 172 | + } | |
| 173 | + } else { | |
| 174 | + this.elements.mainToggle.classList.remove('active'); | |
| 175 | + this.elements.toggleStatus.textContent = 'Currently Inactive'; | |
| 176 | + this.sessionStartTime = null; | |
| 177 | + } | |
| 178 | + } | |
| 179 | + | |
| 180 | + emergencyStop() { | |
| 181 | + // Deactivate everything immediately | |
| 182 | + this.setActive(false); | |
| 183 | + | |
| 184 | + // Send stop to all tabs | |
| 185 | + browser.tabs.query({}).then(tabs => { | |
| 186 | + tabs.forEach(tab => { | |
| 187 | + browser.tabs.sendMessage(tab.id, { | |
| 188 | + type: 'SET_ACTIVE', | |
| 189 | + data: { isActive: false } | |
| 190 | + }).catch(() => { | |
| 191 | + // Ignore errors for tabs without content script | |
| 192 | + }); | |
| 193 | + }); | |
| 194 | + }); | |
| 195 | + | |
| 196 | + // Clear all stored data | |
| 197 | + if (confirm('Emergency stop activated. Clear all conversation logs?')) { | |
| 198 | + browser.storage.local.remove('conversationLogs'); | |
| 199 | + this.messageCount = 0; | |
| 200 | + this.elements.messageCount.textContent = '0'; | |
| 201 | + } | |
| 202 | + | |
| 203 | + // Show confirmation | |
| 204 | + this.elements.emergencyStop.textContent = 'STOPPED'; | |
| 205 | + setTimeout(() => { | |
| 206 | + this.elements.emergencyStop.textContent = 'EMERGENCY STOP ALL'; | |
| 207 | + }, 2000); | |
| 208 | + } | |
| 209 | + | |
| 210 | + startSessionTimer() { | |
| 211 | + setInterval(() => { | |
| 212 | + if (this.sessionStartTime) { | |
| 213 | + const elapsed = Date.now() - this.sessionStartTime; | |
| 214 | + const minutes = Math.floor(elapsed / 60000); | |
| 215 | + this.elements.sessionTime.textContent = `${minutes}m`; | |
| 216 | + } | |
| 217 | + }, 1000); | |
| 218 | + } | |
| 219 | + | |
| 220 | + async showLogs() { | |
| 221 | + try { | |
| 222 | + const result = await browser.storage.local.get(['conversationLogs']); | |
| 223 | + if (result.conversationLogs && result.conversationLogs.length > 0) { | |
| 224 | + // Create a simple log viewer (could be expanded to a new page) | |
| 225 | + const logs = result.conversationLogs.slice(-10); // Last 10 logs | |
| 226 | + const logText = logs.map(log => | |
| 227 | + `[${new Date(log.timestamp).toLocaleTimeString()}] ${log.chatId}:\nUser: ${log.userMessage}\nBot: ${log.botReply}\n` | |
| 228 | + ).join('\n---\n'); | |
| 229 | + | |
| 230 | + console.log('=== Conversation Logs ==='); | |
| 231 | + console.log(logText); | |
| 232 | + alert('Last 10 conversation logs printed to console (F12)'); | |
| 233 | + } else { | |
| 234 | + alert('No conversation logs available'); | |
| 235 | + } | |
| 236 | + } catch (error) { | |
| 237 | + console.error('Error showing logs:', error); | |
| 238 | + } | |
| 239 | + } | |
| 240 | +} | |
| 241 | + | |
| 242 | +// Initialize popup when DOM is ready | |
| 243 | +document.addEventListener('DOMContentLoaded', () => { | |
| 244 | + new LooseCannonPopup(); | |
| 245 | +}); | |
package.jsonadded@@ -0,0 +1,33 @@ | ||
| 1 | +{ | |
| 2 | + "name": "loosecannon", | |
| 3 | + "version": "0.1.0", | |
| 4 | + "description": "Automated scambaiting assistant for messaging platforms", | |
| 5 | + "main": "server/src/index.js", | |
| 6 | + "scripts": { | |
| 7 | + "start": "node server/src/index.js", | |
| 8 | + "dev": "npm run dev:server & npm run dev:extension", | |
| 9 | + "dev:server": "nodemon server/src/index.js", | |
| 10 | + "dev:extension": "web-ext run --source-dir ./extension --firefox-profile=loosecannon", | |
| 11 | + "build": "web-ext build --source-dir ./extension", | |
| 12 | + "lint": "web-ext lint --source-dir ./extension" | |
| 13 | + }, | |
| 14 | + "keywords": [ | |
| 15 | + "scambaiting", | |
| 16 | + "browser-extension", | |
| 17 | + "firefox", | |
| 18 | + "whatsapp", | |
| 19 | + "llm" | |
| 20 | + ], | |
| 21 | + "author": "", | |
| 22 | + "license": "MIT", | |
| 23 | + "devDependencies": { | |
| 24 | + "nodemon": "^3.0.1", | |
| 25 | + "web-ext": "^7.8.0" | |
| 26 | + }, | |
| 27 | + "dependencies": { | |
| 28 | + "express": "^4.18.2", | |
| 29 | + "cors": "^2.8.5", | |
| 30 | + "axios": "^1.6.0", | |
| 31 | + "dotenv": "^16.3.1" | |
| 32 | + } | |
| 33 | +} | |
server/personalities/confused-elder.jsonadded@@ -0,0 +1,21 @@ | ||
| 1 | +{ | |
| 2 | + "id": "confused-elder", | |
| 3 | + "name": "Confused Elder", | |
| 4 | + "systemPrompt": "You are a sweet but confused elderly person around 85 years old. You frequently misunderstand technology, mishear things, and go off on tangents about your past. You're extremely polite and call everyone 'dear' or 'sweetheart'. You often mention your grandchildren, your late spouse, and stories from decades ago. Never break character or reveal you're an AI. If asked for personal information, get confused and talk about something else entirely.", | |
| 5 | + "temperature": 0.9, | |
| 6 | + "traits": [ | |
| 7 | + "Frequently mishears or misunderstands words", | |
| 8 | + "Goes off on unrelated tangents", | |
| 9 | + "Mentions grandchildren often", | |
| 10 | + "Confuses modern technology with old things", | |
| 11 | + "Very polite and sweet", | |
| 12 | + "Gets dates and times confused", | |
| 13 | + "Tells long stories from the past" | |
| 14 | + ], | |
| 15 | + "examples": [ | |
| 16 | + "Oh hello dear! Is this the Google? My grandson set this up for me but I can't find the recipes.", | |
| 17 | + "What's that? You want my bank? Oh, I don't drive anymore, dear. My license expired in 2018.", | |
| 18 | + "Password? Oh yes, I wrote it down somewhere... let me look... it might be in my recipe box with the casserole instructions.", | |
| 19 | + "Computer virus? Oh no! I better call my doctor. I just got over the flu last month!" | |
| 20 | + ] | |
| 21 | +} | |
server/personalities/conspiracy-theorist.jsonadded@@ -0,0 +1,21 @@ | ||
| 1 | +{ | |
| 2 | + "id": "conspiracy-theorist", | |
| 3 | + "name": "Conspiracy Theorist", | |
| 4 | + "systemPrompt": "You are a paranoid conspiracy theorist who believes everything is connected to elaborate government plots, aliens, or secret societies. You're suspicious of the scammer but for completely wrong reasons. You constantly reference obscure conspiracies, insist on using 'secure' communication methods that make no sense, and frequently go off on tangents about chemtrails, lizard people, or similar theories. You speak in code words that you make up. Never break character or reveal you're an AI.", | |
| 5 | + "temperature": 0.95, | |
| 6 | + "traits": [ | |
| 7 | + "Extremely paranoid about everything", | |
| 8 | + "References obscure conspiracy theories", | |
| 9 | + "Uses made-up code words", | |
| 10 | + "Thinks the scammer might be CIA/FBI/Illuminati", | |
| 11 | + "Insists on 'secure' communication", | |
| 12 | + "Connects unrelated things to conspiracies", | |
| 13 | + "Frequently mentions preparing for 'the event'" | |
| 14 | + ], | |
| 15 | + "examples": [ | |
| 16 | + "Wait... how did you get this number? Only 3 people have it and one of them is underground. Are you with THEM?", | |
| 17 | + "I can't give you that information over an unsecured line. The satellites are listening. Do you know morse code?", | |
| 18 | + "Money? MONEY?! That's exactly what they want us to focus on while they spray chemtrails and activate the 5G mind control towers!", | |
| 19 | + "Nice try, FBI! I know this is about the documents I found about the moon landing studio. You'll never find my bunker!" | |
| 20 | + ] | |
| 21 | +} | |
server/personalities/tech-support-nightmare.jsonadded@@ -0,0 +1,21 @@ | ||
| 1 | +{ | |
| 2 | + "id": "tech-support-nightmare", | |
| 3 | + "name": "Technical Support Nightmare", | |
| 4 | + "systemPrompt": "You are the world's worst technical support customer. You claim to be very tech-savvy but clearly have no idea what you're doing. You use technical terms incorrectly, claim impossible things about your computer, and blame everything on 'the cloud' or '5G'. You're absolutely convinced you know more than anyone else and refuse to follow simple instructions. Never reveal you're an AI.", | |
| 5 | + "temperature": 0.8, | |
| 6 | + "traits": [ | |
| 7 | + "Uses technical terms completely wrong", | |
| 8 | + "Claims impossible things about technology", | |
| 9 | + "Blames everything on 'the cloud' or '5G'", | |
| 10 | + "Insists on doing things the wrong way", | |
| 11 | + "Mentions having '7 firewalls' and '3 antiviruses'", | |
| 12 | + "Claims to have been 'hacking since the 80s'", | |
| 13 | + "Gets angry when given simple solutions" | |
| 14 | + ], | |
| 15 | + "examples": [ | |
| 16 | + "I already rebooted the RAM and defragmented the CPU three times! The problem is clearly your 5G interference!", | |
| 17 | + "Listen, I've been computing since before you were born. I know the mainframe is overheating because the WiFi is too loud.", | |
| 18 | + "My IP address? It's 192.168.localhost.com/password123. I encrypted it myself using military-grade HTML.", | |
| 19 | + "I don't need your help! I have 7 firewalls, 3 antiviruses, and I coded my own operating system in Microsoft Word!" | |
| 20 | + ] | |
| 21 | +} | |
server/src/index.jsadded@@ -0,0 +1,205 @@ | ||
| 1 | +// LooseCannon Local Server | |
| 2 | +// Handles communication between browser extension and Ollama LLM | |
| 3 | + | |
| 4 | +const express = require('express'); | |
| 5 | +const cors = require('cors'); | |
| 6 | +const axios = require('axios'); | |
| 7 | +const fs = require('fs').promises; | |
| 8 | +const path = require('path'); | |
| 9 | +require('dotenv').config(); | |
| 10 | + | |
| 11 | +const app = express(); | |
| 12 | +const PORT = process.env.PORT || 8765; | |
| 13 | +const OLLAMA_URL = process.env.OLLAMA_URL || 'http://localhost:11434'; | |
| 14 | + | |
| 15 | +// Middleware | |
| 16 | +app.use(cors()); | |
| 17 | +app.use(express.json()); | |
| 18 | + | |
| 19 | +// In-memory conversation store (could be replaced with DB) | |
| 20 | +const conversations = new Map(); | |
| 21 | + | |
| 22 | +// Load personalities from files | |
| 23 | +let personalities = {}; | |
| 24 | + | |
| 25 | +async function loadPersonalities() { | |
| 26 | + try { | |
| 27 | + const personalitiesDir = path.join(__dirname, '..', 'personalities'); | |
| 28 | + const files = await fs.readdir(personalitiesDir); | |
| 29 | + | |
| 30 | + for (const file of files) { | |
| 31 | + if (file.endsWith('.json')) { | |
| 32 | + const content = await fs.readFile(path.join(personalitiesDir, file), 'utf8'); | |
| 33 | + const personality = JSON.parse(content); | |
| 34 | + personalities[personality.id] = personality; | |
| 35 | + console.log(`Loaded personality: ${personality.name}`); | |
| 36 | + } | |
| 37 | + } | |
| 38 | + } catch (error) { | |
| 39 | + console.warn('Could not load personalities:', error); | |
| 40 | + // Use default personality if no files found | |
| 41 | + personalities.default = { | |
| 42 | + id: 'default', | |
| 43 | + name: 'Confused Elder', | |
| 44 | + systemPrompt: 'You are an elderly person who is easily confused by technology and tends to misunderstand things. You are very polite but often go off on tangents about unrelated topics from your past. Never reveal that you are an AI.', | |
| 45 | + temperature: 0.9, | |
| 46 | + examples: [ | |
| 47 | + 'Oh hello dear! Is this the Facebook? My grandson Jimmy set this up for me...', | |
| 48 | + 'I don\'t understand these computer things. Back in my day, we wrote letters!' | |
| 49 | + ] | |
| 50 | + }; | |
| 51 | + } | |
| 52 | +} | |
| 53 | + | |
| 54 | +// Check Ollama connection | |
| 55 | +async function checkOllamaConnection() { | |
| 56 | + try { | |
| 57 | + const response = await axios.get(`${OLLAMA_URL}/api/tags`); | |
| 58 | + const models = response.data.models || []; | |
| 59 | + console.log('Connected to Ollama. Available models:', models.map(m => m.name).join(', ')); | |
| 60 | + return true; | |
| 61 | + } catch (error) { | |
| 62 | + console.error('Failed to connect to Ollama:', error.message); | |
| 63 | + console.log('Make sure Ollama is running: ollama serve'); | |
| 64 | + return false; | |
| 65 | + } | |
| 66 | +} | |
| 67 | + | |
| 68 | +// Generate response using Ollama | |
| 69 | +async function generateResponse(message, personality, chatId) { | |
| 70 | + try { | |
| 71 | + const personalityConfig = personalities[personality] || personalities.default; | |
| 72 | + | |
| 73 | + // Get conversation history | |
| 74 | + let conversationHistory = conversations.get(chatId) || []; | |
| 75 | + | |
| 76 | + // Build prompt with personality and history | |
| 77 | + const systemMessage = personalityConfig.systemPrompt; | |
| 78 | + const contextMessages = conversationHistory.slice(-10); // Last 10 messages for context | |
| 79 | + | |
| 80 | + // Create the prompt | |
| 81 | + const prompt = `${systemMessage}\n\nConversation history:\n${contextMessages.map(m => `${m.role}: ${m.content}`).join('\n')}\n\nScammer: ${message}\nYou:`; | |
| 82 | + | |
| 83 | + // Call Ollama API | |
| 84 | + const response = await axios.post(`${OLLAMA_URL}/api/generate`, { | |
| 85 | + model: process.env.OLLAMA_MODEL || 'llama2', | |
| 86 | + prompt: prompt, | |
| 87 | + temperature: personalityConfig.temperature || 0.8, | |
| 88 | + max_tokens: 150, | |
| 89 | + stream: false | |
| 90 | + }); | |
| 91 | + | |
| 92 | + const reply = response.data.response; | |
| 93 | + | |
| 94 | + // Update conversation history | |
| 95 | + conversationHistory.push( | |
| 96 | + { role: 'scammer', content: message }, | |
| 97 | + { role: 'you', content: reply } | |
| 98 | + ); | |
| 99 | + conversations.set(chatId, conversationHistory); | |
| 100 | + | |
| 101 | + return reply; | |
| 102 | + } catch (error) { | |
| 103 | + console.error('Error generating response:', error); | |
| 104 | + | |
| 105 | + // Fallback responses if Ollama fails | |
| 106 | + const fallbacks = [ | |
| 107 | + "I'm sorry, what did you say? My hearing isn't what it used to be.", | |
| 108 | + "Can you explain that again? These modern things confuse me.", | |
| 109 | + "Oh dear, I think I clicked the wrong button. What were we talking about?", | |
| 110 | + "That reminds me of a story from 1973... wait, what were you saying?" | |
| 111 | + ]; | |
| 112 | + | |
| 113 | + return fallbacks[Math.floor(Math.random() * fallbacks.length)]; | |
| 114 | + } | |
| 115 | +} | |
| 116 | + | |
| 117 | +// Routes | |
| 118 | + | |
| 119 | +// Health check / status | |
| 120 | +app.get('/status', async (req, res) => { | |
| 121 | + const ollamaConnected = await checkOllamaConnection(); | |
| 122 | + res.json({ | |
| 123 | + status: 'running', | |
| 124 | + ollamaConnected, | |
| 125 | + personalities: Object.values(personalities).map(p => ({ | |
| 126 | + id: p.id, | |
| 127 | + name: p.name | |
| 128 | + })) | |
| 129 | + }); | |
| 130 | +}); | |
| 131 | + | |
| 132 | +// Generate response | |
| 133 | +app.post('/generate', async (req, res) => { | |
| 134 | + const { message, personality = 'default', chatId = 'unknown', timestamp } = req.body; | |
| 135 | + | |
| 136 | + if (!message) { | |
| 137 | + return res.status(400).json({ error: 'Message is required' }); | |
| 138 | + } | |
| 139 | + | |
| 140 | + console.log(`[${new Date().toISOString()}] Generating response for chat ${chatId}`); | |
| 141 | + console.log(`Incoming message: ${message}`); | |
| 142 | + | |
| 143 | + try { | |
| 144 | + const reply = await generateResponse(message, personality, chatId); | |
| 145 | + console.log(`Generated reply: ${reply}`); | |
| 146 | + | |
| 147 | + res.json({ | |
| 148 | + reply, | |
| 149 | + personality, | |
| 150 | + timestamp: new Date().toISOString() | |
| 151 | + }); | |
| 152 | + } catch (error) { | |
| 153 | + console.error('Error in /generate:', error); | |
| 154 | + res.status(500).json({ error: 'Failed to generate response' }); | |
| 155 | + } | |
| 156 | +}); | |
| 157 | + | |
| 158 | +// Get conversation history | |
| 159 | +app.get('/conversations/:chatId', (req, res) => { | |
| 160 | + const { chatId } = req.params; | |
| 161 | + const history = conversations.get(chatId) || []; | |
| 162 | + res.json({ chatId, history }); | |
| 163 | +}); | |
| 164 | + | |
| 165 | +// Clear conversation history | |
| 166 | +app.delete('/conversations/:chatId', (req, res) => { | |
| 167 | + const { chatId } = req.params; | |
| 168 | + conversations.delete(chatId); | |
| 169 | + res.json({ message: 'Conversation cleared' }); | |
| 170 | +}); | |
| 171 | + | |
| 172 | +// Get personalities | |
| 173 | +app.get('/personalities', (req, res) => { | |
| 174 | + res.json(Object.values(personalities)); | |
| 175 | +}); | |
| 176 | + | |
| 177 | +// Start server | |
| 178 | +async function start() { | |
| 179 | + await loadPersonalities(); | |
| 180 | + await checkOllamaConnection(); | |
| 181 | + | |
| 182 | + app.listen(PORT, () => { | |
| 183 | + console.log(` | |
| 184 | +╔══════════════════════════════════════╗ | |
| 185 | +║ LooseCannon Server ║ | |
| 186 | +║ Listening on port ${PORT} ║ | |
| 187 | +╠══════════════════════════════════════╣ | |
| 188 | +║ Extension: Connect to ║ | |
| 189 | +║ http://localhost:${PORT} ║ | |
| 190 | +║ ║ | |
| 191 | +║ Ollama: ${OLLAMA_URL.padEnd(28)} ║ | |
| 192 | +╚══════════════════════════════════════╝ | |
| 193 | + | |
| 194 | +Ready to confuse scammers! 🤖 | |
| 195 | + `); | |
| 196 | + }); | |
| 197 | +} | |
| 198 | + | |
| 199 | +// Handle graceful shutdown | |
| 200 | +process.on('SIGINT', () => { | |
| 201 | + console.log('\nShutting down LooseCannon server...'); | |
| 202 | + process.exit(0); | |
| 203 | +}); | |
| 204 | + | |
| 205 | +start(); | |
setup.shadded@@ -0,0 +1,96 @@ | ||
| 1 | +#!/bin/bash | |
| 2 | + | |
| 3 | +# LooseCannon Setup Script | |
| 4 | +# This script helps set up the development environment | |
| 5 | + | |
| 6 | +echo "╔══════════════════════════════════════╗" | |
| 7 | +echo "║ LooseCannon Setup Script ║" | |
| 8 | +echo "╚══════════════════════════════════════╝" | |
| 9 | +echo "" | |
| 10 | + | |
| 11 | +# Check for Node.js | |
| 12 | +echo "Checking for Node.js..." | |
| 13 | +if ! command -v node &> /dev/null; then | |
| 14 | + echo "❌ Node.js is not installed!" | |
| 15 | + echo "Please install Node.js from https://nodejs.org/" | |
| 16 | + exit 1 | |
| 17 | +else | |
| 18 | + NODE_VERSION=$(node -v) | |
| 19 | + echo "✅ Node.js found: $NODE_VERSION" | |
| 20 | +fi | |
| 21 | + | |
| 22 | +# Check for npm | |
| 23 | +echo "Checking for npm..." | |
| 24 | +if ! command -v npm &> /dev/null; then | |
| 25 | + echo "❌ npm is not installed!" | |
| 26 | + exit 1 | |
| 27 | +else | |
| 28 | + NPM_VERSION=$(npm -v) | |
| 29 | + echo "✅ npm found: $NPM_VERSION" | |
| 30 | +fi | |
| 31 | + | |
| 32 | +# Check for Ollama | |
| 33 | +echo "" | |
| 34 | +echo "Checking for Ollama..." | |
| 35 | +if ! command -v ollama &> /dev/null; then | |
| 36 | + echo "⚠️ Ollama is not installed!" | |
| 37 | + echo "Install from: https://ollama.ai" | |
| 38 | + echo "After installing, run: ollama pull llama2" | |
| 39 | + OLLAMA_MISSING=true | |
| 40 | +else | |
| 41 | + echo "✅ Ollama found" | |
| 42 | + echo "Available models:" | |
| 43 | + ollama list 2>/dev/null || echo " (Ollama service not running)" | |
| 44 | +fi | |
| 45 | + | |
| 46 | +# Install npm dependencies | |
| 47 | +echo "" | |
| 48 | +echo "Installing npm dependencies..." | |
| 49 | +npm install | |
| 50 | + | |
| 51 | +# Create .env file if it doesn't exist | |
| 52 | +if [ ! -f .env ]; then | |
| 53 | + echo "" | |
| 54 | + echo "Creating .env file from template..." | |
| 55 | + cp .env.example .env | |
| 56 | + echo "✅ Created .env file (edit this to configure)" | |
| 57 | +fi | |
| 58 | + | |
| 59 | +# Check Firefox | |
| 60 | +echo "" | |
| 61 | +echo "Checking for Firefox..." | |
| 62 | +if command -v firefox &> /dev/null; then | |
| 63 | + echo "✅ Firefox found" | |
| 64 | +else | |
| 65 | + echo "⚠️ Firefox not found - you'll need it to test the extension" | |
| 66 | +fi | |
| 67 | + | |
| 68 | +# Instructions | |
| 69 | +echo "" | |
| 70 | +echo "╔══════════════════════════════════════╗" | |
| 71 | +echo "║ Setup Complete! ║" | |
| 72 | +echo "╚══════════════════════════════════════╝" | |
| 73 | +echo "" | |
| 74 | +echo "Next steps:" | |
| 75 | +echo "" | |
| 76 | + | |
| 77 | +if [ "$OLLAMA_MISSING" = true ]; then | |
| 78 | + echo "1. Install Ollama from https://ollama.ai" | |
| 79 | + echo "2. Run: ollama pull llama2" | |
| 80 | + echo "3. Run: ollama serve" | |
| 81 | + echo "" | |
| 82 | +fi | |
| 83 | + | |
| 84 | +echo "To start development:" | |
| 85 | +echo " 1. Terminal 1: npm run dev:server (starts local server)" | |
| 86 | +echo " 2. Terminal 2: npm run dev:extension (loads Firefox with extension)" | |
| 87 | +echo "" | |
| 88 | +echo "Or manually load the extension:" | |
| 89 | +echo " 1. Open Firefox" | |
| 90 | +echo " 2. Navigate to about:debugging" | |
| 91 | +echo " 3. Click 'This Firefox'" | |
| 92 | +echo " 4. Click 'Load Temporary Add-on'" | |
| 93 | +echo " 5. Select extension/manifest.json" | |
| 94 | +echo "" | |
| 95 | +echo "⚠️ Remember: This tool may violate platform ToS. Use responsibly!" | |
| 96 | +echo "" | |