HuggingClaw / wa-guardian.js
somratpro's picture
feat: add optional WhatsApp integration with toggle support and status tracking
bcbf1ad
raw
history blame
8.01 kB
/**
* HuggingClaw WhatsApp Guardian
*
* Automates the WhatsApp pairing process on HuggingFace Spaces.
* Handles the "515 Restart" by monitoring the channel status and
* re-applying the configuration after a successful scan.
*/
"use strict";
const fs = require("fs");
const path = require("path");
const { WebSocket } = require('/home/node/.openclaw/openclaw-app/node_modules/ws');
const { randomUUID } = require('node:crypto');
const GATEWAY_URL = "ws://127.0.0.1:7860";
const GATEWAY_TOKEN = process.env.GATEWAY_TOKEN || "huggingclaw";
const WHATSAPP_ENABLED = /^true$/i.test(process.env.WHATSAPP_ENABLED || "");
const CHECK_INTERVAL = 5000;
const WAIT_TIMEOUT = 120000;
const POST_515_NO_LOGOUT_MS = 90 * 1000;
const SUCCESS_COOLDOWN_MS = 60 * 1000;
const RESET_MARKER_PATH = path.join(
process.env.HOME || "/home/node",
".openclaw",
"workspace",
".reset_credentials",
);
const STATUS_FILE_PATH = "/tmp/huggingclaw-wa-status.json";
let isWaiting = false;
let hasShownWaitMessage = false;
let last515At = 0;
let lastConnectedAt = 0;
let shouldStop = false;
function extractErrorMessage(msg) {
if (!msg || typeof msg !== "object") return "Unknown error";
if (typeof msg.error === "string") return msg.error;
if (msg.error && typeof msg.error.message === "string") return msg.error.message;
if (typeof msg.message === "string") return msg.message;
return "Unknown error";
}
function writeResetMarker() {
try {
fs.mkdirSync(path.dirname(RESET_MARKER_PATH), { recursive: true });
fs.writeFileSync(RESET_MARKER_PATH, "reset\n");
console.log(`[guardian] Created backup reset marker at ${RESET_MARKER_PATH}`);
} catch (error) {
console.log(`[guardian] Failed to write backup reset marker: ${error.message}`);
}
}
function writeStatus(partial) {
try {
const current = fs.existsSync(STATUS_FILE_PATH)
? JSON.parse(fs.readFileSync(STATUS_FILE_PATH, "utf8"))
: {};
const next = {
configured: true,
connected: false,
pairing: false,
updatedAt: new Date().toISOString(),
...current,
...partial,
};
fs.writeFileSync(STATUS_FILE_PATH, JSON.stringify(next));
} catch (error) {
console.log(`[guardian] Failed to write status file: ${error.message}`);
}
}
async function createConnection() {
return new Promise((resolve, reject) => {
const ws = new WebSocket(GATEWAY_URL);
let resolved = false;
ws.on("message", (data) => {
const msg = JSON.parse(data.toString());
if (msg.type === "event" && msg.event === "connect.challenge") {
ws.send(JSON.stringify({
type: "req",
id: randomUUID(),
method: "connect",
params: {
minProtocol: 3,
maxProtocol: 3,
client: {
id: "gateway-client",
version: "1.0.0",
platform: "linux",
mode: "backend",
},
caps: [],
auth: { token: GATEWAY_TOKEN },
role: "operator",
scopes: ["operator.read", "operator.write", "operator.admin", "operator.pairing"],
},
}));
return;
}
if (!resolved && msg.type === "res" && msg.ok === false) {
resolved = true;
ws.close();
reject(new Error(extractErrorMessage(msg)));
return;
}
if (!resolved && msg.type === "res" && msg.ok) {
resolved = true;
resolve(ws);
}
});
ws.on("error", (e) => { if (!resolved) reject(e); });
setTimeout(() => { if (!resolved) { ws.close(); reject(new Error("Timeout")); } }, 10000);
});
}
async function callRpc(ws, method, params) {
return new Promise((resolve, reject) => {
const id = randomUUID();
const handler = (data) => {
const msg = JSON.parse(data.toString());
if (msg.id === id) {
ws.removeListener("message", handler);
if (msg.ok === false) {
reject(new Error(extractErrorMessage(msg)));
return;
}
resolve(msg);
}
};
ws.on("message", handler);
ws.send(JSON.stringify({ type: "req", id, method, params }));
setTimeout(() => { ws.removeListener("message", handler); reject(new Error("RPC Timeout")); }, WAIT_TIMEOUT + 5000);
});
}
async function checkStatus() {
if (shouldStop) return;
if (isWaiting) return;
if (lastConnectedAt && Date.now() - lastConnectedAt < SUCCESS_COOLDOWN_MS) return;
let ws;
try {
ws = await createConnection();
const statusRes = await callRpc(ws, "channels.status", {});
const channels = (statusRes.payload || statusRes.result)?.channels || {};
const wa = channels.whatsapp;
if (!wa) {
hasShownWaitMessage = false;
writeStatus({ configured: true, connected: false, pairing: false });
return;
}
if (wa.connected) {
hasShownWaitMessage = false;
lastConnectedAt = Date.now();
writeStatus({ configured: true, connected: true, pairing: false });
shouldStop = true;
setTimeout(() => process.exit(0), 1000);
return;
}
isWaiting = true;
writeStatus({ configured: true, connected: false, pairing: true });
if (!hasShownWaitMessage) {
console.log("\n[guardian] 📱 WhatsApp pairing in progress. Please scan the QR code in the Control UI.");
hasShownWaitMessage = true;
}
console.log("[guardian] Waiting for pairing completion...");
const waitRes = await callRpc(ws, "web.login.wait", { timeoutMs: WAIT_TIMEOUT });
const result = waitRes.payload || waitRes.result;
const message = result?.message || "";
const linkedAfter515 = !result?.connected && message.includes("515");
if (linkedAfter515) {
last515At = Date.now();
}
if (result && (result.connected || linkedAfter515)) {
hasShownWaitMessage = false;
lastConnectedAt = Date.now();
writeStatus({ configured: true, connected: true, pairing: false });
if (linkedAfter515) {
console.log("[guardian] 515 after scan: credentials saved, reloading config to start WhatsApp...");
} else {
console.log("[guardian] ✅ Pairing completed! Reloading config...");
}
const getRes = await callRpc(ws, "config.get", {});
if (getRes.payload?.raw && getRes.payload?.hash) {
await callRpc(ws, "config.apply", { raw: getRes.payload.raw, baseHash: getRes.payload.hash });
console.log("[guardian] Configuration re-applied.");
}
shouldStop = true;
setTimeout(() => process.exit(0), 1000);
} else if (!message.includes("No active") && !message.includes("Still waiting")) {
console.log(`[guardian] Wait result: ${message}`);
}
} catch (e) {
const message = e && e.message ? e.message : "";
if (
/401|unauthorized|logged out|440|conflict/i.test(message) &&
Date.now() - last515At >= POST_515_NO_LOGOUT_MS
) {
console.log("[guardian] Clearing invalid WhatsApp session so a fresh QR can be used...");
try {
if (ws) {
await callRpc(ws, "channels.logout", { channel: "whatsapp" });
writeResetMarker();
hasShownWaitMessage = false;
console.log("[guardian] Logged out invalid WhatsApp session.");
}
} catch (error) {
console.log(`[guardian] Failed to log out invalid session: ${error.message}`);
}
}
if (!/RPC Timeout/i.test(message)) {
writeStatus({ configured: true, connected: false, pairing: false });
}
// Normal timeout or gateway starting up; retry on the next interval.
} finally {
isWaiting = false;
if (ws) ws.close();
}
}
if (!WHATSAPP_ENABLED) {
writeStatus({ configured: false, connected: false, pairing: false });
process.exit(0);
}
writeStatus({ configured: true, connected: false, pairing: false });
console.log("[guardian] ⚔️ WhatsApp Guardian active. Monitoring pairing status...");
setInterval(checkStatus, CHECK_INTERVAL);
setTimeout(checkStatus, 15000);