openclaw-vainplex/src/web/login-qr.ts
2025-12-20 18:39:17 +01:00

203 lines
5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

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 { danger, info, success } from "../globals.js";
import { logInfo } from "../logger.js";
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
import { renderQrPngBase64 } from "./qr-image.js";
import {
createWaSocket,
formatError,
readWebSelfId,
waitForWaConnection,
webAuthExists,
} from "./session.js";
type WaSocket = Awaited<ReturnType<typeof createWaSocket>>;
type ActiveLogin = {
id: string;
sock: WaSocket;
startedAt: number;
qr?: string;
qrDataUrl?: string;
connected: boolean;
error?: string;
waitPromise: Promise<void>;
};
const ACTIVE_LOGIN_TTL_MS = 3 * 60_000;
let activeLogin: ActiveLogin | null = null;
function closeSocket(sock: WaSocket) {
try {
sock.ws?.close();
} catch {
// ignore
}
}
async function resetActiveLogin(reason?: string) {
if (activeLogin) {
closeSocket(activeLogin.sock);
activeLogin = null;
}
if (reason) {
logInfo(reason);
}
}
function isLoginFresh(login: ActiveLogin) {
return Date.now() - login.startedAt < ACTIVE_LOGIN_TTL_MS;
}
export async function startWebLoginWithQr(
opts: {
verbose?: boolean;
timeoutMs?: number;
force?: boolean;
runtime?: RuntimeEnv;
} = {},
): Promise<{ qrDataUrl?: string; message: string }> {
const runtime = opts.runtime ?? defaultRuntime;
const hasWeb = await webAuthExists();
const selfId = readWebSelfId();
if (hasWeb && !opts.force) {
const who = selfId.e164 ?? selfId.jid ?? "unknown";
return {
message: `WhatsApp is already linked (${who}). Say “relink” if you want a fresh QR.`,
};
}
if (activeLogin && isLoginFresh(activeLogin) && activeLogin.qrDataUrl) {
return {
qrDataUrl: activeLogin.qrDataUrl,
message: "QR already active. Scan it in WhatsApp → Linked Devices.",
};
}
await resetActiveLogin();
let resolveQr: ((qr: string) => void) | null = null;
let rejectQr: ((err: Error) => void) | null = null;
const qrPromise = new Promise<string>((resolve, reject) => {
resolveQr = resolve;
rejectQr = reject;
});
const qrTimer = setTimeout(
() => {
rejectQr?.(new Error("Timed out waiting for WhatsApp QR"));
},
Math.max(opts.timeoutMs ?? 30_000, 5000),
);
let sock: WaSocket;
try {
sock = await createWaSocket(false, Boolean(opts.verbose), {
onQr: (qr: string) => {
if (!activeLogin || activeLogin.qr) return;
activeLogin.qr = qr;
clearTimeout(qrTimer);
runtime.log(info("WhatsApp QR received."));
resolveQr?.(qr);
},
});
} catch (err) {
clearTimeout(qrTimer);
await resetActiveLogin();
return {
message: `Failed to start WhatsApp login: ${String(err)}`,
};
}
const login: ActiveLogin = {
id: randomUUID(),
sock,
startedAt: Date.now(),
connected: false,
waitPromise: Promise.resolve(),
};
activeLogin = login;
login.waitPromise = waitForWaConnection(sock)
.then(() => {
if (activeLogin?.id === login.id) {
activeLogin.connected = true;
}
})
.catch((err) => {
if (activeLogin?.id === login.id) {
activeLogin.error = formatError(err);
}
});
let qr: string;
try {
qr = await qrPromise;
} catch (err) {
clearTimeout(qrTimer);
await resetActiveLogin();
return {
message: `Failed to get QR: ${String(err)}`,
};
}
const base64 = await renderQrPngBase64(qr);
login.qrDataUrl = `data:image/png;base64,${base64}`;
return {
qrDataUrl: login.qrDataUrl,
message: "Scan this QR in WhatsApp → Linked Devices.",
};
}
export async function waitForWebLogin(
opts: { timeoutMs?: number; runtime?: RuntimeEnv } = {},
): Promise<{ connected: boolean; message: string }> {
const runtime = opts.runtime ?? defaultRuntime;
if (!activeLogin) {
return {
connected: false,
message: "No active WhatsApp login in progress.",
};
}
const login = activeLogin;
if (!isLoginFresh(login)) {
await resetActiveLogin();
return {
connected: false,
message: "The login QR expired. Ask me to generate a new one.",
};
}
const timeoutMs = Math.max(opts.timeoutMs ?? 120_000, 1000);
const timeout = new Promise<"timeout">((resolve) =>
setTimeout(() => resolve("timeout"), timeoutMs),
);
const result = await Promise.race([
login.waitPromise.then(() => "done"),
timeout,
]);
if (result === "timeout") {
return {
connected: false,
message:
"Still waiting for the QR scan. Let me know when youve scanned it.",
};
}
if (login.error) {
const message = `WhatsApp login failed: ${login.error}`;
await resetActiveLogin(message);
runtime.log(danger(message));
return { connected: false, message };
}
if (login.connected) {
const message = "✅ Linked! WhatsApp is ready.";
runtime.log(success(message));
await resetActiveLogin();
return { connected: true, message };
}
return { connected: false, message: "Login ended without a connection." };
}