From 60a6d973f515588d035bd3ee809482b42c09e941 Mon Sep 17 00:00:00 2001 From: Johan Jongsma Date: Sun, 1 Feb 2026 08:03:40 +0000 Subject: [PATCH] Initial commit --- .gitignore | 5 + package-lock.json | 37 ++++++ package.json | 16 +++ server.js | 327 ++++++++++++++++++++++++++++++++++++++++++++++ watch-calls.sh | 10 ++ 5 files changed, 395 insertions(+) create mode 100644 .gitignore create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 server.js create mode 100755 watch-calls.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9d82971 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +.env +*.log +.DS_Store + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..1e1fb37 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,37 @@ +{ + "name": "clawdnode-gateway", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "clawdnode-gateway", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "ws": "^8.19.0" + } + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..ee3f431 --- /dev/null +++ b/package.json @@ -0,0 +1,16 @@ +{ + "name": "clawdnode-gateway", + "version": "1.0.0", + "description": "", + "main": "server.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "start": "node server.js" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "ws": "^8.19.0" + } +} diff --git a/server.js b/server.js new file mode 100644 index 0000000..f49c1a0 --- /dev/null +++ b/server.js @@ -0,0 +1,327 @@ +#!/usr/bin/env node +/** + * ClawdNode Gateway - Custom protocol for direct phone control + * + * Protocol: + * - Server sends: {"type":"welcome","protocol":"clawdnode/1.0","serverTime":...} + * - Client sends: {"type":"hello","client":"clawdnode-android","version":"..."} + * - Server sends: {"type":"ready","message":"Connected"} + * - Then bidirectional events/commands + * + * If a client connects speaking Clawdbot protocol, we send a clear error. + */ + +const http = require('http'); +const { WebSocketServer, WebSocket } = require('ws'); +const fs = require('fs'); + +const HTTP_PORT = 9877; +const WS_PORT = 9878; +const LOG_FILE = '/tmp/clawdnode-gateway.log'; +const PROTOCOL_VERSION = 'clawdnode/1.0'; + +// Connected clients +const clients = new Map(); +let commandQueue = []; + +// Logging +const logStream = fs.createWriteStream(LOG_FILE, { flags: 'a' }); +function log(msg) { + const ts = new Date().toISOString(); + const line = `[${ts}] ${msg}`; + console.log(line); + logStream.write(line + '\n'); +} + +// Callback for notifications/events (James can hook into this) +let onNotification = null; +let onCall = null; + +// ============================================ +// WebSocket Server (for Android app) +// ============================================ +const wss = new WebSocketServer({ port: WS_PORT }); + +wss.on('connection', (ws, req) => { + const clientId = `client_${Date.now()}`; + const ip = req.socket.remoteAddress; + + log(`📱 [CONNECT] ${clientId} from ${ip}`); + clients.set(clientId, { ws, ip, connectedAt: Date.now(), ready: false }); + + // Send our protocol welcome - this identifies us as ClawdNode gateway + ws.send(JSON.stringify({ + type: 'welcome', + protocol: PROTOCOL_VERSION, + serverTime: Date.now(), + message: 'ClawdNode Gateway - Send {"type":"hello",...} to connect' + })); + + ws.on('message', (data) => { + try { + const msg = JSON.parse(data.toString()); + handleMessage(clientId, msg, ws); + } catch (e) { + log(`❌ [ERROR] Parse error from ${clientId}: ${e.message}`); + ws.send(JSON.stringify({ + type: 'error', + code: 'PARSE_ERROR', + message: 'Invalid JSON' + })); + } + }); + + ws.on('close', (code, reason) => { + log(`📱 [DISCONNECT] ${clientId} code=${code} reason=${reason || 'n/a'}`); + clients.delete(clientId); + }); + + ws.on('error', (err) => { + log(`❌ [WS_ERROR] ${clientId}: ${err.message}`); + }); +}); + +function handleMessage(clientId, msg, ws) { + const type = msg.type || msg.event || 'unknown'; + const client = clients.get(clientId); + + // Detect Clawdbot protocol attempts + if (msg.method === 'connect' || msg.event === 'connect.handshake' || msg.type === 'req') { + log(`⚠️ [PROTOCOL_ERROR] ${clientId} is using Clawdbot protocol`); + ws.send(JSON.stringify({ + type: 'error', + code: 'WRONG_PROTOCOL', + message: 'This is ClawdNode Gateway (port 9878), not Clawdbot Gateway (port 18789). ' + + 'Update your app to use ws://100.123.216.65:9878 with ClawdNode protocol.', + expected: PROTOCOL_VERSION, + received: msg.method || msg.type + })); + ws.close(4000, 'Wrong protocol - use ClawdNode protocol'); + return; + } + + switch (type) { + case 'hello': + // Client hello - mark as ready + log(`👋 [HELLO] ${clientId}: ${msg.client || 'unknown'} v${msg.version || '?'}`); + if (client) { + client.ready = true; + client.clientInfo = { client: msg.client, version: msg.version }; + } + ws.send(JSON.stringify({ + type: 'ready', + message: 'Connected to ClawdNode Gateway', + protocol: PROTOCOL_VERSION + })); + break; + + case 'notification': + log(`🔔 [NOTIFICATION] ${msg.app || msg.packageName}:`); + log(` Title: ${msg.title}`); + log(` Text: ${msg.text}`); + log(` Actions: ${JSON.stringify(msg.actions || [])}`); + log(` ID: ${msg.id}`); + if (onNotification) onNotification(msg); + break; + + case 'call': + log(`📞 [CALL] ${msg.state}:`); + log(` Number: ${msg.number}`); + log(` Contact: ${msg.contact || 'Unknown'}`); + log(` CallID: ${msg.callId}`); + if (onCall) onCall(msg); + + // Alert on incoming calls + if (msg.state === 'incoming' || msg.state === 'ringing') { + log(`🚨 INCOMING CALL from ${msg.contact || msg.number}`); + } + break; + + case 'call.incoming': + log(`📞 [INCOMING CALL]:`); + log(` Number: ${msg.number}`); + log(` Contact: ${msg.contact || 'Unknown'}`); + if (onCall) onCall({ ...msg, state: 'incoming' }); + break; + + case 'response': + case 'result': + log(`✅ [RESPONSE] Command ${msg.commandId}: ${msg.success ? 'OK' : 'FAILED'}`); + if (msg.error) log(` Error: ${msg.error}`); + break; + + case 'log': + log(`📋 [LOG] ${msg.message}`); + break; + + case 'lifecycle': + log(`🔄 [LIFECYCLE:${msg.event}] ${msg.message || ''}`); + break; + + default: + log(`⚡ [EVENT:${type}] ${JSON.stringify(msg)}`); + } +} + +function broadcast(msg) { + const data = JSON.stringify(msg); + let sent = 0; + clients.forEach((client, id) => { + if (client.ws.readyState === WebSocket.OPEN && client.ready) { + client.ws.send(data); + sent++; + } + }); + return sent; +} + +function sendCommand(command, params = {}) { + const msg = { + type: 'command', + command, + params, + commandId: `cmd_${Date.now()}`, + timestamp: Date.now() + }; + + const sent = broadcast(msg); + if (sent === 0) { + commandQueue.push(msg); + log(`📤 [QUEUE] No ready clients, queued: ${command}`); + } else { + log(`📤 [COMMAND] Sent to ${sent} client(s): ${command}`); + } + return msg.commandId; +} + +// ============================================ +// HTTP API (for James to send commands) +// ============================================ +const httpServer = http.createServer(async (req, res) => { + res.setHeader('Content-Type', 'application/json'); + res.setHeader('Access-Control-Allow-Origin', '*'); + + if (req.method === 'OPTIONS') { + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); + res.end(); + return; + } + + const body = await parseBody(req); + const url = req.url.split('?')[0]; + + switch (url) { + case '/health': + const readyClients = Array.from(clients.values()).filter(c => c.ready).length; + res.end(JSON.stringify({ + status: 'ok', + protocol: PROTOCOL_VERSION, + clients: clients.size, + readyClients, + queuedCommands: commandQueue.length, + time: new Date().toISOString() + })); + break; + + case '/clients': + const clientList = []; + clients.forEach((c, id) => clientList.push({ + id, + ip: c.ip, + ready: c.ready, + clientInfo: c.clientInfo, + connectedAt: c.connectedAt, + uptime: Date.now() - c.connectedAt + })); + res.end(JSON.stringify({ clients: clientList })); + break; + + case '/command': + if (req.method !== 'POST') { + res.statusCode = 405; + res.end(JSON.stringify({ error: 'POST required' })); + return; + } + const cmdId = sendCommand(body.command, body.params || {}); + res.end(JSON.stringify({ status: 'sent', commandId: cmdId, clients: clients.size })); + break; + + case '/notification/action': + if (req.method !== 'POST') { + res.statusCode = 405; + res.end(JSON.stringify({ error: 'POST required' })); + return; + } + const actionCmdId = sendCommand('notification.action', { + notificationId: body.notificationId || body.id, + action: body.action, + replyText: body.replyText || body.text + }); + log(`🔔 [ACTION] Triggering "${body.action}" on ${body.notificationId || body.id}`); + res.end(JSON.stringify({ status: 'sent', commandId: actionCmdId })); + break; + + case '/call/answer': + const answerCmdId = sendCommand('call.answer', { callId: body.callId }); + log(`📞 [ANSWER] Answering call ${body.callId}`); + res.end(JSON.stringify({ status: 'sent', commandId: answerCmdId })); + break; + + case '/call/reject': + const rejectCmdId = sendCommand('call.reject', { callId: body.callId }); + log(`📞 [REJECT] Rejecting call ${body.callId}`); + res.end(JSON.stringify({ status: 'sent', commandId: rejectCmdId })); + break; + + case '/call/hangup': + const hangupCmdId = sendCommand('call.hangup', { callId: body.callId }); + log(`📞 [HANGUP] Hanging up call ${body.callId}`); + res.end(JSON.stringify({ status: 'sent', commandId: hangupCmdId })); + break; + + default: + res.end(JSON.stringify({ + name: 'ClawdNode Gateway', + protocol: PROTOCOL_VERSION, + endpoints: [ + 'GET /health - Server status', + 'GET /clients - Connected clients', + 'POST /command - Send raw command', + 'POST /notification/action - Trigger notification action', + 'POST /call/answer - Answer incoming call', + 'POST /call/reject - Reject incoming call', + 'POST /call/hangup - Hang up active call' + ], + note: 'This is ClawdNode Gateway (custom protocol), NOT Clawdbot Gateway.' + })); + } +}); + +function parseBody(req) { + return new Promise((resolve) => { + let body = ''; + req.on('data', chunk => body += chunk); + req.on('end', () => { + try { resolve(JSON.parse(body || '{}')); } + catch { resolve({}); } + }); + }); +} + +httpServer.listen(HTTP_PORT, '0.0.0.0', () => { + log('================================================'); + log('ClawdNode Gateway - Custom Protocol'); + log(`Protocol: ${PROTOCOL_VERSION}`); + log('================================================'); + log(`HTTP API: http://100.123.216.65:${HTTP_PORT}`); + log(`WebSocket: ws://100.123.216.65:${WS_PORT}`); + log(`Log file: ${LOG_FILE}`); + log(''); + log('NOTE: This is NOT Clawdbot Gateway (port 18789).'); + log(' Configure ClawdNode app to use port 9878.'); + log('================================================'); +}); + +// Export for potential module use +module.exports = { sendCommand, broadcast, onNotification, onCall }; diff --git a/watch-calls.sh b/watch-calls.sh new file mode 100755 index 0000000..5786bd1 --- /dev/null +++ b/watch-calls.sh @@ -0,0 +1,10 @@ +#!/bin/bash +LAST_CALL="" +while true; do + CALL=$(grep -o "INCOMING.*CallID.*" /tmp/clawdnode-gateway.log 2>/dev/null | tail -1) + if [ -n "$CALL" ] && [ "$CALL" != "$LAST_CALL" ]; then + echo "🚨 $CALL" + LAST_CALL="$CALL" + fi + sleep 1 +done