diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6d294c755..fd20904c9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,13 +34,12 @@ jobs: - name: Install dependencies env: CI: true - NPM_CONFIG_ENGINE_STRICT: "false" run: | export PATH="$NODE_BIN:$PATH" which node node -v pnpm -v - pnpm install --no-frozen-lockfile --ignore-scripts=false + pnpm install --ignore-scripts=false --config.engine-strict=false --config.enable-pre-post-scripts=true || pnpm install --ignore-scripts=false --config.engine-strict=false --config.enable-pre-post-scripts=true - name: Lint run: pnpm lint diff --git a/src/auto-reply/claude.ts b/src/auto-reply/claude.ts index 76160792b..f6f174e88 100644 --- a/src/auto-reply/claude.ts +++ b/src/auto-reply/claude.ts @@ -104,7 +104,9 @@ export function parseClaudeJson( try { const parsed = JSON.parse(candidate); if (firstParsed === undefined) firstParsed = parsed; - let validation; + let validation: + | z.SafeParseReturnType> + | { success: false }; try { validation = ClaudeJsonSchema.safeParse(parsed); } catch { @@ -131,7 +133,9 @@ export function parseClaudeJson( } } if (firstParsed !== undefined) { - let validation; + let validation: + | z.SafeParseReturnType> + | { success: false }; try { validation = ClaudeJsonSchema.safeParse(firstParsed); } catch { diff --git a/src/config/config.ts b/src/config/config.ts index e89c376b9..bf405a40f 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -49,7 +49,9 @@ const ReplySchema = z mediaUrl: z.string().optional(), session: z .object({ - scope: z.union([z.literal("per-sender"), z.literal("global")]).optional(), + scope: z + .union([z.literal("per-sender"), z.literal("global")]) + .optional(), resetTriggers: z.array(z.string()).optional(), idleMinutes: z.number().int().positive().optional(), store: z.string().optional(), @@ -70,7 +72,8 @@ const ReplySchema = z .refine( (val) => (val.mode === "text" ? Boolean(val.text) : Boolean(val.command)), { - message: "reply.text is required for mode=text; reply.command is required for mode=command", + message: + "reply.text is required for mode=text; reply.command is required for mode=command", }, ); @@ -93,7 +96,9 @@ export function loadConfig(): WarelayConfig { const validated = WarelaySchema.safeParse(parsed); if (!validated.success) { console.error("Invalid warelay config:"); - validated.error.issues.forEach((iss) => console.error(`- ${iss.path.join(".")}: ${iss.message}`)); + for (const iss of validated.error.issues) { + console.error(`- ${iss.path.join(".")}: ${iss.message}`); + } return {}; } return validated.data as WarelayConfig; diff --git a/src/provider-web.test.ts b/src/provider-web.test.ts index c9aae12e1..d69448ab2 100644 --- a/src/provider-web.test.ts +++ b/src/provider-web.test.ts @@ -38,15 +38,15 @@ vi.mock("qrcode-terminal", () => ({ generate: vi.fn(), })); +import { monitorWebProvider } from "./index.js"; import { createWaSocket, loginWeb, - monitorWebInbox, logWebSelfId, + monitorWebInbox, sendMessageWeb, waitForWaConnection, } from "./provider-web.js"; -import { monitorWebProvider } from "./index.js"; const baileys = (await import( "@whiskeysockets/baileys" @@ -87,8 +87,9 @@ describe("provider-web", () => { expect.objectContaining({ printQRInTerminal: false }), ); const passed = makeWASocket.mock.calls[0][0]; - const passedLogger = (passed as { logger?: { level?: string; trace?: unknown } }) - .logger; + const passedLogger = ( + passed as { logger?: { level?: string; trace?: unknown } } + ).logger; expect(passedLogger?.level).toBe("silent"); expect(typeof passedLogger?.trace).toBe("function"); const sock = getLastSocket(); @@ -173,10 +174,18 @@ describe("provider-web", () => { expect.objectContaining({ body: "ping", from: "+999", to: "+123" }), ); expect(sock.readMessages).toHaveBeenCalledWith([ - { remoteJid: "999@s.whatsapp.net", id: "abc", participant: undefined, fromMe: false }, + { + remoteJid: "999@s.whatsapp.net", + id: "abc", + participant: undefined, + fromMe: false, + }, ]); expect(sock.sendPresenceUpdate).toHaveBeenCalledWith("available"); - expect(sock.sendPresenceUpdate).toHaveBeenCalledWith("composing", "999@s.whatsapp.net"); + expect(sock.sendPresenceUpdate).toHaveBeenCalledWith( + "composing", + "999@s.whatsapp.net", + ); expect(sock.sendMessage).toHaveBeenCalledWith("999@s.whatsapp.net", { text: "pong", }); @@ -263,23 +272,27 @@ describe("provider-web", () => { mediaUrl: "https://example.com/img.png", }); - let capturedOnMessage: ((msg: any) => Promise) | undefined; - const listenerFactory = async (opts: { onMessage: (msg: any) => Promise }) => { + let capturedOnMessage: + | ((msg: import("./provider-web.js").WebInboundMessage) => Promise) + | undefined; + const listenerFactory = async (opts: { + onMessage: ( + msg: import("./provider-web.js").WebInboundMessage, + ) => Promise; + }) => { capturedOnMessage = opts.onMessage; return { close: vi.fn() }; }; - const fetchMock = vi - .spyOn(global as any, "fetch") - .mockResolvedValue({ - ok: true, - body: true, - arrayBuffer: async () => new ArrayBuffer(1024), - headers: { get: () => "image/png" }, - status: 200, - } as any); + const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue({ + ok: true, + body: true, + arrayBuffer: async () => new ArrayBuffer(1024), + headers: { get: () => "image/png" }, + status: 200, + } as Response); - await monitorWebProvider(false, listenerFactory as any, false, resolver); + await monitorWebProvider(false, listenerFactory, false, resolver); expect(capturedOnMessage).toBeDefined(); await capturedOnMessage?.({ @@ -303,9 +316,7 @@ describe("provider-web", () => { .mockReturnValue(true as never); const readSpy = vi .spyOn(fsSync, "readFileSync") - .mockReturnValue( - JSON.stringify({ me: { id: "12345@s.whatsapp.net" } }), - ); + .mockReturnValue(JSON.stringify({ me: { id: "12345@s.whatsapp.net" } })); const runtime = { log: vi.fn(), error: vi.fn(), diff --git a/src/provider-web.ts b/src/provider-web.ts index 6b86e3454..e065a2a86 100644 --- a/src/provider-web.ts +++ b/src/provider-web.ts @@ -1,27 +1,28 @@ -import fs from "node:fs/promises"; import fsSync from "node:fs"; +import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import type { proto } from "@whiskeysockets/baileys"; import { + type AnyMessageContent, DisconnectReason, + downloadMediaMessage, fetchLatestBaileysVersion, makeCacheableSignalKeyStore, makeWASocket, useMultiFileAuthState, - downloadMediaMessage, - type AnyMessageContent, + type WAMessage, } from "@whiskeysockets/baileys"; import pino from "pino"; import qrcode from "qrcode-terminal"; -import { danger, info, isVerbose, logVerbose, success, warn } from "./globals.js"; -import { ensureDir, jidToE164, toWhatsappJid } from "./utils.js"; -import type { Provider } from "./utils.js"; -import { waitForever } from "./cli/wait.js"; import { getReplyFromConfig } from "./auto-reply/reply.js"; -import { defaultRuntime, type RuntimeEnv } from "./runtime.js"; -import { logInfo, logWarn } from "./logger.js"; +import { waitForever } from "./cli/wait.js"; +import { danger, info, isVerbose, logVerbose, success } from "./globals.js"; +import { logInfo } from "./logger.js"; import { saveMediaBuffer } from "./media/store.js"; +import { defaultRuntime, type RuntimeEnv } from "./runtime.js"; +import type { Provider } from "./utils.js"; +import { ensureDir, jidToE164, toWhatsappJid } from "./utils.js"; function formatDuration(ms: number) { return ms >= 1000 ? `${(ms / 1000).toFixed(2)}s` : `${ms}ms`; @@ -231,7 +232,11 @@ export type WebInboundMessage = { timestamp?: number; sendComposing: () => Promise; reply: (text: string) => Promise; - sendMedia: (payload: { image: Buffer; caption?: string; mimetype?: string }) => Promise; + sendMedia: (payload: { + image: Buffer; + caption?: string; + mimetype?: string; + }) => Promise; mediaPath?: string; mediaType?: string; mediaUrl?: string; @@ -248,7 +253,9 @@ export async function monitorWebInbox(options: { await sock.sendPresenceUpdate("available"); if (isVerbose()) logVerbose("Sent global 'available' presence on connect"); } catch (err) { - logVerbose(`Failed to send 'available' presence on connect: ${String(err)}`); + logVerbose( + `Failed to send 'available' presence on connect: ${String(err)}`, + ); } const selfJid = sock.user?.id; const selfE164 = selfJid ? jidToE164(selfJid) : null; @@ -275,7 +282,9 @@ export async function monitorWebInbox(options: { ]); if (isVerbose()) { const suffix = participant ? ` (participant ${participant})` : ""; - logVerbose(`Marked message ${id} as read for ${remoteJid}${suffix}`); + logVerbose( + `Marked message ${id} as read for ${remoteJid}${suffix}`, + ); } } catch (err) { logVerbose(`Failed to mark message ${id} read: ${String(err)}`); @@ -389,16 +398,12 @@ export async function monitorWebProvider( }, ); if (!replyResult || (!replyResult.text && !replyResult.mediaUrl)) { - logVerbose( - "Skipping auto-reply: no text/media returned from resolver", - ); + logVerbose("Skipping auto-reply: no text/media returned from resolver"); return; } try { if (replyResult.mediaUrl) { - logVerbose( - `Web auto-reply media detected: ${replyResult.mediaUrl}`, - ); + logVerbose(`Web auto-reply media detected: ${replyResult.mediaUrl}`); try { const media = await loadWebMedia(replyResult.mediaUrl); if (isVerbose()) { @@ -417,9 +422,7 @@ export async function monitorWebProvider( ); } catch (err) { console.error( - danger( - `Failed sending web media to ${msg.from}: ${String(err)}`, - ), + danger(`Failed sending web media to ${msg.from}: ${String(err)}`), ); if (replyResult.text) { await msg.reply(replyResult.text); @@ -499,9 +502,7 @@ export function logWebSelfId( e164 || jid ? `${e164 ?? "unknown"}${jid ? ` (jid ${jid})` : ""}` : "unknown"; - const prefix = includeProviderPrefix - ? "Web Provider: " - : ""; + const prefix = includeProviderPrefix ? "Web Provider: " : ""; runtime.log(info(`${prefix}${details}`)); } @@ -526,7 +527,9 @@ function extractText(message: proto.IMessage | undefined): string | undefined { return undefined; } -function extractMediaPlaceholder(message: proto.IMessage | undefined): string | undefined { +function extractMediaPlaceholder( + message: proto.IMessage | undefined, +): string | undefined { if (!message) return undefined; if (message.imageMessage) return ""; if (message.videoMessage) return ""; @@ -559,10 +562,15 @@ async function downloadInboundMedia( return undefined; } try { - const buffer = (await downloadMediaMessage(msg as any, "buffer", {}, { - reuploadRequest: sock.updateMediaMessage, - logger: (sock as { logger?: unknown })?.logger as any, - })) as Buffer; + const buffer = (await downloadMediaMessage( + msg as WAMessage, + "buffer", + {}, + { + reuploadRequest: sock.updateMediaMessage, + logger: sock.logger, + }, + )) as Buffer; return { buffer, mimetype }; } catch (err) { logVerbose(`downloadMediaMessage failed: ${String(err)}`); @@ -586,20 +594,21 @@ async function loadWebMedia( if (array.length > MAX_WEB_BYTES) { throw new Error( `Media exceeds ${Math.floor(MAX_WEB_BYTES / (1024 * 1024))}MB limit (got ${( - array.length / - (1024 * 1024) + array.length / (1024 * 1024) ).toFixed(1)}MB)`, ); } - return { buffer: array, contentType: res.headers.get("content-type") ?? undefined }; + return { + buffer: array, + contentType: res.headers.get("content-type") ?? undefined, + }; } // Local path const data = await fs.readFile(mediaUrl); if (data.length > MAX_WEB_BYTES) { throw new Error( `Media exceeds ${Math.floor(MAX_WEB_BYTES / (1024 * 1024))}MB limit (got ${( - data.length / - (1024 * 1024) + data.length / (1024 * 1024) ).toFixed(1)}MB)`, ); }