somratpro commited on
Commit
dd7ada8
·
1 Parent(s): 7a0af95

feat: implement wa-guardian for automated WhatsApp pairing and add Control UI shortcut to dashboard

Browse files
Files changed (5) hide show
  1. .env.example +2 -6
  2. README.md +3 -12
  3. health-server.js +66 -29
  4. start.sh +11 -10
  5. wa-guardian.js +126 -0
.env.example CHANGED
@@ -142,9 +142,6 @@ TELEGRAM_USER_ID=123456789
142
  # Multiple user IDs (comma-separated for team access)
143
  # TELEGRAM_USER_IDS=123456789,987654321,555555555
144
 
145
- # WhatsApp integration (pairing mode via CLI)
146
- # WHATSAPP_ENABLED=true
147
-
148
  # ── OPTIONAL: Workspace Backup to HF Dataset ──
149
  HF_USERNAME=your_hf_username
150
  HF_TOKEN=hf_your_token_here
@@ -164,9 +161,8 @@ KEEP_ALIVE_INTERVAL=300
164
  # Workspace auto-sync interval (seconds). Default: 600.
165
  SYNC_INTERVAL=600
166
 
167
- # ── OPTIONAL: Webhooks ──
168
- # URL to send HTTP POST requests on startup and backup errors
169
- # WEBHOOK_URL=https://your-webhook-endpoint.com/webhook
170
 
171
  # ── OPTIONAL: Advanced ──
172
  # Pin OpenClaw version. Default: latest
 
142
  # Multiple user IDs (comma-separated for team access)
143
  # TELEGRAM_USER_IDS=123456789,987654321,555555555
144
 
 
 
 
145
  # ── OPTIONAL: Workspace Backup to HF Dataset ──
146
  HF_USERNAME=your_hf_username
147
  HF_TOKEN=hf_your_token_here
 
161
  # Workspace auto-sync interval (seconds). Default: 600.
162
  SYNC_INTERVAL=600
163
 
164
+ # Webhooks: Standard POST notifications for lifecycle events
165
+ # WEBHOOK_URL=https://your-webhook-endpoint.com/log
 
166
 
167
  # ── OPTIONAL: Advanced ──
168
  # Pin OpenClaw version. Default: latest
README.md CHANGED
@@ -95,18 +95,9 @@ After restarting, the bot should appear online on Telegram.
95
 
96
  To use WhatsApp:
97
 
98
- 1. Add the secret `WHATSAPP_ENABLED` and set it to `true`.
99
- 2. Once the Space restarts, use the OpenClaw CLI to link your account:
100
-
101
- ```bash
102
- npm install -g openclaw@latest
103
- openclaw channels login --gateway https://YOUR_SPACE_URL.hf.space --channel whatsapp
104
- ```
105
-
106
- 3. Scan the QR code with your phone (WhatsApp → Linked Devices).
107
-
108
- > [!NOTE]
109
- > HuggingClaw uses dynamic DNS-over-HTTPS probing to ensure reliability on HuggingFace Spaces.
110
 
111
  ## 💾 Workspace Backup *(Optional)*
112
 
 
95
 
96
  To use WhatsApp:
97
 
98
+ 1. Visit your Space's Dashboard (Port 7861) and click **🚀 Open Control UI**.
99
+ 2. In the Control UI, go to **Channels** **WhatsApp** **Login**.
100
+ 3. Scan the QR code with your phone. 📱
 
 
 
 
 
 
 
 
 
101
 
102
  ## 💾 Workspace Backup *(Optional)*
103
 
health-server.js CHANGED
@@ -1,50 +1,67 @@
1
  // Lightweight health server and dashboard on port 7861
2
- const http = require('http');
3
- const fs = require('fs');
4
- const path = require('path');
5
 
6
  const PORT = process.env.HEALTH_PORT || 7861;
7
  const startTime = Date.now();
