UNPKG

@gguf/claw

Version:

WhatsApp gateway CLI (Baileys web) with Pi RPC agent

478 lines (474 loc) 16.2 kB
import { t as __exportAll } from "./rolldown-runtime-Cbj13DAv.js"; import { D as info, E as danger, H as toPinoLikeLogger, P as success, c as defaultRuntime, z as getChildLogger } from "./subsystem-CAq3uyo7.js"; import { h as resolveUserPath, s as ensureDir } from "./utils-CKSrBNwq.js"; import { s as logInfo } from "./exec-HEWTMJ7j.js"; import { t as formatCliCommand } from "./command-format-ChfKqObn.js"; import { i as loadConfig, j as VERSION } from "./config-CAuZ-EkU.js"; import { a as logoutWeb, c as resolveDefaultWebAuthDir, d as webAuthExists, l as resolveWebCredsBackupPath, n as resolveWhatsAppAccount, o as maybeRestoreCredsFromBackup, s as readWebSelfId, u as resolveWebCredsPath } from "./accounts-BgZmhIm6.js"; import fs from "node:fs"; import { randomUUID } from "node:crypto"; import { DisconnectReason, fetchLatestBaileysVersion, makeCacheableSignalKeyStore, makeWASocket, useMultiFileAuthState } from "@whiskeysockets/baileys"; import qrcode from "qrcode-terminal"; import { deflateSync } from "node:zlib"; import QRCodeModule from "qrcode-terminal/vendor/QRCode/index.js"; import QRErrorCorrectLevelModule from "qrcode-terminal/vendor/QRCode/QRErrorCorrectLevel.js"; //#region src/web/session.ts let credsSaveQueue = Promise.resolve(); function enqueueSaveCreds(authDir, saveCreds, logger) { credsSaveQueue = credsSaveQueue.then(() => safeSaveCreds(authDir, saveCreds, logger)).catch((err) => { logger.warn({ error: String(err) }, "WhatsApp creds save queue error"); }); } function readCredsJsonRaw(filePath) { try { if (!fs.existsSync(filePath)) return null; const stats = fs.statSync(filePath); if (!stats.isFile() || stats.size <= 1) return null; return fs.readFileSync(filePath, "utf-8"); } catch { return null; } } async function safeSaveCreds(authDir, saveCreds, logger) { try { const credsPath = resolveWebCredsPath(authDir); const backupPath = resolveWebCredsBackupPath(authDir); const raw = readCredsJsonRaw(credsPath); if (raw) try { JSON.parse(raw); fs.copyFileSync(credsPath, backupPath); } catch {} } catch {} try { await Promise.resolve(saveCreds()); } catch (err) { logger.warn({ error: String(err) }, "failed saving WhatsApp creds"); } } /** * Create a Baileys socket backed by the multi-file auth store we keep on disk. * Consumers can opt into QR printing for interactive login flows. */ async function createWaSocket(printQr, verbose, opts = {}) { const logger = toPinoLikeLogger(getChildLogger({ module: "baileys" }, { level: verbose ? "info" : "silent" }), verbose ? "info" : "silent"); const authDir = resolveUserPath(opts.authDir ?? resolveDefaultWebAuthDir()); await ensureDir(authDir); const sessionLogger = getChildLogger({ module: "web-session" }); maybeRestoreCredsFromBackup(authDir); const { state, saveCreds } = await useMultiFileAuthState(authDir); const { version } = await fetchLatestBaileysVersion(); const sock = makeWASocket({ auth: { creds: state.creds, keys: makeCacheableSignalKeyStore(state.keys, logger) }, version, logger, printQRInTerminal: false, browser: [ "openclaw", "cli", VERSION ], syncFullHistory: false, markOnlineOnConnect: false }); sock.ev.on("creds.update", () => enqueueSaveCreds(authDir, saveCreds, sessionLogger)); sock.ev.on("connection.update", (update) => { try { const { connection, lastDisconnect, qr } = update; if (qr) { opts.onQr?.(qr); if (printQr) { console.log("Scan this QR in WhatsApp (Linked Devices):"); qrcode.generate(qr, { small: true }); } } if (connection === "close") { if (getStatusCode(lastDisconnect?.error) === DisconnectReason.loggedOut) console.error(danger(`WhatsApp session logged out. Run: ${formatCliCommand("openclaw channels login")}`)); } if (connection === "open" && verbose) console.log(success("WhatsApp Web connected.")); } catch (err) { sessionLogger.error({ error: String(err) }, "connection.update handler error"); } }); if (sock.ws && typeof sock.ws.on === "function") sock.ws.on("error", (err) => { sessionLogger.error({ error: String(err) }, "WebSocket error"); }); return sock; } async function waitForWaConnection(sock) { return new Promise((resolve, reject) => { const evWithOff = sock.ev; const handler = (...args) => { const update = args[0] ?? {}; if (update.connection === "open") { evWithOff.off?.("connection.update", handler); resolve(); } if (update.connection === "close") { evWithOff.off?.("connection.update", handler); reject(update.lastDisconnect ?? /* @__PURE__ */ new Error("Connection closed")); } }; sock.ev.on("connection.update", handler); }); } function getStatusCode(err) { return err?.output?.statusCode ?? err?.status; } function safeStringify(value, limit = 800) { try { const seen = /* @__PURE__ */ new WeakSet(); const raw = JSON.stringify(value, (_key, v) => { if (typeof v === "bigint") return v.toString(); if (typeof v === "function") { const maybeName = v.name; return `[Function ${typeof maybeName === "string" && maybeName.length > 0 ? maybeName : "anonymous"}]`; } if (typeof v === "object" && v) { if (seen.has(v)) return "[Circular]"; seen.add(v); } return v; }, 2); if (!raw) return String(value); return raw.length > limit ? `${raw.slice(0, limit)}…` : raw; } catch { return String(value); } } function extractBoomDetails(err) { if (!err || typeof err !== "object") return null; const output = err?.output; if (!output || typeof output !== "object") return null; const payload = output.payload; const statusCode = typeof output.statusCode === "number" ? output.statusCode : typeof payload?.statusCode === "number" ? payload.statusCode : void 0; const error = typeof payload?.error === "string" ? payload.error : void 0; const message = typeof payload?.message === "string" ? payload.message : void 0; if (!statusCode && !error && !message) return null; return { statusCode, error, message }; } function formatError(err) { if (err instanceof Error) return err.message; if (typeof err === "string") return err; if (!err || typeof err !== "object") return String(err); const boom = extractBoomDetails(err) ?? extractBoomDetails(err?.error) ?? extractBoomDetails(err?.lastDisconnect?.error); const status = boom?.statusCode ?? getStatusCode(err); const code = err?.code; const codeText = typeof code === "string" || typeof code === "number" ? String(code) : void 0; const message = [ boom?.message, typeof err?.message === "string" ? err.message : void 0, typeof err?.error?.message === "string" ? err.error?.message : void 0 ].filter((v) => Boolean(v && v.trim().length > 0))[0]; const pieces = []; if (typeof status === "number") pieces.push(`status=${status}`); if (boom?.error) pieces.push(boom.error); if (message) pieces.push(message); if (codeText) pieces.push(`code=${codeText}`); if (pieces.length > 0) return pieces.join(" "); return safeStringify(err); } //#endregion //#region src/web/qr-image.ts const QRCode = QRCodeModule; const QRErrorCorrectLevel = QRErrorCorrectLevelModule; function createQrMatrix(input) { const qr = new QRCode(-1, QRErrorCorrectLevel.L); qr.addData(input); qr.make(); return qr; } function fillPixel(buf, x, y, width, r, g, b, a = 255) { const idx = (y * width + x) * 4; buf[idx] = r; buf[idx + 1] = g; buf[idx + 2] = b; buf[idx + 3] = a; } function crcTable() { const table = new Uint32Array(256); for (let i = 0; i < 256; i += 1) { let c = i; for (let k = 0; k < 8; k += 1) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1; table[i] = c >>> 0; } return table; } const CRC_TABLE = crcTable(); function crc32(buf) { let crc = 4294967295; for (let i = 0; i < buf.length; i += 1) crc = CRC_TABLE[(crc ^ buf[i]) & 255] ^ crc >>> 8; return (crc ^ 4294967295) >>> 0; } function pngChunk(type, data) { const typeBuf = Buffer.from(type, "ascii"); const len = Buffer.alloc(4); len.writeUInt32BE(data.length, 0); const crc = crc32(Buffer.concat([typeBuf, data])); const crcBuf = Buffer.alloc(4); crcBuf.writeUInt32BE(crc, 0); return Buffer.concat([ len, typeBuf, data, crcBuf ]); } function encodePngRgba(buffer, width, height) { const stride = width * 4; const raw = Buffer.alloc((stride + 1) * height); for (let row = 0; row < height; row += 1) { const rawOffset = row * (stride + 1); raw[rawOffset] = 0; buffer.copy(raw, rawOffset + 1, row * stride, row * stride + stride); } const compressed = deflateSync(raw); const signature = Buffer.from([ 137, 80, 78, 71, 13, 10, 26, 10 ]); const ihdr = Buffer.alloc(13); ihdr.writeUInt32BE(width, 0); ihdr.writeUInt32BE(height, 4); ihdr[8] = 8; ihdr[9] = 6; ihdr[10] = 0; ihdr[11] = 0; ihdr[12] = 0; return Buffer.concat([ signature, pngChunk("IHDR", ihdr), pngChunk("IDAT", compressed), pngChunk("IEND", Buffer.alloc(0)) ]); } async function renderQrPngBase64(input, opts = {}) { const { scale = 6, marginModules = 4 } = opts; const qr = createQrMatrix(input); const modules = qr.getModuleCount(); const size = (modules + marginModules * 2) * scale; const buf = Buffer.alloc(size * size * 4, 255); for (let row = 0; row < modules; row += 1) for (let col = 0; col < modules; col += 1) { if (!qr.isDark(row, col)) continue; const startX = (col + marginModules) * scale; const startY = (row + marginModules) * scale; for (let y = 0; y < scale; y += 1) { const pixelY = startY + y; for (let x = 0; x < scale; x += 1) fillPixel(buf, startX + x, pixelY, size, 0, 0, 0, 255); } } return encodePngRgba(buf, size, size).toString("base64"); } //#endregion //#region src/web/login-qr.ts var login_qr_exports = /* @__PURE__ */ __exportAll({ startWebLoginWithQr: () => startWebLoginWithQr, waitForWebLogin: () => waitForWebLogin }); const ACTIVE_LOGIN_TTL_MS = 3 * 6e4; const activeLogins = /* @__PURE__ */ new Map(); function closeSocket(sock) { try { sock.ws?.close(); } catch {} } async function resetActiveLogin(accountId, reason) { const login = activeLogins.get(accountId); if (login) { closeSocket(login.sock); activeLogins.delete(accountId); } if (reason) logInfo(reason); } function isLoginFresh(login) { return Date.now() - login.startedAt < ACTIVE_LOGIN_TTL_MS; } function attachLoginWaiter(accountId, login) { login.waitPromise = waitForWaConnection(login.sock).then(() => { const current = activeLogins.get(accountId); if (current?.id === login.id) current.connected = true; }).catch((err) => { const current = activeLogins.get(accountId); if (current?.id !== login.id) return; current.error = formatError(err); current.errorStatus = getStatusCode(err); }); } async function restartLoginSocket(login, runtime) { if (login.restartAttempted) return false; login.restartAttempted = true; runtime.log(info("WhatsApp asked for a restart after pairing (code 515); retrying connection once…")); closeSocket(login.sock); try { login.sock = await createWaSocket(false, login.verbose, { authDir: login.authDir }); login.connected = false; login.error = void 0; login.errorStatus = void 0; attachLoginWaiter(login.accountId, login); return true; } catch (err) { login.error = formatError(err); login.errorStatus = getStatusCode(err); return false; } } async function startWebLoginWithQr(opts = {}) { const runtime = opts.runtime ?? defaultRuntime; const account = resolveWhatsAppAccount({ cfg: loadConfig(), accountId: opts.accountId }); const hasWeb = await webAuthExists(account.authDir); const selfId = readWebSelfId(account.authDir); if (hasWeb && !opts.force) return { message: `WhatsApp is already linked (${selfId.e164 ?? selfId.jid ?? "unknown"}). Say “relink” if you want a fresh QR.` }; const existing = activeLogins.get(account.accountId); if (existing && isLoginFresh(existing) && existing.qrDataUrl) return { qrDataUrl: existing.qrDataUrl, message: "QR already active. Scan it in WhatsApp → Linked Devices." }; await resetActiveLogin(account.accountId); let resolveQr = null; let rejectQr = null; const qrPromise = new Promise((resolve, reject) => { resolveQr = resolve; rejectQr = reject; }); const qrTimer = setTimeout(() => { rejectQr?.(/* @__PURE__ */ new Error("Timed out waiting for WhatsApp QR")); }, Math.max(opts.timeoutMs ?? 3e4, 5e3)); let sock; let pendingQr = null; try { sock = await createWaSocket(false, Boolean(opts.verbose), { authDir: account.authDir, onQr: (qr) => { if (pendingQr) return; pendingQr = qr; const current = activeLogins.get(account.accountId); if (current && !current.qr) current.qr = qr; clearTimeout(qrTimer); runtime.log(info("WhatsApp QR received.")); resolveQr?.(qr); } }); } catch (err) { clearTimeout(qrTimer); await resetActiveLogin(account.accountId); return { message: `Failed to start WhatsApp login: ${String(err)}` }; } const login = { accountId: account.accountId, authDir: account.authDir, isLegacyAuthDir: account.isLegacyAuthDir, id: randomUUID(), sock, startedAt: Date.now(), connected: false, waitPromise: Promise.resolve(), restartAttempted: false, verbose: Boolean(opts.verbose) }; activeLogins.set(account.accountId, login); if (pendingQr && !login.qr) login.qr = pendingQr; attachLoginWaiter(account.accountId, login); let qr; try { qr = await qrPromise; } catch (err) { clearTimeout(qrTimer); await resetActiveLogin(account.accountId); return { message: `Failed to get QR: ${String(err)}` }; } login.qrDataUrl = `data:image/png;base64,${await renderQrPngBase64(qr)}`; return { qrDataUrl: login.qrDataUrl, message: "Scan this QR in WhatsApp → Linked Devices." }; } async function waitForWebLogin(opts = {}) { const runtime = opts.runtime ?? defaultRuntime; const account = resolveWhatsAppAccount({ cfg: loadConfig(), accountId: opts.accountId }); const activeLogin = activeLogins.get(account.accountId); if (!activeLogin) return { connected: false, message: "No active WhatsApp login in progress." }; const login = activeLogin; if (!isLoginFresh(login)) { await resetActiveLogin(account.accountId); return { connected: false, message: "The login QR expired. Ask me to generate a new one." }; } const timeoutMs = Math.max(opts.timeoutMs ?? 12e4, 1e3); const deadline = Date.now() + timeoutMs; while (true) { const remaining = deadline - Date.now(); if (remaining <= 0) return { connected: false, message: "Still waiting for the QR scan. Let me know when you’ve scanned it." }; const timeout = new Promise((resolve) => setTimeout(() => resolve("timeout"), remaining)); if (await Promise.race([login.waitPromise.then(() => "done"), timeout]) === "timeout") return { connected: false, message: "Still waiting for the QR scan. Let me know when you’ve scanned it." }; if (login.error) { if (login.errorStatus === DisconnectReason.loggedOut) { await logoutWeb({ authDir: login.authDir, isLegacyAuthDir: login.isLegacyAuthDir, runtime }); const message = "WhatsApp reported the session is logged out. Cleared cached web session; please scan a new QR."; await resetActiveLogin(account.accountId, message); runtime.log(danger(message)); return { connected: false, message }; } if (login.errorStatus === 515) { if (await restartLoginSocket(login, runtime) && isLoginFresh(login)) continue; } const message = `WhatsApp login failed: ${login.error}`; await resetActiveLogin(account.accountId, message); runtime.log(danger(message)); return { connected: false, message }; } if (login.connected) { const message = "✅ Linked! WhatsApp is ready."; runtime.log(success(message)); await resetActiveLogin(account.accountId); return { connected: true, message }; } return { connected: false, message: "Login ended without a connection." }; } } //#endregion export { formatError as a, createWaSocket as i, startWebLoginWithQr as n, getStatusCode as o, waitForWebLogin as r, waitForWaConnection as s, login_qr_exports as t };