Add web provider inbound monitor with auto-replies
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import path from "node:path";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import {
|
||||
DisconnectReason,
|
||||
fetchLatestBaileysVersion,
|
||||
@@ -7,10 +8,11 @@ import {
|
||||
makeWASocket,
|
||||
useMultiFileAuthState,
|
||||
} from "baileys";
|
||||
import type { proto } from "baileys";
|
||||
import pino from "pino";
|
||||
import qrcode from "qrcode-terminal";
|
||||
import { danger, info, logVerbose, success } from "./globals.js";
|
||||
import { ensureDir, toWhatsappJid } from "./utils.js";
|
||||
import { ensureDir, jidToE164, toWhatsappJid } from "./utils.js";
|
||||
|
||||
const WA_WEB_AUTH_DIR = path.join(os.homedir(), ".warelay", "waweb");
|
||||
|
||||
@@ -25,6 +27,7 @@ export async function createWaSocket(printQr: boolean, verbose: boolean) {
|
||||
keys: makeCacheableSignalKeyStore(state.keys, logger),
|
||||
},
|
||||
version,
|
||||
logger,
|
||||
printQRInTerminal: false,
|
||||
browser: ["Warelay", "CLI", "1.0.0"],
|
||||
syncFullHistory: false,
|
||||
@@ -32,25 +35,27 @@ export async function createWaSocket(printQr: boolean, verbose: boolean) {
|
||||
});
|
||||
|
||||
sock.ev.on("creds.update", saveCreds);
|
||||
sock.ev.on("connection.update", (update: Partial<import("baileys").ConnectionState>) => {
|
||||
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 code = (lastDisconnect?.error as { output?: { statusCode?: number } })
|
||||
?.output?.statusCode;
|
||||
if (code === DisconnectReason.loggedOut) {
|
||||
console.error(
|
||||
danger("WhatsApp session logged out. Run: warelay web:login"),
|
||||
);
|
||||
sock.ev.on(
|
||||
"connection.update",
|
||||
(update: Partial<import("baileys").ConnectionState>) => {
|
||||
const { connection, lastDisconnect, qr } = update;
|
||||
if (qr && printQr) {
|
||||
console.log("Scan this QR in WhatsApp (Linked Devices):");
|
||||
qrcode.generate(qr, { small: true });
|
||||
}
|
||||
}
|
||||
if (connection === "open" && verbose) {
|
||||
console.log(success("WhatsApp Web connected."));
|
||||
}
|
||||
});
|
||||
if (connection === "close") {
|
||||
const status = getStatusCode(lastDisconnect?.error);
|
||||
if (status === DisconnectReason.loggedOut) {
|
||||
console.error(
|
||||
danger("WhatsApp session logged out. Run: warelay web:login"),
|
||||
);
|
||||
}
|
||||
}
|
||||
if (connection === "open" && verbose) {
|
||||
console.log(success("WhatsApp Web connected."));
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return sock;
|
||||
}
|
||||
@@ -104,12 +109,44 @@ export async function sendMessageWeb(
|
||||
}
|
||||
}
|
||||
|
||||
export async function loginWeb(verbose: boolean) {
|
||||
export async function loginWeb(
|
||||
verbose: boolean,
|
||||
waitForConnection: typeof waitForWaConnection = waitForWaConnection,
|
||||
) {
|
||||
const sock = await createWaSocket(true, verbose);
|
||||
console.log(info("Waiting for WhatsApp connection..."));
|
||||
try {
|
||||
await waitForWaConnection(sock);
|
||||
await waitForConnection(sock);
|
||||
console.log(success("✅ Linked! Credentials saved for future sends."));
|
||||
} catch (err) {
|
||||
const code =
|
||||
(err as { error?: { output?: { statusCode?: number } } })?.error?.output
|
||||
?.statusCode ??
|
||||
(err as { output?: { statusCode?: number } })?.output?.statusCode;
|
||||
if (code === 515) {
|
||||
console.log(
|
||||
info(
|
||||
"WhatsApp asked for a restart after pairing (code 515); creds are saved. You can now send with provider=web.",
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (code === DisconnectReason.loggedOut) {
|
||||
await fs.rm(WA_WEB_AUTH_DIR, { recursive: true, force: true });
|
||||
console.error(
|
||||
danger(
|
||||
"WhatsApp reported the session is logged out. Cleared cached web session; please rerun warelay web:login and scan the QR again.",
|
||||
),
|
||||
);
|
||||
throw new Error("Session logged out; cache cleared. Re-run web:login.");
|
||||
}
|
||||
const formatted = formatError(err);
|
||||
console.error(
|
||||
danger(
|
||||
`WhatsApp Web connection ended before fully opening. ${formatted}`,
|
||||
),
|
||||
);
|
||||
throw new Error(formatted);
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
try {
|
||||
@@ -122,3 +159,111 @@ export async function loginWeb(verbose: boolean) {
|
||||
}
|
||||
|
||||
export { WA_WEB_AUTH_DIR };
|
||||
|
||||
export type WebInboundMessage = {
|
||||
id?: string;
|
||||
from: string;
|
||||
to: string;
|
||||
body: string;
|
||||
pushName?: string;
|
||||
timestamp?: number;
|
||||
sendComposing: () => Promise<void>;
|
||||
reply: (text: string) => Promise<void>;
|
||||
};
|
||||
|
||||
export async function monitorWebInbox(options: {
|
||||
verbose: boolean;
|
||||
onMessage: (msg: WebInboundMessage) => Promise<void>;
|
||||
}) {
|
||||
const sock = await createWaSocket(false, options.verbose);
|
||||
await waitForWaConnection(sock);
|
||||
const selfJid = sock.user?.id;
|
||||
const selfE164 = selfJid ? jidToE164(selfJid) : null;
|
||||
const seen = new Set<string>();
|
||||
|
||||
sock.ev.on("messages.upsert", async (upsert) => {
|
||||
if (upsert.type !== "notify") return;
|
||||
for (const msg of upsert.messages) {
|
||||
const id = msg.key?.id ?? undefined;
|
||||
// De-dupe on message id; Baileys can emit retries.
|
||||
if (id && seen.has(id)) continue;
|
||||
if (id) seen.add(id);
|
||||
if (msg.key?.fromMe) continue;
|
||||
const remoteJid = msg.key?.remoteJid;
|
||||
if (!remoteJid) continue;
|
||||
// Ignore status/broadcast traffic; we only care about direct chats.
|
||||
if (remoteJid.endsWith("@status") || remoteJid.endsWith("@broadcast"))
|
||||
continue;
|
||||
const from = jidToE164(remoteJid);
|
||||
if (!from) continue;
|
||||
const body = extractText(msg.message);
|
||||
if (!body) continue;
|
||||
const chatJid = remoteJid;
|
||||
const sendComposing = async () => {
|
||||
try {
|
||||
await sock.sendPresenceUpdate("composing", chatJid);
|
||||
} catch (err) {
|
||||
logVerbose(`Presence update failed: ${String(err)}`);
|
||||
}
|
||||
};
|
||||
const reply = async (text: string) => {
|
||||
await sock.sendMessage(chatJid, { text });
|
||||
};
|
||||
const timestamp = msg.messageTimestamp
|
||||
? Number(msg.messageTimestamp) * 1000
|
||||
: undefined;
|
||||
try {
|
||||
await options.onMessage({
|
||||
id,
|
||||
from,
|
||||
to: selfE164 ?? "me",
|
||||
body,
|
||||
pushName: msg.pushName ?? undefined,
|
||||
timestamp,
|
||||
sendComposing,
|
||||
reply,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(danger(`Failed handling inbound web message: ${String(err)}`));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
close: async () => {
|
||||
try {
|
||||
sock.ws?.close();
|
||||
} catch (err) {
|
||||
logVerbose(`Socket close failed: ${String(err)}`);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function extractText(message: proto.IMessage | undefined): string | undefined {
|
||||
if (!message) return undefined;
|
||||
if (typeof message.conversation === "string" && message.conversation.trim()) {
|
||||
return message.conversation.trim();
|
||||
}
|
||||
const extended = message.extendedTextMessage?.text;
|
||||
if (extended?.trim()) return extended.trim();
|
||||
const caption = message.imageMessage?.caption ?? message.videoMessage?.caption;
|
||||
if (caption?.trim()) return caption.trim();
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function getStatusCode(err: unknown) {
|
||||
return (
|
||||
(err as { output?: { statusCode?: number } })?.output?.statusCode ??
|
||||
(err as { status?: number })?.status
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user