fix: auto-restart WhatsApp QR login
This commit is contained in:
@@ -17,7 +17,7 @@ Short guide to verify the WhatsApp Web / Baileys stack without guessing.
|
||||
## Deep diagnostics
|
||||
- Creds on disk: `ls -l ~/.clawdis/credentials/creds.json` (mtime should be recent).
|
||||
- Session store: `ls -l ~/.clawdis/sessions/sessions.json` (legacy: `~/.clawdis/sessions.json`; path can be overridden in config). Count and recent recipients are surfaced via `status`.
|
||||
- Relink flow: `clawdis logout && clawdis login --verbose` when status codes 409–515 or `loggedOut` appear in logs.
|
||||
- Relink flow: `clawdis logout && clawdis login --verbose` when status codes 409–515 or `loggedOut` appear in logs. (Note: the QR login flow auto-restarts once for status 515 after pairing.)
|
||||
|
||||
## When something fails
|
||||
- `logged out` or status 409–515 → relink with `clawdis logout` then `clawdis login`.
|
||||
|
||||
@@ -10,6 +10,7 @@ read_when:
|
||||
- **Logged out:** Console prints “session logged out”; re-link with `clawdis login`.
|
||||
- **Repeated retries then exit:** Tune reconnect behavior via config `web.reconnect` and restart the Gateway.
|
||||
- **No inbound messages:** Ensure the QR-linked account is online in WhatsApp, and check logs for `web-heartbeat` to confirm auth age/connection.
|
||||
- **Status 515 right after pairing:** The QR login flow now auto-restarts once; you should not need a manual gateway restart after scanning.
|
||||
- **Fast nuke:** From an allowed WhatsApp sender you can send `/restart` to request a supervised restart (launchd/mac app setups); wait a few seconds for it to come back.
|
||||
|
||||
## Helpful commands
|
||||
|
||||
66
src/web/login-qr.test.ts
Normal file
66
src/web/login-qr.test.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("./session.js", () => {
|
||||
const createWaSocket = vi.fn(
|
||||
async (
|
||||
_printQr: boolean,
|
||||
_verbose: boolean,
|
||||
opts?: { onQr?: (qr: string) => void },
|
||||
) => {
|
||||
const sock = { ws: { close: vi.fn() } };
|
||||
if (opts?.onQr) {
|
||||
setImmediate(() => opts.onQr?.("qr-data"));
|
||||
}
|
||||
return sock;
|
||||
},
|
||||
);
|
||||
const waitForWaConnection = vi.fn();
|
||||
const formatError = vi.fn((err: unknown) => `formatted:${String(err)}`);
|
||||
const getStatusCode = vi.fn(
|
||||
(err: unknown) =>
|
||||
(err as { output?: { statusCode?: number } })?.output?.statusCode ??
|
||||
(err as { status?: number })?.status,
|
||||
);
|
||||
const webAuthExists = vi.fn(async () => false);
|
||||
const readWebSelfId = vi.fn(() => ({ e164: null, jid: null }));
|
||||
const logoutWeb = vi.fn(async () => true);
|
||||
return {
|
||||
createWaSocket,
|
||||
waitForWaConnection,
|
||||
formatError,
|
||||
getStatusCode,
|
||||
webAuthExists,
|
||||
readWebSelfId,
|
||||
logoutWeb,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("./qr-image.js", () => ({
|
||||
renderQrPngBase64: vi.fn(async () => "base64"),
|
||||
}));
|
||||
|
||||
const { startWebLoginWithQr, waitForWebLogin } = await import("./login-qr.js");
|
||||
const { createWaSocket, waitForWaConnection, logoutWeb } = await import(
|
||||
"./session.js"
|
||||
);
|
||||
|
||||
describe("login-qr", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("restarts login once on status 515 and completes", async () => {
|
||||
waitForWaConnection
|
||||
.mockRejectedValueOnce({ output: { statusCode: 515 } })
|
||||
.mockResolvedValueOnce(undefined);
|
||||
|
||||
const start = await startWebLoginWithQr({ timeoutMs: 5000 });
|
||||
expect(start.qrDataUrl).toBe("data:image/png;base64,base64");
|
||||
|
||||
const result = await waitForWebLogin({ timeoutMs: 5000 });
|
||||
|
||||
expect(result.connected).toBe(true);
|
||||
expect(createWaSocket).toHaveBeenCalledTimes(2);
|
||||
expect(logoutWeb).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -28,6 +28,8 @@ type ActiveLogin = {
|
||||
error?: string;
|
||||
errorStatus?: number;
|
||||
waitPromise: Promise<void>;
|
||||
restartAttempted: boolean;
|
||||
verbose: boolean;
|
||||
};
|
||||
|
||||
const ACTIVE_LOGIN_TTL_MS = 3 * 60_000;
|
||||
@@ -55,6 +57,45 @@ function isLoginFresh(login: ActiveLogin) {
|
||||
return Date.now() - login.startedAt < ACTIVE_LOGIN_TTL_MS;
|
||||
}
|
||||
|
||||
function attachLoginWaiter(login: ActiveLogin) {
|
||||
login.waitPromise = waitForWaConnection(login.sock)
|
||||
.then(() => {
|
||||
if (activeLogin?.id === login.id) {
|
||||
activeLogin.connected = true;
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
if (activeLogin?.id === login.id) {
|
||||
activeLogin.error = formatError(err);
|
||||
activeLogin.errorStatus = getStatusCode(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function restartLoginSocket(login: ActiveLogin, runtime: RuntimeEnv) {
|
||||
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 {
|
||||
const sock = await createWaSocket(false, login.verbose);
|
||||
login.sock = sock;
|
||||
login.connected = false;
|
||||
login.error = undefined;
|
||||
login.errorStatus = undefined;
|
||||
attachLoginWaiter(login);
|
||||
return true;
|
||||
} catch (err) {
|
||||
login.error = formatError(err);
|
||||
login.errorStatus = getStatusCode(err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function startWebLoginWithQr(
|
||||
opts: {
|
||||
verbose?: boolean;
|
||||
@@ -120,21 +161,11 @@ export async function startWebLoginWithQr(
|
||||
startedAt: Date.now(),
|
||||
connected: false,
|
||||
waitPromise: Promise.resolve(),
|
||||
restartAttempted: false,
|
||||
verbose: Boolean(opts.verbose),
|
||||
};
|
||||
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);
|
||||
activeLogin.errorStatus = getStatusCode(err);
|
||||
}
|
||||
});
|
||||
attachLoginWaiter(login);
|
||||
|
||||
let qr: string;
|
||||
try {
|
||||
@@ -175,43 +206,61 @@ export async function waitForWebLogin(
|
||||
};
|
||||
}
|
||||
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,
|
||||
]);
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
|
||||
if (result === "timeout") {
|
||||
return {
|
||||
connected: false,
|
||||
message:
|
||||
"Still waiting for the QR scan. Let me know when you’ve scanned it.",
|
||||
};
|
||||
}
|
||||
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<"timeout">((resolve) =>
|
||||
setTimeout(() => resolve("timeout"), remaining),
|
||||
);
|
||||
const result = await Promise.race([
|
||||
login.waitPromise.then(() => "done"),
|
||||
timeout,
|
||||
]);
|
||||
|
||||
if (login.error) {
|
||||
if (login.errorStatus === DisconnectReason.loggedOut) {
|
||||
await logoutWeb(runtime);
|
||||
const message =
|
||||
"WhatsApp reported the session is logged out. Cleared cached web session; please scan a new QR.";
|
||||
if (result === "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(runtime);
|
||||
const message =
|
||||
"WhatsApp reported the session is logged out. Cleared cached web session; please scan a new QR.";
|
||||
await resetActiveLogin(message);
|
||||
runtime.log(danger(message));
|
||||
return { connected: false, message };
|
||||
}
|
||||
if (login.errorStatus === 515) {
|
||||
const restarted = await restartLoginSocket(login, runtime);
|
||||
if (restarted && isLoginFresh(login)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
const message = `WhatsApp login failed: ${login.error}`;
|
||||
await resetActiveLogin(message);
|
||||
runtime.log(danger(message));
|
||||
return { connected: false, message };
|
||||
}
|
||||
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 };
|
||||
}
|
||||
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." };
|
||||
return { connected: false, message: "Login ended without a connection." };
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user