@gguf/claw
Version:
WhatsApp gateway CLI (Baileys web) with Pi RPC agent
478 lines (474 loc) • 16.2 kB
JavaScript
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 };