openclaw/src/web/session.ts
Arne Moor 1f959a188f chore: apply biome formatting
Applied formatting fixes to 21 files:
- Fixed import ordering
- Normalized indentation
- Wrapped long lines

No logic changes, formatting only.
2025-12-06 18:45:25 +01:00

337 lines
9.9 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { randomUUID } from "node:crypto";
import fsSync from "node:fs";
import fs from "node:fs/promises";
import path from "node:path";
import {
DisconnectReason,
fetchLatestBaileysVersion,
makeCacheableSignalKeyStore,
makeWASocket,
useMultiFileAuthState,
} from "@whiskeysockets/baileys";
import qrcode from "qrcode-terminal";
import { SESSION_STORE_DEFAULT } from "../config/sessions.js";
import { danger, info, success } from "../globals.js";
import { getChildLogger } from "../logging.js";
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
import { telegramAuthExists } from "../telegram/session.js";
import type { Provider } from "../utils.js";
import { CONFIG_DIR, ensureDir, jidToE164 } from "../utils.js";
import { VERSION } from "../version.js";
export { telegramAuthExists };
export const WA_WEB_AUTH_DIR = path.join(CONFIG_DIR, "credentials");
/**
* 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.
*/
export async function createWaSocket(printQr: boolean, verbose: boolean) {
const logger = getChildLogger(
{ module: "baileys" },
{
level: verbose ? "info" : "silent",
},
);
// Some Baileys internals call logger.trace even when silent; ensure it's present.
const loggerAny = logger as unknown as Record<string, unknown>;
if (typeof loggerAny.trace !== "function") {
loggerAny.trace = () => {};
}
await ensureDir(WA_WEB_AUTH_DIR);
const { state, saveCreds } = await useMultiFileAuthState(WA_WEB_AUTH_DIR);
const { version } = await fetchLatestBaileysVersion();
const sock = makeWASocket({
auth: {
creds: state.creds,
keys: makeCacheableSignalKeyStore(state.keys, logger),
},
version,
logger,
printQRInTerminal: false,
browser: ["warelay", "cli", VERSION],
syncFullHistory: false,
markOnlineOnConnect: false,
});
const sessionLogger = getChildLogger({ module: "web-session" });
sock.ev.on("creds.update", saveCreds);
sock.ev.on(
"connection.update",
(update: Partial<import("@whiskeysockets/baileys").ConnectionState>) => {
try {
const { connection, lastDisconnect, qr } = update;
if (qr && 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: warelay login"),
);
}
}
if (connection === "open" && verbose) {
console.log(success("WhatsApp Web connected."));
}
} catch (err) {
sessionLogger.error(
{ error: String(err) },
"connection.update handler error",
);
}
},
);
// Handle WebSocket-level errors to prevent unhandled exceptions from crashing the process
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
);
}
export function formatError(err: unknown): string {
if (err instanceof Error) return err.message;
if (typeof err === "string") return err;
const status = getStatusCode(err);
const code = (err as { code?: unknown })?.code;
if (status || code)
return `status=${status ?? "unknown"} code=${code ?? "unknown"}`;
return String(err);
}
export async function webAuthExists() {
return fs
.access(WA_WEB_AUTH_DIR)
.then(() => true)
.catch(() => false);
}
export async function logoutWeb(runtime: RuntimeEnv = defaultRuntime) {
const exists = await webAuthExists();
if (!exists) {
runtime.log(info("No WhatsApp Web session found; nothing to delete."));
return false;
}
await fs.rm(WA_WEB_AUTH_DIR, { recursive: true, force: true });
// Also drop session store to clear lingering per-sender state after logout.
await fs.rm(SESSION_STORE_DEFAULT, { force: true });
runtime.log(success("Cleared WhatsApp Web credentials."));
return true;
}
function readWebSelfId() {
// Read the cached WhatsApp Web identity (jid + E.164) from disk if present.
const credsPath = path.join(WA_WEB_AUTH_DIR, "creds.json");
try {
if (!fsSync.existsSync(credsPath)) {
return { e164: null, jid: null } as const;
}
const raw = fsSync.readFileSync(credsPath, "utf-8");
const parsed = JSON.parse(raw) as { me?: { id?: string } } | undefined;
const jid = parsed?.me?.id ?? null;
const e164 = jid ? jidToE164(jid) : null;
return { e164, jid } as const;
} catch {
return { e164: null, jid: null } as const;
}
}
/**
* Return the age (in milliseconds) of the cached WhatsApp web auth state, or null when missing.
* Helpful for heartbeats/observability to spot stale credentials.
*/
export function getWebAuthAgeMs(): number | null {
const credsPath = path.join(WA_WEB_AUTH_DIR, "creds.json");
try {
const stats = fsSync.statSync(credsPath);
return Date.now() - stats.mtimeMs;
} catch {
return null;
}
}
export function newConnectionId() {
return randomUUID();
}
export function logWebSelfId(
runtime: RuntimeEnv = defaultRuntime,
includeProviderPrefix = false,
) {
// Human-friendly log of the currently linked personal web session.
const { e164, jid } = readWebSelfId();
const details =
e164 || jid
? `${e164 ?? "unknown"}${jid ? ` (jid ${jid})` : ""}`
: "unknown";
const prefix = includeProviderPrefix ? "Web Provider: " : "";
runtime.log(info(`${prefix}${details}`));
}
export async function pickProvider(pref: Provider | "auto"): Promise<Provider> {
// Auto-select web when logged in; otherwise fall back to telegram or twilio.
if (pref !== "auto") return pref;
// Priority: web > telegram > twilio
const hasWeb = await webAuthExists();
if (hasWeb) return "web";
const hasTelegram = await telegramAuthExists();
if (hasTelegram) return "telegram";
return "twilio";
}
/**
* Select providers for multi-provider relay.
* Validates authentication and filters out unavailable providers.
*/
/**
* Check if Twilio is configured via environment variables.
*/
function isTwilioConfigured(): boolean {
const required = ["TWILIO_ACCOUNT_SID", "TWILIO_WHATSAPP_FROM"];
const hasRequired = required.every((k) => Boolean(process.env[k]));
const hasToken = Boolean(process.env.TWILIO_AUTH_TOKEN);
const hasKey = Boolean(
process.env.TWILIO_API_KEY && process.env.TWILIO_API_SECRET,
);
return hasRequired && (hasToken || hasKey);
}
export async function selectProviders(
prefs: (Provider | "auto")[],
): Promise<Provider[]> {
const skipped: string[] = [];
// If 'auto' in list, expand to all authenticated providers
if (prefs.includes("auto")) {
const available: Provider[] = [];
// Check web
if (await webAuthExists()) {
available.push("web");
} else {
skipped.push(
"web (not authenticated - run: warelay login --provider web)",
);
}
// Check telegram
if (await telegramAuthExists()) {
available.push("telegram");
} else {
skipped.push(
"telegram (not authenticated - run: warelay login --provider telegram)",
);
}
// Check twilio
if (isTwilioConfigured()) {
available.push("twilio");
} else {
skipped.push(
"twilio (not configured - set TWILIO_ACCOUNT_SID, TWILIO_WHATSAPP_FROM, and auth credentials in .env)",
);
}
if (available.length > 0 && skipped.length > 0) {
console.log(
` Auto-selected ${available.length} provider(s), skipped ${skipped.length}:`,
);
for (const skip of skipped) {
console.log(`${skip}`);
}
}
return available;
}
// Explicit provider list - validate each one
const available: Provider[] = [];
for (const pref of prefs) {
if (pref === "auto") continue; // Already handled above
if (pref === "web") {
if (await webAuthExists()) {
available.push("web");
} else {
skipped.push(
"web (not authenticated - run: warelay login --provider web)",
);
}
} else if (pref === "telegram") {
if (await telegramAuthExists()) {
available.push("telegram");
} else {
skipped.push(
"telegram (not authenticated - run: warelay login --provider telegram)",
);
}
} else if (pref === "twilio") {
if (isTwilioConfigured()) {
available.push("twilio");
} else {
skipped.push(
"twilio (not configured - set TWILIO_ACCOUNT_SID, TWILIO_WHATSAPP_FROM, and auth credentials in .env)",
);
}
}
}
if (skipped.length > 0) {
console.log(` Skipped ${skipped.length} provider(s):`);
for (const skip of skipped) {
console.log(`${skip}`);
}
}
return available;
}