#!/usr/bin/env node /** * iMessage Webhook Filter for Clawdbot * * Filters BlueBubbles webhooks before they reach Clawdbot. * Only forwards messages that contain "Buba" (case insensitive). * * Usage: node filter.js * * Config via env vars: * FILTER_PORT=18790 * CLAWDBOT_URL=http://127.0.0.1:18789/bluebubbles-webhook * MENTION_PATTERN=buba (case insensitive) */ const http = require('http'); const https = require('https'); const FILTER_PORT = process.env.FILTER_PORT || 18790; const CLAWDBOT_URL = process.env.CLAWDBOT_URL || 'http://127.0.0.1:18789/bluebubbles-webhook'; const MENTION_PATTERN = new RegExp(process.env.MENTION_PATTERN || '\\bbuba\\b', 'i'); // Track which chats have said the password const unlockedChats = new Set(); const PASSWORD = 'TANGO12'; function log(msg) { console.log(`[${new Date().toISOString()}] ${msg}`); } function extractMessageText(body) { try { // BlueBubbles webhook payload structure if (body.data?.text) return body.data.text; if (body.message?.text) return body.message.text; if (body.text) return body.text; // Try to find text in nested structure const str = JSON.stringify(body); return str; } catch (e) { return ''; } } function extractChatId(body) { try { if (body.data?.chatGuid) return body.data.chatGuid; if (body.data?.chat?.guid) return body.data.chat.guid; if (body.chatGuid) return body.chatGuid; if (body.chat?.guid) return body.chat.guid; return 'unknown'; } catch (e) { return 'unknown'; } } function shouldForward(body) { const text = extractMessageText(body); const chatId = extractChatId(body); // Check for password if (text.includes(PASSWORD)) { unlockedChats.add(chatId); log(`Chat ${chatId} UNLOCKED with password`); } // Always forward if it contains "Buba" if (MENTION_PATTERN.test(text)) { log(`FORWARD: Message contains mention pattern`); return true; } // Check if this is a non-message event (typing, read receipt, etc.) // These should be forwarded const eventType = body.type || body.event || ''; if (eventType && !eventType.includes('message')) { log(`FORWARD: Non-message event (${eventType})`); return true; } // Block messages without mention log(`BLOCK: No mention found in message`); return false; } function forwardToClawdbot(body, query, callback) { const url = new URL(CLAWDBOT_URL); // Preserve query params (password, guid, etc.) if (query) { const params = new URLSearchParams(query); params.forEach((value, key) => url.searchParams.set(key, value)); } const payload = JSON.stringify(body); const protocol = url.protocol === 'https:' ? https : http; const options = { hostname: url.hostname, port: url.port || (url.protocol === 'https:' ? 443 : 80), path: url.pathname + url.search, method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) } }; const req = protocol.request(options, (res) => { let data = ''; res.on('data', chunk => data += chunk); res.on('end', () => { log(`Clawdbot responded: ${res.statusCode}`); callback(null, res.statusCode, data); }); }); req.on('error', (err) => { log(`Forward error: ${err.message}`); callback(err); }); req.write(payload); req.end(); } const server = http.createServer((req, res) => { // Health check if (req.method === 'GET' && req.url === '/health') { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ status: 'ok', unlockedChats: unlockedChats.size })); return; } // Only handle POST requests if (req.method !== 'POST') { res.writeHead(405); res.end('Method not allowed'); return; } let body = ''; req.on('data', chunk => body += chunk); req.on('end', () => { let parsed; try { parsed = JSON.parse(body); } catch (e) { log(`Parse error: ${e.message}`); res.writeHead(400); res.end('Invalid JSON'); return; } const query = req.url.split('?')[1] || ''; if (shouldForward(parsed)) { forwardToClawdbot(parsed, query, (err, statusCode, data) => { if (err) { res.writeHead(502); res.end('Forward failed'); } else { res.writeHead(statusCode || 200, { 'Content-Type': 'application/json' }); res.end(data || '{"status":"forwarded"}'); } }); } else { // Return success but don't forward res.writeHead(200, { 'Content-Type': 'application/json' }); res.end('{"status":"filtered","reason":"no_mention"}'); } }); }); server.listen(FILTER_PORT, '127.0.0.1', () => { log(`iMessage filter running on port ${FILTER_PORT}`); log(`Forwarding to: ${CLAWDBOT_URL}`); log(`Mention pattern: ${MENTION_PATTERN}`); log(`Password: ${PASSWORD}`); }); process.on('SIGTERM', () => { log('Shutting down...'); server.close(() => process.exit(0)); }); process.on('SIGINT', () => { log('Shutting down...'); server.close(() => process.exit(0)); });