191 lines
5.0 KiB
JavaScript
Executable File
191 lines
5.0 KiB
JavaScript
Executable File
#!/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));
|
|
});
|