refactor: lint cleanups and helpers
This commit is contained in:
@@ -253,8 +253,9 @@ export async function runWebHeartbeatOnce(opts: {
|
||||
if (sessionId) {
|
||||
const storePath = resolveStorePath(cfg.inbound?.session?.store);
|
||||
const store = loadSessionStore(storePath);
|
||||
const current = store[sessionKey] ?? {};
|
||||
store[sessionKey] = {
|
||||
...(store[sessionKey] ?? {}),
|
||||
...current,
|
||||
sessionId,
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
@@ -404,10 +405,10 @@ export async function runWebHeartbeatOnce(opts: {
|
||||
);
|
||||
whatsappHeartbeatLog.info(`heartbeat alert sent to ${to}`);
|
||||
} catch (err) {
|
||||
const reason = String(err);
|
||||
const reason = formatError(err);
|
||||
heartbeatLogger.warn({ to, error: reason }, "heartbeat failed");
|
||||
whatsappHeartbeatLog.warn(`heartbeat failed (${reason})`);
|
||||
emitHeartbeatEvent({ status: "failed", to, reason: String(err) });
|
||||
emitHeartbeatEvent({ status: "failed", to, reason });
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
@@ -561,18 +562,17 @@ async function deliverWebReply(params: {
|
||||
return await fn();
|
||||
} catch (err) {
|
||||
lastErr = err;
|
||||
const errText = formatError(err);
|
||||
const isLast = attempt === maxAttempts;
|
||||
const shouldRetry = /closed|reset|timed\s*out|disconnect/i.test(
|
||||
String(err ?? ""),
|
||||
errText,
|
||||
);
|
||||
if (!shouldRetry || isLast) {
|
||||
throw err;
|
||||
}
|
||||
const backoffMs = 500 * attempt;
|
||||
logVerbose(
|
||||
`Retrying ${label} to ${msg.from} after failure (${attempt}/${maxAttempts - 1}) in ${backoffMs}ms: ${String(
|
||||
err,
|
||||
)}`,
|
||||
`Retrying ${label} to ${msg.from} after failure (${attempt}/${maxAttempts - 1}) in ${backoffMs}ms: ${errText}`,
|
||||
);
|
||||
await sleep(backoffMs);
|
||||
}
|
||||
@@ -688,7 +688,7 @@ async function deliverWebReply(params: {
|
||||
);
|
||||
} catch (err) {
|
||||
whatsappOutboundLog.error(
|
||||
`Failed sending web media to ${msg.from}: ${String(err)}`,
|
||||
`Failed sending web media to ${msg.from}: ${formatError(err)}`,
|
||||
);
|
||||
replyLogger.warn({ err, mediaUrl }, "failed to send web media reply");
|
||||
if (index === 0) {
|
||||
@@ -1043,12 +1043,12 @@ export async function monitorWebProvider(
|
||||
to,
|
||||
}).catch((err) => {
|
||||
replyLogger.warn(
|
||||
{ error: String(err), storePath, sessionKey: mainKey, to },
|
||||
{ error: formatError(err), storePath, sessionKey: mainKey, to },
|
||||
"failed updating last route",
|
||||
);
|
||||
});
|
||||
backgroundTasks.add(task);
|
||||
task.finally(() => {
|
||||
void task.finally(() => {
|
||||
backgroundTasks.delete(task);
|
||||
});
|
||||
}
|
||||
@@ -1096,7 +1096,7 @@ export async function monitorWebProvider(
|
||||
})
|
||||
.catch((err) => {
|
||||
whatsappOutboundLog.error(
|
||||
`Failed sending web tool update to ${msg.from ?? conversationId}: ${String(err)}`,
|
||||
`Failed sending web tool update to ${msg.from ?? conversationId}: ${formatError(err)}`,
|
||||
);
|
||||
});
|
||||
};
|
||||
@@ -1201,7 +1201,7 @@ export async function monitorWebProvider(
|
||||
}
|
||||
} catch (err) {
|
||||
whatsappOutboundLog.error(
|
||||
`Failed sending web auto-reply to ${msg.from ?? conversationId}: ${String(err)}`,
|
||||
`Failed sending web auto-reply to ${msg.from ?? conversationId}: ${formatError(err)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1323,7 +1323,7 @@ export async function monitorWebProvider(
|
||||
try {
|
||||
await listener.close();
|
||||
} catch (err) {
|
||||
logVerbose(`Socket close failed: ${String(err)}`);
|
||||
logVerbose(`Socket close failed: ${formatError(err)}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1378,7 +1378,9 @@ export async function monitorWebProvider(
|
||||
whatsappHeartbeatLog.warn(
|
||||
`No messages received in ${minutesSinceLastMessage}m - restarting connection`,
|
||||
);
|
||||
closeListener(); // Trigger reconnect
|
||||
void closeListener().catch((err) => {
|
||||
logVerbose(`Close listener failed: ${formatError(err)}`);
|
||||
}); // Trigger reconnect
|
||||
}
|
||||
}
|
||||
}, WATCHDOG_CHECK_MS);
|
||||
@@ -1593,7 +1595,7 @@ export async function monitorWebProvider(
|
||||
heartbeatLogger.warn(
|
||||
{
|
||||
connectionId,
|
||||
error: String(err),
|
||||
error: formatError(err),
|
||||
durationMs,
|
||||
},
|
||||
"reply heartbeat failed",
|
||||
@@ -1601,7 +1603,7 @@ export async function monitorWebProvider(
|
||||
whatsappHeartbeatLog.warn(
|
||||
`heartbeat failed (${formatDuration(durationMs)})`,
|
||||
);
|
||||
return { status: "failed", reason: String(err) };
|
||||
return { status: "failed", reason: formatError(err) };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1630,7 +1632,7 @@ export async function monitorWebProvider(
|
||||
const reason = await Promise.race([
|
||||
listener.onClose?.catch((err) => {
|
||||
reconnectLogger.error(
|
||||
{ error: String(err) },
|
||||
{ error: formatError(err) },
|
||||
"listener.onClose rejected",
|
||||
);
|
||||
return { status: 500, isLoggedOut: false, error: err };
|
||||
|
||||
@@ -19,6 +19,7 @@ vi.mock("./session.js", () => {
|
||||
createWaSocket,
|
||||
waitForWaConnection,
|
||||
formatError,
|
||||
resolveWebAuthDir: () => "/tmp/wa-creds",
|
||||
WA_WEB_AUTH_DIR: "/tmp/wa-creds",
|
||||
};
|
||||
});
|
||||
|
||||
@@ -35,11 +35,12 @@ describe("web login", () => {
|
||||
|
||||
it("loginWeb waits for connection and closes", async () => {
|
||||
const sock = await createWaSocket();
|
||||
const close = vi.spyOn(sock.ws, "close");
|
||||
const waiter: typeof waitForWaConnection = vi
|
||||
.fn()
|
||||
.mockResolvedValue(undefined);
|
||||
await loginWeb(false, "web", waiter);
|
||||
await new Promise((resolve) => setTimeout(resolve, 550));
|
||||
expect(sock.ws.close).toHaveBeenCalled();
|
||||
expect(close).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,7 +8,7 @@ import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
|
||||
import {
|
||||
createWaSocket,
|
||||
formatError,
|
||||
WA_WEB_AUTH_DIR,
|
||||
resolveWebAuthDir,
|
||||
waitForWaConnection,
|
||||
} from "./session.js";
|
||||
|
||||
@@ -56,7 +56,7 @@ export async function loginWeb(
|
||||
}
|
||||
}
|
||||
if (code === DisconnectReason.loggedOut) {
|
||||
await fs.rm(WA_WEB_AUTH_DIR, { recursive: true, force: true });
|
||||
await fs.rm(resolveWebAuthDir(), { recursive: true, force: true });
|
||||
console.error(
|
||||
danger(
|
||||
"WhatsApp reported the session is logged out. Cleared cached web session; please rerun clawdis login and scan the QR again.",
|
||||
|
||||
@@ -35,10 +35,12 @@ export function resolveReconnectPolicy(
|
||||
cfg: ClawdisConfig,
|
||||
overrides?: Partial<ReconnectPolicy>,
|
||||
): ReconnectPolicy {
|
||||
const reconnectOverrides = cfg.web?.reconnect ?? {};
|
||||
const overrideConfig = overrides ?? {};
|
||||
const merged = {
|
||||
...DEFAULT_RECONNECT_POLICY,
|
||||
...(cfg.web?.reconnect ?? {}),
|
||||
...(overrides ?? {}),
|
||||
...reconnectOverrides,
|
||||
...overrideConfig,
|
||||
} as ReconnectPolicy;
|
||||
|
||||
merged.initialMs = Math.max(250, merged.initialMs);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import fsSync from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import {
|
||||
DisconnectReason,
|
||||
@@ -19,9 +20,19 @@ import type { Provider } from "../utils.js";
|
||||
import { CONFIG_DIR, ensureDir, jidToE164 } from "../utils.js";
|
||||
import { VERSION } from "../version.js";
|
||||
|
||||
export function resolveWebAuthDir() {
|
||||
return path.join(os.homedir(), ".clawdis", "credentials");
|
||||
}
|
||||
|
||||
function resolveWebCredsPath() {
|
||||
return path.join(resolveWebAuthDir(), "creds.json");
|
||||
}
|
||||
|
||||
function resolveWebCredsBackupPath() {
|
||||
return path.join(resolveWebAuthDir(), "creds.json.bak");
|
||||
}
|
||||
|
||||
export const WA_WEB_AUTH_DIR = path.join(CONFIG_DIR, "credentials");
|
||||
const WA_CREDS_PATH = path.join(WA_WEB_AUTH_DIR, "creds.json");
|
||||
const WA_CREDS_BACKUP_PATH = path.join(WA_WEB_AUTH_DIR, "creds.json.bak");
|
||||
|
||||
let credsSaveQueue: Promise<void> = Promise.resolve();
|
||||
function enqueueSaveCreds(
|
||||
@@ -50,21 +61,23 @@ function maybeRestoreCredsFromBackup(
|
||||
logger: ReturnType<typeof getChildLogger>,
|
||||
): void {
|
||||
try {
|
||||
const raw = readCredsJsonRaw(WA_CREDS_PATH);
|
||||
const credsPath = resolveWebCredsPath();
|
||||
const backupPath = resolveWebCredsBackupPath();
|
||||
const raw = readCredsJsonRaw(credsPath);
|
||||
if (raw) {
|
||||
// Validate that creds.json is parseable.
|
||||
JSON.parse(raw);
|
||||
return;
|
||||
}
|
||||
|
||||
const backupRaw = readCredsJsonRaw(WA_CREDS_BACKUP_PATH);
|
||||
const backupRaw = readCredsJsonRaw(backupPath);
|
||||
if (!backupRaw) return;
|
||||
|
||||
// Ensure backup is parseable before restoring.
|
||||
JSON.parse(backupRaw);
|
||||
fsSync.copyFileSync(WA_CREDS_BACKUP_PATH, WA_CREDS_PATH);
|
||||
fsSync.copyFileSync(backupPath, credsPath);
|
||||
logger.warn(
|
||||
{ credsPath: WA_CREDS_PATH },
|
||||
{ credsPath },
|
||||
"restored corrupted WhatsApp creds.json from backup",
|
||||
);
|
||||
} catch {
|
||||
@@ -79,11 +92,13 @@ async function safeSaveCreds(
|
||||
try {
|
||||
// Best-effort backup so we can recover after abrupt restarts.
|
||||
// Important: don't clobber a good backup with a corrupted/truncated creds.json.
|
||||
const raw = readCredsJsonRaw(WA_CREDS_PATH);
|
||||
const credsPath = resolveWebCredsPath();
|
||||
const backupPath = resolveWebCredsBackupPath();
|
||||
const raw = readCredsJsonRaw(credsPath);
|
||||
if (raw) {
|
||||
try {
|
||||
JSON.parse(raw);
|
||||
fsSync.copyFileSync(WA_CREDS_PATH, WA_CREDS_BACKUP_PATH);
|
||||
fsSync.copyFileSync(credsPath, backupPath);
|
||||
} catch {
|
||||
// keep existing backup
|
||||
}
|
||||
@@ -114,10 +129,11 @@ export async function createWaSocket(
|
||||
},
|
||||
);
|
||||
const logger = toPinoLikeLogger(baseLogger, verbose ? "info" : "silent");
|
||||
await ensureDir(WA_WEB_AUTH_DIR);
|
||||
const authDir = resolveWebAuthDir();
|
||||
await ensureDir(authDir);
|
||||
const sessionLogger = getChildLogger({ module: "web-session" });
|
||||
maybeRestoreCredsFromBackup(sessionLogger);
|
||||
const { state, saveCreds } = await useMultiFileAuthState(WA_WEB_AUTH_DIR);
|
||||
const { state, saveCreds } = await useMultiFileAuthState(authDir);
|
||||
const { version } = await fetchLatestBaileysVersion();
|
||||
const sock = makeWASocket({
|
||||
auth: {
|
||||
@@ -283,6 +299,10 @@ export function formatError(err: unknown): string {
|
||||
|
||||
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,
|
||||
@@ -300,7 +320,7 @@ export function formatError(err: unknown): string {
|
||||
if (typeof status === "number") pieces.push(`status=${status}`);
|
||||
if (boom?.error) pieces.push(boom.error);
|
||||
if (message) pieces.push(message);
|
||||
if (code !== undefined && code !== null) pieces.push(`code=${String(code)}`);
|
||||
if (codeText) pieces.push(`code=${codeText}`);
|
||||
|
||||
if (pieces.length > 0) return pieces.join(" ");
|
||||
return safeStringify(err);
|
||||
@@ -309,15 +329,17 @@ export function formatError(err: unknown): string {
|
||||
export async function webAuthExists() {
|
||||
const sessionLogger = getChildLogger({ module: "web-session" });
|
||||
maybeRestoreCredsFromBackup(sessionLogger);
|
||||
const authDir = resolveWebAuthDir();
|
||||
const credsPath = resolveWebCredsPath();
|
||||
try {
|
||||
await fs.access(WA_WEB_AUTH_DIR);
|
||||
await fs.access(authDir);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const stats = await fs.stat(WA_CREDS_PATH);
|
||||
const stats = await fs.stat(credsPath);
|
||||
if (!stats.isFile() || stats.size <= 1) return false;
|
||||
const raw = await fs.readFile(WA_CREDS_PATH, "utf-8");
|
||||
const raw = await fs.readFile(credsPath, "utf-8");
|
||||
JSON.parse(raw);
|
||||
return true;
|
||||
} catch {
|
||||
@@ -331,7 +353,7 @@ export async function logoutWeb(runtime: RuntimeEnv = defaultRuntime) {
|
||||
runtime.log(info("No WhatsApp Web session found; nothing to delete."));
|
||||
return false;
|
||||
}
|
||||
await fs.rm(WA_WEB_AUTH_DIR, { recursive: true, force: true });
|
||||
await fs.rm(resolveWebAuthDir(), { recursive: true, force: true });
|
||||
// Also drop session store to clear lingering per-sender state after logout.
|
||||
await fs.rm(resolveDefaultSessionStorePath(), { force: true });
|
||||
runtime.log(success("Cleared WhatsApp Web credentials."));
|
||||
@@ -341,10 +363,11 @@ export async function logoutWeb(runtime: RuntimeEnv = defaultRuntime) {
|
||||
export function readWebSelfId() {
|
||||
// Read the cached WhatsApp Web identity (jid + E.164) from disk if present.
|
||||
try {
|
||||
if (!fsSync.existsSync(WA_CREDS_PATH)) {
|
||||
const credsPath = resolveWebCredsPath();
|
||||
if (!fsSync.existsSync(credsPath)) {
|
||||
return { e164: null, jid: null } as const;
|
||||
}
|
||||
const raw = fsSync.readFileSync(WA_CREDS_PATH, "utf-8");
|
||||
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;
|
||||
@@ -360,7 +383,7 @@ export function readWebSelfId() {
|
||||
*/
|
||||
export function getWebAuthAgeMs(): number | null {
|
||||
try {
|
||||
const stats = fsSync.statSync(WA_CREDS_PATH);
|
||||
const stats = fsSync.statSync(resolveWebCredsPath());
|
||||
return Date.now() - stats.mtimeMs;
|
||||
} catch {
|
||||
return null;
|
||||
|
||||
@@ -20,7 +20,7 @@ if (!(globalThis as Record<symbol, unknown>)[CONFIG_KEY]) {
|
||||
(globalThis as Record<symbol, unknown>)[CONFIG_KEY] = () => DEFAULT_CONFIG;
|
||||
}
|
||||
|
||||
export function setLoadConfigMock(fn: (() => unknown) | unknown) {
|
||||
export function setLoadConfigMock(fn: unknown) {
|
||||
(globalThis as Record<symbol, unknown>)[CONFIG_KEY] =
|
||||
typeof fn === "function" ? fn : () => fn;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user