File size: 9,176 Bytes
dd7ada8
 
 
 
 
 
 
 
 
d439036
 
d43f799
 
 
 
 
 
dd7ada8
 
73b4eda
 
dd7ada8
de4ed0d
64857cd
dd7ada8
d439036
9aba9c1
d439036
 
 
 
 
 
de4ed0d
dd7ada8
 
 
d439036
9aba9c1
ad768a0
2ab78d1
 
 
 
 
 
 
 
dd7ada8
d439036
 
 
 
 
 
 
 
 
 
de4ed0d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
dd7ada8
 
9f04df0
dd7ada8
 
 
d43f799
 
dd7ada8
 
 
 
 
 
 
ba7c710
d5203bf
9f04df0
 
 
 
 
 
 
dd7ada8
9f04df0
6ca12eb
9f04df0
dd7ada8
 
 
 
2ab78d1
 
 
 
 
 
 
dd7ada8
 
 
 
 
 
 
 
 
 
 
73b4eda
 
dd7ada8
 
 
d43f799
 
dd7ada8
 
d439036
 
 
 
dd7ada8
 
 
 
f72a7b7
 
 
 
 
 
 
73b4eda
dd7ada8
 
 
 
ad768a0
dd7ada8
9aba9c1
dd7ada8
 
 
 
9aba9c1
 
 
 
 
 
 
de4ed0d
9aba9c1
 
 
 
 
 
de4ed0d
 
 
9aba9c1
 
 
dd7ada8
de4ed0d
dd7ada8
d43f799
dd7ada8
 
 
 
73b4eda
dd7ada8
d439036
 
 
 
 
 
dd7ada8
d439036
dd7ada8
9aba9c1
de4ed0d
d439036
73b4eda
 
 
 
d439036
 
 
d43f799
d439036
 
73b4eda
 
 
 
 
 
 
 
 
 
dd7ada8
ad768a0
d439036
 
dd7ada8
 
 
a0b9abe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
de4ed0d
 
 
9f04df0
dd7ada8
 
 
 
 
 
de4ed0d
 
 
 
 
f72a7b7
 
 
 
 
 
 
de4ed0d
d43f799
dd7ada8
9f04df0
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
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
/**
 * 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");
let WebSocket;
try {
  ({ WebSocket } = require('ws'));
} catch (_) {
  ({ WebSocket } = require('/home/node/.openclaw/openclaw-app/node_modules/ws'));
}
const { randomUUID } = require('node:crypto');

const GATEWAY_PORT = Number.parseInt(process.env.GATEWAY_PORT || "7860", 10);
const GATEWAY_URL = `ws://127.0.0.1:${GATEWAY_PORT}`;
const GATEWAY_TOKEN = process.env.GATEWAY_TOKEN || "huggingclaw";
const WHATSAPP_ENABLED = /^true$/i.test(process.env.WHATSAPP_ENABLED || "");
const CHECK_INTERVAL = 30000;
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) => {
      let msg;
      try { msg = JSON.parse(data.toString()); } catch { return; }

      if (msg.type === "event" && msg.event === "connect.challenge") {
        ws.send(JSON.stringify({
          type: "req",
          id: randomUUID(),
          method: "connect",
          params: {
            minProtocol: 3,
            maxProtocol: 4,
            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, timeoutMs) {
  const ms = timeoutMs !== undefined ? timeoutMs : 10000; // default 10s for normal calls
  return new Promise((resolve, reject) => {
    const id = randomUUID();
    const handler = (data) => {
      let msg;
      try { msg = JSON.parse(data.toString()); } catch { return; }
      if (msg.id === id) {
        ws.removeListener("message", handler);
        if (msg.ok === false) {
          reject(new Error(extractErrorMessage(msg)));
          return;
        }
        resolve(msg);
      }
    };
    ws.on("message", handler);
    try {
      ws.send(JSON.stringify({ type: "req", id, method, params }));
    } catch (sendErr) {
      ws.removeListener("message", handler);
      reject(sendErr);
      return;
    }
    setTimeout(() => { ws.removeListener("message", handler); reject(new Error("RPC Timeout")); }, ms);
  });
}

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 }, WAIT_TIMEOUT + 5000);
    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 });

      // Set shouldStop BEFORE config.apply — gateway restart during apply must not
      // leave guardian running (it would then incorrectly wipe valid credentials).
      shouldStop = true;

      if (linkedAfter515) {
        console.log("[guardian] 515 after scan: credentials saved, reloading config to start WhatsApp...");
      } else {
        console.log("[guardian] Pairing completed! Reloading config...");
      }

      try {
        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.");
        }
      } catch (applyErr) {
        // Gateway restarted during config.apply — that is expected and fine.
        // shouldStop is already true so we will not retry or attempt logout.
        console.log(`[guardian] Config re-apply interrupted (gateway restarting): ${applyErr.message}`);
      }
      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);
}

process.on("unhandledRejection", (reason) => {
  const msg = reason && reason.message ? reason.message : String(reason);
  if (!/RPC Timeout|Timeout/i.test(msg)) {
    console.log(`[guardian] Unhandled rejection: ${msg}`);
  }
});

writeStatus({ configured: true, connected: false, pairing: false });
console.log("[guardian] WhatsApp Guardian active. Monitoring pairing status...");
setInterval(checkStatus, CHECK_INTERVAL);
setTimeout(checkStatus, 15000);