Initial commit
This commit is contained in:
commit
60a6d973f5
|
|
@ -0,0 +1,5 @@
|
||||||
|
node_modules/
|
||||||
|
.env
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 };
|
||||||
|
|
@ -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
|
||||||
Loading…
Reference in New Issue