| import { randomUUID } from "node:crypto"; |
| import fsSync from "node:fs"; |
| import { |
| DisconnectReason, |
| fetchLatestBaileysVersion, |
| makeCacheableSignalKeyStore, |
| makeWASocket, |
| useMultiFileAuthState, |
| } from "@whiskeysockets/baileys"; |
| import qrcode from "qrcode-terminal"; |
| import { danger, success } from "../globals.js"; |
| import { getChildLogger, toPinoLikeLogger } from "../logging.js"; |
| import { ensureDir, resolveUserPath } from "../utils.js"; |
| import { VERSION } from "../version.js"; |
| import { formatCliCommand } from "../cli/command-format.js"; |
|
|
| import { |
| maybeRestoreCredsFromBackup, |
| resolveDefaultWebAuthDir, |
| resolveWebCredsBackupPath, |
| resolveWebCredsPath, |
| } from "./auth-store.js"; |
|
|
| export { |
| getWebAuthAgeMs, |
| logoutWeb, |
| logWebSelfId, |
| pickWebChannel, |
| readWebSelfId, |
| WA_WEB_AUTH_DIR, |
| webAuthExists, |
| } from "./auth-store.js"; |
|
|
| let credsSaveQueue: Promise<void> = Promise.resolve(); |
| function enqueueSaveCreds( |
| authDir: string, |
| saveCreds: () => Promise<void> | void, |
| logger: ReturnType<typeof getChildLogger>, |
| ): void { |
| credsSaveQueue = credsSaveQueue |
| .then(() => safeSaveCreds(authDir, saveCreds, logger)) |
| .catch((err) => { |
| logger.warn({ error: String(err) }, "WhatsApp creds save queue error"); |
| }); |
| } |
|
|
| function readCredsJsonRaw(filePath: string): string | null { |
| try { |
| if (!fsSync.existsSync(filePath)) { |
| return null; |
| } |
| const stats = fsSync.statSync(filePath); |
| if (!stats.isFile() || stats.size <= 1) { |
| return null; |
| } |
| return fsSync.readFileSync(filePath, "utf-8"); |
| } catch { |
| return null; |
| } |
| } |
|
|
| async function safeSaveCreds( |
| authDir: string, |
| saveCreds: () => Promise<void> | void, |
| logger: ReturnType<typeof getChildLogger>, |
| ): Promise<void> { |
| try { |
| |
| |
| const credsPath = resolveWebCredsPath(authDir); |
| const backupPath = resolveWebCredsBackupPath(authDir); |
| const raw = readCredsJsonRaw(credsPath); |
| if (raw) { |
| try { |
| JSON.parse(raw); |
| fsSync.copyFileSync(credsPath, backupPath); |
| } catch { |
| |
| } |
| } |
| } catch { |
| |
| } |
| try { |
| await Promise.resolve(saveCreds()); |
| } catch (err) { |
| logger.warn({ error: String(err) }, "failed saving WhatsApp creds"); |
| } |
| } |
|
|
| |
| |
| |
| |
| export async function createWaSocket( |
| printQr: boolean, |
| verbose: boolean, |
| opts: { authDir?: string; onQr?: (qr: string) => void } = {}, |
| ) { |
| const baseLogger = getChildLogger( |
| { module: "baileys" }, |
| { |
| level: verbose ? "info" : "silent", |
| }, |
| ); |
| const logger = toPinoLikeLogger(baseLogger, 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: Partial<import("@whiskeysockets/baileys").ConnectionState>) => { |
| 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") { |
| const status = getStatusCode(lastDisconnect?.error); |
| if (status === 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 as unknown as { on?: unknown }).on === "function") { |
| sock.ws.on("error", (err: Error) => { |
| sessionLogger.error({ error: String(err) }, "WebSocket error"); |
| }); |
| } |
|
|
| return sock; |
| } |
|
|
| export async function waitForWaConnection(sock: ReturnType<typeof makeWASocket>) { |
| return new Promise<void>((resolve, reject) => { |
| type OffCapable = { |
| off?: (event: string, listener: (...args: unknown[]) => void) => void; |
| }; |
| const evWithOff = sock.ev as unknown as OffCapable; |
|
|
| const handler = (...args: unknown[]) => { |
| const update = (args[0] ?? {}) as Partial<import("@whiskeysockets/baileys").ConnectionState>; |
| if (update.connection === "open") { |
| evWithOff.off?.("connection.update", handler); |
| resolve(); |
| } |
| if (update.connection === "close") { |
| evWithOff.off?.("connection.update", handler); |
| reject(update.lastDisconnect ?? new Error("Connection closed")); |
| } |
| }; |
|
|
| sock.ev.on("connection.update", handler); |
| }); |
| } |
|
|
| export function getStatusCode(err: unknown) { |
| return ( |
| (err as { output?: { statusCode?: number } })?.output?.statusCode ?? |
| (err as { status?: number })?.status |
| ); |
| } |
|
|
| function safeStringify(value: unknown, limit = 800): string { |
| try { |
| const seen = new WeakSet(); |
| const raw = JSON.stringify( |
| value, |
| (_key, v) => { |
| if (typeof v === "bigint") { |
| return v.toString(); |
| } |
| if (typeof v === "function") { |
| const maybeName = (v as { name?: unknown }).name; |
| const name = |
| typeof maybeName === "string" && maybeName.length > 0 ? maybeName : "anonymous"; |
| return `[Function ${name}]`; |
| } |
| 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: unknown): { |
| statusCode?: number; |
| error?: string; |
| message?: string; |
| } | null { |
| if (!err || typeof err !== "object") { |
| return null; |
| } |
| const output = (err as { output?: unknown })?.output as |
| | { statusCode?: unknown; payload?: unknown } |
| | undefined; |
| if (!output || typeof output !== "object") { |
| return null; |
| } |
| const payload = (output as { payload?: unknown }).payload as |
| | { error?: unknown; message?: unknown; statusCode?: unknown } |
| | undefined; |
| const statusCode = |
| typeof (output as { statusCode?: unknown }).statusCode === "number" |
| ? ((output as { statusCode?: unknown }).statusCode as number) |
| : typeof payload?.statusCode === "number" |
| ? payload.statusCode |
| : undefined; |
| const error = typeof payload?.error === "string" ? payload.error : undefined; |
| const message = typeof payload?.message === "string" ? payload.message : undefined; |
| if (!statusCode && !error && !message) { |
| return null; |
| } |
| return { statusCode, error, message }; |
| } |
|
|
| export function formatError(err: unknown): string { |
| 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 as { error?: unknown })?.error) ?? |
| extractBoomDetails((err as { lastDisconnect?: { error?: unknown } })?.lastDisconnect?.error); |
|
|
| const status = boom?.statusCode ?? getStatusCode(err); |
| const code = (err as { code?: unknown })?.code; |
| const codeText = typeof code === "string" || typeof code === "number" ? String(code) : undefined; |
|
|
| const messageCandidates = [ |
| boom?.message, |
| typeof (err as { message?: unknown })?.message === "string" |
| ? ((err as { message?: unknown }).message as string) |
| : undefined, |
| typeof (err as { error?: { message?: unknown } })?.error?.message === "string" |
| ? ((err as { error?: { message?: unknown } }).error?.message as string) |
| : undefined, |
| ].filter((v): v is string => Boolean(v && v.trim().length > 0)); |
| const message = messageCandidates[0]; |
|
|
| const pieces: string[] = []; |
| 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); |
| } |
|
|
| export function newConnectionId() { |
| return randomUUID(); |
| } |
|
|