File size: 8,011 Bytes
51ec4bc
 
 
 
 
 
 
 
 
969345a
 
bcbf1ad
 
51ec4bc
529ca98
51ec4bc
bcbf1ad
51ec4bc
 
969345a
e0ab924
969345a
 
 
 
 
 
bcbf1ad
51ec4bc
 
 
969345a
e0ab924
6a8bffa
544bf0f
 
 
 
bcbf1ad
544bf0f
 
 
51ec4bc
969345a
 
 
 
bcbf1ad
969345a
bcbf1ad
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
969345a
 
 
51ec4bc
 
529ca98
51ec4bc
 
 
 
 
 
bcbf1ad
 
 
 
 
 
 
 
 
 
 
 
529ca98
bcbf1ad
 
 
 
 
 
51ec4bc
 
 
544bf0f
 
 
 
 
 
 
51ec4bc
 
 
 
 
 
bcbf1ad
 
51ec4bc
 
 
 
 
 
 
 
 
 
969345a
 
 
 
51ec4bc
 
 
 
 
bcbf1ad
51ec4bc
 
 
 
6a8bffa
51ec4bc
bcbf1ad
51ec4bc
 
 
 
e0ab924
 
 
 
 
 
 
bcbf1ad
e0ab924
 
 
 
 
 
bcbf1ad
 
 
e0ab924
 
 
51ec4bc
bcbf1ad
51ec4bc
bcbf1ad
51ec4bc
 
 
 
bcbf1ad
51ec4bc
969345a
 
 
 
 
 
51ec4bc
969345a
51ec4bc
e0ab924
bcbf1ad
969345a
 
bcbf1ad
969345a
 
 
 
51ec4bc
969345a
bcbf1ad
51ec4bc
 
6a8bffa
 
 
bcbf1ad
969345a
51ec4bc
bcbf1ad
51ec4bc
43f221d
 
 
 
 
bcbf1ad
43f221d
 
 
 
 
 
 
 
bcbf1ad
43f221d
 
bcbf1ad
 
 
529ca98
51ec4bc
 
 
 
 
 
bcbf1ad
 
 
 
 
 
 
51ec4bc
529ca98
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
/**
 * 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);