8
- const LLM_MODEL = process.env.LLM_MODEL || 'Not Set';
9
- const WHATSAPP_ENABLED = process.env.WHATSAPP_ENABLED === 'true' || process.env.WHATSAPP_ENABLED === '1';
 
10
  const TELEGRAM_ENABLED = !!process.env.TELEGRAM_BOT_TOKEN;
11
 
12
  const server = http.createServer((req, res) => {
13
  const uptime = Math.floor((Date.now() - startTime) / 1000);
14
  const uptimeHuman = `${Math.floor(uptime / 3600)}h ${Math.floor((uptime % 3600) / 60)}m`;
15
 
16
- if (req.url === '/health') {
17
- res.writeHead(200, { 'Content-Type': 'application/json' });
18
- res.end(JSON.stringify({
19
- status: 'ok',
20
- uptime: uptime,
21
- uptimeHuman: uptimeHuman,
22
- timestamp: new Date().toISOString()
23
- }));
 
 
24
  return;
25
  }
26
 
27
- if (req.url === '/status') {
28
- let syncStatus = { status: 'unknown', message: 'No sync data yet' };
29
  try {
30
- if (fs.existsSync('/tmp/sync-status.json')) {
31
- syncStatus = JSON.parse(fs.readFileSync('/tmp/sync-status.json', 'utf8'));
 
 
32
  }
33
  } catch (e) {}
34
-
35
- res.writeHead(200, { 'Content-Type': 'application/json' });
36
- res.end(JSON.stringify({
37
- model: LLM_MODEL,
38
- whatsapp: WHATSAPP_ENABLED,
39
- telegram: TELEGRAM_ENABLED,
40
- sync: syncStatus,
41
- uptime: uptimeHuman
42
- }));
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  return;
44
  }
45
 
46
- if (req.url === '/') {
47
- res.writeHead(200, { 'Content-Type': 'text/html' });
48
  res.end(`
49
  <!DOCTYPE html>
50
  <html lang="en">
@@ -153,6 +170,25 @@ const server = http.createServer((req, res) => {
153
  word-break: break-all;
154
  }
155
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
  .status-badge {
157
  display: inline-flex;
158
  align-items: center;
@@ -226,6 +262,7 @@ const server = http.createServer((req, res) => {
226
  <span class="stat-label">Telegram</span>
227
  <span id="tg-status">Loading...</span>
228
  </div>
 
229
  </div>
230
 
231
  <div class="stat-card" style="width: 100%;">
@@ -297,6 +334,6 @@ const server = http.createServer((req, res) => {
297
  res.end();
298
  });
299
 
300
- server.listen(PORT, '0.0.0.0', () => {
301
  console.log(`🏥 Health server & Dashboard listening on port ${PORT}`);
302
  });
 
1
  // Lightweight health server and dashboard on port 7861
2
+ const http = require("http");
3
+ const fs = require("fs");
 
4
 
5
  const PORT = process.env.HEALTH_PORT || 7861;
6
  const startTime = Date.now();
7
+ const LLM_MODEL = process.env.LLM_MODEL || "Not Set";
8
+ const GATEWAY_TOKEN = process.env.GATEWAY_TOKEN || "huggingclaw";
9
+ const SPACE_HOST = process.env.SPACE_HOST || "localhost:7860";
10
  const TELEGRAM_ENABLED = !!process.env.TELEGRAM_BOT_TOKEN;
11
 
12
  const server = http.createServer((req, res) => {
13
  const uptime = Math.floor((Date.now() - startTime) / 1000);
14
  const uptimeHuman = `${Math.floor(uptime / 3600)}h ${Math.floor((uptime % 3600) / 60)}m`;
15
 
16
+ if (req.url === "/health") {
17
+ res.writeHead(200, { "Content-Type": "application/json" });
18
+ res.end(
19
+ JSON.stringify({
20
+ status: "ok",
21
+ uptime: uptime,
22
+ uptimeHuman: uptimeHuman,
23
+ timestamp: new Date().toISOString(),
24
+ }),
25
+ );
26
  return;
27
  }
28
 
29
+ if (req.url === "/status") {
30
+ let syncStatus = { status: "unknown", message: "No sync data yet" };
31
  try {
32
+ if (fs.existsSync("/tmp/sync-status.json")) {
33
+ syncStatus = JSON.parse(
34
+ fs.readFileSync("/tmp/sync-status.json", "utf8"),
35
+ );
36
  }
37
  } catch (e) {}
38
+
39
+ res.writeHead(200, { "Content-Type": "application/json" });
40
+ res.end(
41
+ JSON.stringify({
42
+ model: LLM_MODEL,
43
+ whatsapp: true,
44
+ telegram: TELEGRAM_ENABLED,
45
+ sync: syncStatus,
46
+ uptime: uptimeHuman,
47
+ token: GATEWAY_TOKEN,
48
+ }),
49
+ );
50
+ return;
51
+ }
52
+
53
+ // Auto-login redirect to OpenClaw Control UI
54
+ if (req.url === "/login") {
55
+ const protocol = req.headers["x-forwarded-proto"] || "http";
56
+ const host = req.headers["host"] || SPACE_HOST;
57
+ const target = `${protocol}://${host}/?token=${GATEWAY_TOKEN}`;
58
+ res.writeHead(302, { Location: target });
59
+ res.end();
60
  return;
61
  }
62
 
63
+ if (req.url === "/") {
64
+ res.writeHead(200, { "Content-Type": "text/html" });
65
  res.end(`
66
  <!DOCTYPE html>
67
  <html lang="en">
 
170
  word-break: break-all;
171
  }
172
 
173
+ .stat-btn {
174
+ grid-column: span 2;
175
+ background: var(--accent);
176
+ color: #fff;
177
+ padding: 16px;
178
+ border-radius: 16px;
179
+ text-align: center;
180
+ text-decoration: none;
181
+ font-weight: 600;
182
+ margin-top: 10px;
183
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
184
+ box-shadow: 0 10px 20px -5px rgba(59, 130, 246, 0.4);
185
+ }
186
+
187
+ .stat-btn:hover {
188
+ transform: scale(1.02);
189
+ box-shadow: 0 15px 30px -5px rgba(59, 130, 246, 0.6);
190
+ }
191
+
192
  .status-badge {
193
  display: inline-flex;
194
  align-items: center;
 
262
  <span class="stat-label">Telegram</span>
263
  <span id="tg-status">Loading...</span>
264
  </div>
265
+ <a href="/login" class="stat-btn">🚀 Open Control UI</a>
266
  </div>
267
 
268
  <div class="stat-card" style="width: 100%;">
 
334
  res.end();
335
  });
336
 
337
+ server.listen(PORT, "0.0.0.0", () => {
338
  console.log(`🏥 Health server & Dashboard listening on port ${PORT}`);
339
  });
start.sh CHANGED
@@ -235,10 +235,8 @@ if [ -n "$TELEGRAM_BOT_TOKEN" ]; then
235
  fi
236
 
237
  # WhatsApp
238
- if [ "$WHATSAPP_ENABLED" = "true" ] || [ "$WHATSAPP_ENABLED" = "1" ]; then
239
- CONFIG_JSON=$(echo "$CONFIG_JSON" | jq '.plugins.entries.whatsapp = {"enabled": true}')
240
- CONFIG_JSON=$(echo "$CONFIG_JSON" | jq '.channels.whatsapp = {"dmPolicy": "pairing"}')
241
- fi
242
 
243
  # Write config
244
  echo "$CONFIG_JSON" > "/home/node/.openclaw/openclaw.json"
@@ -255,11 +253,7 @@ printf " │ %-40s │\n" "Telegram: ✅ enabled"
255
  else
256
  printf " │ %-40s │\n" "Telegram: ❌ not configured"
257
  fi
258
- if [ "$WHATSAPP_ENABLED" = "true" ] || [ "$WHATSAPP_ENABLED" = "1" ]; then
259
  printf " │ %-40s │\n" "WhatsApp: ✅ enabled"
260
- else
261
- printf " │ %-40s │\n" "WhatsApp: ❌ not configured"
262
- fi
263
  if [ -n "$HF_USERNAME" ] && [ -n "$HF_TOKEN" ]; then
264
  printf " │ %-40s │\n" "Backup: ✅ ${HF_USERNAME}/${BACKUP_DATASET:-huggingclaw-backup}"
265
  else
@@ -323,10 +317,17 @@ trap graceful_shutdown SIGTERM SIGINT
323
 
324
  # ── Start background services ──
325
  export LLM_MODEL="$LLM_MODEL"
 
326
  node /home/node/app/health-server.js &
327
- /home/node/app/keep-alive.sh &
 
 
 
 
 
328
 
329
- python3 /home/node/app/workspace-sync.py &
 
330
 
331
  # ── Launch gateway ──
332
  echo "🚀 Launching OpenClaw gateway on port 7860..."
 
235
  fi
236
 
237
  # WhatsApp
238
+ CONFIG_JSON=$(echo "$CONFIG_JSON" | jq '.plugins.entries.whatsapp = {"enabled": true}')
239
+ CONFIG_JSON=$(echo "$CONFIG_JSON" | jq '.channels.whatsapp = {"dmPolicy": "pairing"}')
 
 
240
 
241
  # Write config
242
  echo "$CONFIG_JSON" > "/home/node/.openclaw/openclaw.json"
 
253
  else
254
  printf " │ %-40s │\n" "Telegram: ❌ not configured"
255
  fi
 
256
  printf " │ %-40s │\n" "WhatsApp: ✅ enabled"
 
 
 
257
  if [ -n "$HF_USERNAME" ] && [ -n "$HF_TOKEN" ]; then
258
  printf " │ %-40s │\n" "Backup: ✅ ${HF_USERNAME}/${BACKUP_DATASET:-huggingclaw-backup}"
259
  else
 
317
 
318
  # ── Start background services ──
319
  export LLM_MODEL="$LLM_MODEL"
320
+ # 10. Start Health Server & Dashboard
321
  node /home/node/app/health-server.js &
322
+ HEALTH_PID=$!
323
+
324
+ # 11. Start WhatsApp Guardian (Automates pairing)
325
+ node /home/node/app/wa-guardian.js &
326
+ GUARDIAN_PID=$!
327
+ echo "🛡️ WhatsApp Guardian started (PID: $GUARDIAN_PID)"
328
 
329
+ # 12. Start Workspace Sync
330
+ python3 -u /home/node/app/workspace-sync.py &
331
 
332
  # ── Launch gateway ──
333
  echo "🚀 Launching OpenClaw gateway on port 7860..."
wa-guardian.js ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * HuggingClaw WhatsApp Guardian
3
+ *
4
+ * Automates the WhatsApp pairing process on HuggingFace Spaces.
5
+ * Handles the "515 Restart" by monitoring the channel status and
6
+ * re-applying the configuration after a successful scan.
7
+ */
8
+ "use strict";
9
+
10
+ const { WebSocket } = require('/home/node/.openclaw/openclaw-app/node_modules/ws');
11
+ const { randomUUID } = require('node:crypto');
12
+
13
+ const GATEWAY_URL = "ws://127.0.0.1:7860";
14
+ const GATEWAY_TOKEN = process.env.GATEWAY_TOKEN || "huggingclaw";
15
+ const CHECK_INTERVAL = 5000;
16
+ const WAIT_TIMEOUT = 120000;
17
+
18
+ let isWaiting = false;
19
+ let hasShownWaitMessage = false;
20
+
21
+ async function createConnection() {
22
+ return new Promise((resolve, reject) => {
23
+ const ws = new WebSocket(GATEWAY_URL);
24
+ let resolved = false;
25
+
26
+ ws.on("message", (data) => {
27
+ const msg = JSON.parse(data.toString());
28
+
29
+ if (msg.type === "event" && msg.event === "connect.challenge") {
30
+ ws.send(JSON.stringify({
31
+ type: "req",
32
+ id: randomUUID(),
33
+ method: "connect",
34
+ params: {
35
+ auth: { token: GATEWAY_TOKEN },
36
+ client: { id: "wa-guardian", platform: "linux", mode: "backend", version: "1.0.0" },
37
+ scopes: ["operator.admin", "operator.pairing", "operator.read", "operator.write"]
38
+ }
39
+ }));
40
+ return;
41
+ }
42
+
43
+ if (!resolved && msg.type === "res" && msg.ok) {
44
+ resolved = true;
45
+ resolve(ws);
46
+ }
47
+ });
48
+
49
+ ws.on("error", (e) => { if (!resolved) reject(e); });
50
+ setTimeout(() => { if (!resolved) { ws.close(); reject(new Error("Timeout")); } }, 10000);
51
+ });
52
+ }
53
+
54
+ async function callRpc(ws, method, params) {
55
+ return new Promise((resolve, reject) => {
56
+ const id = randomUUID();
57
+ const handler = (data) => {
58
+ const msg = JSON.parse(data.toString());
59
+ if (msg.id === id) {
60
+ ws.removeListener("message", handler);
61
+ resolve(msg);
62
+ }
63
+ };
64
+ ws.on("message", handler);
65
+ ws.send(JSON.stringify({ type: "req", id, method, params }));
66
+ setTimeout(() => { ws.removeListener("message", handler); reject(new Error("RPC Timeout")); }, WAIT_TIMEOUT + 5000);
67
+ });
68
+ }
69
+
70
+ async function checkStatus() {
71
+ if (isWaiting) return;
72
+
73
+ let ws;
74
+ try {
75
+ ws = await createConnection();
76
+
77
+ // Check if WhatsApp channel exists and its status
78
+ const statusRes = await callRpc(ws, "channels.status", {});
79
+ const channels = (statusRes.payload || statusRes.result)?.channels || {};
80
+ const wa = channels.whatsapp;
81
+
82
+ if (!wa) {
83
+ ws.close();
84
+ return;
85
+ }
86
+
87
+ // If connected, we are good
88
+ if (wa.connected) {
89
+ ws.close();
90
+ return;
91
+ }
92
+
93
+ // If "Ready to pair", we wait for the scan
94
+ isWaiting = true;
95
+ if (!hasShownWaitMessage) {
96
+ console.log("\n[guardian] 📱 WhatsApp pairing in progress. Please scan the QR code in the Control UI.");
97
+ hasShownWaitMessage = true;
98
+ }
99
+
100
+ console.log("[guardian] Waiting for pairing completion...");
101
+ const waitRes = await callRpc(ws, "web.login.wait", { timeoutMs: WAIT_TIMEOUT });
102
+ const result = waitRes.payload || waitRes.result;
103
+
104
+ if (result && (result.connected || (result.message && result.message.includes("515")))) {
105
+ console.log("[guardian] ✅ Pairing completed! Saving session and restarting gateway...");
106
+ hasShownWaitMessage = false;
107
+
108
+ // Auto-reapply config to finalize pairing
109
+ const getRes = await callRpc(ws, "config.get", {});
110
+ if (getRes.ok) {
111
+ await callRpc(ws, "config.apply", { raw: getRes.payload.raw, baseHash: getRes.payload.hash });
112
+ console.log("[guardian] Configuration re-applied.");
113
+ }
114
+ }
115
+
116
+ } catch (e) {
117
+ // Normal timeout or gateway starting up
118
+ } finally {
119
+ isWaiting = false;
120
+ if (ws) ws.close();
121
+ }
122
+ }
123
+
124
+ console.log("[guardian] ⚔️ WhatsApp Guardian active. Monitoring pairing status...");
125
+ setInterval(checkStatus, CHECK_INTERVAL);
126
+ setTimeout(checkStatus, 10000);