try / scripts /wa-login-guardian.cjs
proti0070's picture
Upload folder using huggingface_hub
d75ac2b verified
/**
* WhatsApp Login Guardian β€” background helper for HF Spaces.
*
* Problem: After QR scan, WhatsApp sends 515 (restart required). The
* web.login.wait RPC handles this restart, but HF Spaces' proxy drops
* WebSocket connections, so the UI's web.login.wait may not be active.
*
* Solution: This script connects to the local gateway and keeps calling
* web.login.wait with long timeouts, ensuring the 515 restart is handled.
*
* Usage: Run as background process from entrypoint.sh
*/
"use strict";
const { WebSocket } = require("ws");
const { randomUUID } = require("node:crypto");
const { exec } = require('child_process');
const GATEWAY_URL = "ws://127.0.0.1:7860";
const TOKEN = "openclaw-space-default";
const CHECK_INTERVAL = 5000; // Check every 5s so we catch QR scan quickly
const WAIT_TIMEOUT = 120000; // 2 minute wait timeout
const POST_515_NO_LOGOUT_MS = 90000; // After 515, don't clear "401" for 90s (avoid wiping just-saved creds)
let isWaiting = false;
let last515At = 0;
let hasShownWaitMessage = false;
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: TOKEN },
role: "operator",
scopes: ["operator.admin"],
},
})
);
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("Connection 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);
resolve(msg);
}
};
ws.on("message", handler);
ws.send(JSON.stringify({ type: "req", id, method, params }));
// Long timeout for web.login.wait
setTimeout(() => {
ws.removeListener("message", handler);
reject(new Error("RPC timeout"));
}, WAIT_TIMEOUT + 5000);
});
}
async function checkAndWait() {
if (isWaiting) return;
let ws;
try {
ws = await createConnection();
} catch {
return; // Gateway not ready yet
}
try {
// Check channel status to see if WhatsApp needs attention
const statusRes = await callRpc(ws, "channels.status", {});
const channels = (statusRes.payload || statusRes.result)?.channels || {};
const wa = channels.whatsapp;
if (!wa) {
ws.close();
return;
}
// If linked but got 401/logged out OR 440/conflict, clear invalid credentials so user can get a fresh QR β€”
// but NOT within POST_515_NO_LOGOUT_MS of a 515 (channel may still report 401 and we'd wipe just-saved creds).
const err = (wa.lastError || "").toLowerCase();
const recently515 = Date.now() - last515At < POST_515_NO_LOGOUT_MS;
const needsLogout = wa.linked && !wa.connected && !recently515 &&
(err.includes("401") || err.includes("unauthorized") || err.includes("logged out") || err.includes("440") || err.includes("conflict"));
if (needsLogout) {
console.log("[wa-guardian] Clearing invalid session (401/440/conflict) so a fresh QR can be used...");
try {
await callRpc(ws, "channels.logout", { channel: "whatsapp" });
console.log("[wa-guardian] Logged out; user can click Login again for a new QR.");
// Signal sync_hf.py to delete remote credentials
const fs = require('fs');
const path = require('path');
// Workspace is usually /home/node/.openclaw/workspace
const markerPath = path.join(process.env.HOME || '/home/node', '.openclaw/workspace/.reset_credentials');
fs.writeFileSync(markerPath, 'reset');
console.log("[wa-guardian] Created .reset_credentials marker for sync script.");
} catch (e) {
console.log("[wa-guardian] channels.logout failed:", e.message);
}
ws.close();
return;
}
// If WhatsApp is already connected, nothing to do
if (wa.connected) {
ws.close();
return;
}
// Try web.login.wait β€” this will handle 515 restart if QR was scanned
isWaiting = true;
if (!hasShownWaitMessage) {
console.log("⏳ Waiting for WhatsApp QR code scan...");
console.log("πŸ“± Please scan the QR code with your phone to continue.");
hasShownWaitMessage = true;
}
console.log("[wa-guardian] Calling web.login.wait...");
const waitRes = await callRpc(ws, "web.login.wait", {
timeoutMs: WAIT_TIMEOUT,
});
const result = waitRes.payload || waitRes.result;
const msg = result?.message || "";
const linkedAfter515 = !result?.connected && msg.includes("515");
if (linkedAfter515) last515At = Date.now();
if (result?.connected || linkedAfter515) {
hasShownWaitMessage = false; // Reset for next time
if (linkedAfter515) {
console.log("[wa-guardian] 515 after scan β€” credentials saved; triggering config reload to start channel...");
} else {
console.log("[wa-guardian] WhatsApp connected successfully! Triggering config reload to start channel...");
}
console.log("βœ… QR code scanned successfully. Login completed.");
// Persistence handled by sync_hf.py background loop
try {
const getRes = await callRpc(ws, "config.get", {});
const raw = getRes.payload?.raw;
const hash = getRes.payload?.hash;
if (raw && hash) {
await callRpc(ws, "config.apply", { raw, baseHash: hash });
console.log("[wa-guardian] Config applied; gateway will restart with WhatsApp channel.");
}
} catch (e) {
console.log("[wa-guardian] Config apply failed:", e.message);
}
} else {
if (!msg.includes("No active") && !msg.includes("Still waiting")) {
console.log("[wa-guardian] Wait result:", msg);
}
}
} catch (e) {
// Timeout or error β€” normal, just retry
} finally {
isWaiting = false;
try {
ws.close();
} catch {}
}
}
// Start checking periodically
console.log("[wa-guardian] WhatsApp login guardian started");
setInterval(checkAndWait, CHECK_INTERVAL);
// Initial check after 15s (give gateway time to start)
setTimeout(checkAndWait, 15000);