mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-09 03:30:48 +08:00
623 lines
22 KiB
TypeScript
623 lines
22 KiB
TypeScript
import fs from "node:fs";
|
||
import path from "node:path";
|
||
import process from "node:process";
|
||
import dns from "node:dns/promises";
|
||
|
||
import makeWASocket, {
|
||
Browsers,
|
||
DisconnectReason,
|
||
areJidsSameUser,
|
||
fetchLatestBaileysVersion,
|
||
jidNormalizedUser,
|
||
proto,
|
||
} from "@whiskeysockets/baileys";
|
||
import { HttpsProxyAgent } from "https-proxy-agent";
|
||
|
||
import { loadAuthState } from "./auth";
|
||
import { printQrToTerminal } from "./qr";
|
||
|
||
type Json = Record<string, unknown>;
|
||
|
||
const LOCAL_BASE_URL = (process.env.AIA_GATEWAY_BASE_URL || "http://127.0.0.1:8787").trim();
|
||
const ACCOUNT_ID = (process.env.AIA_WHATSAPP_ACCOUNT_ID || "wa-default").trim();
|
||
const STATE_DIR = (process.env.OCLAW_STATE_DIR || path.resolve(process.cwd(), "state")).trim();
|
||
const LOGIN_ONLY = process.argv.includes("--login") || String(process.env.AIA_WHATSAPP_LOGIN_ONLY || "").trim() === "1";
|
||
const VERBOSE = process.argv.includes("--verbose") || String(process.env.AIA_WHATSAPP_VERBOSE || "").trim() === "1";
|
||
const PROXY_URL = (
|
||
process.env.AIA_WHATSAPP_PROXY_URL ||
|
||
process.env.HTTPS_PROXY ||
|
||
process.env.HTTP_PROXY ||
|
||
process.env.https_proxy ||
|
||
process.env.http_proxy ||
|
||
""
|
||
).trim();
|
||
function log(msg: string): void {
|
||
process.stdout.write(`${new Date().toISOString()} [baileys-whatsapp] ${msg}\n`);
|
||
}
|
||
|
||
function ensureDir(dir: string): void {
|
||
fs.mkdirSync(dir, { recursive: true });
|
||
}
|
||
|
||
function pickText(m: proto.IMessage | null | undefined): string {
|
||
const u = unwrapMessage(m);
|
||
if (!u) return "";
|
||
const c = (u.conversation || "").trim();
|
||
if (c) return c;
|
||
const ext = (u.extendedTextMessage?.text || "").trim();
|
||
if (ext) return ext;
|
||
const imgCap = (u.imageMessage?.caption || "").trim();
|
||
if (imgCap) return imgCap;
|
||
const vidCap = (u.videoMessage?.caption || "").trim();
|
||
if (vidCap) return vidCap;
|
||
return "";
|
||
}
|
||
|
||
function unwrapMessage(m: proto.IMessage | null | undefined): proto.IMessage | null {
|
||
if (!m) return null;
|
||
const nested =
|
||
m.ephemeralMessage?.message ||
|
||
m.viewOnceMessage?.message ||
|
||
m.viewOnceMessageV2?.message ||
|
||
m.documentWithCaptionMessage?.message ||
|
||
m.editedMessage?.message ||
|
||
null;
|
||
if (nested) return unwrapMessage(nested);
|
||
return m;
|
||
}
|
||
|
||
function messageContextInfo(m: proto.IMessage | null | undefined): proto.IContextInfo | null | undefined {
|
||
const u = unwrapMessage(m);
|
||
if (!u) return null;
|
||
const top = (u as proto.IMessage & { messageContextInfo?: proto.IMessageContextInfo }).messageContextInfo;
|
||
const fromTop = top?.mentionedJid?.length ? top : null;
|
||
return (
|
||
(fromTop as unknown as proto.IContextInfo) ||
|
||
u.extendedTextMessage?.contextInfo ||
|
||
u.imageMessage?.contextInfo ||
|
||
u.videoMessage?.contextInfo ||
|
||
u.documentMessage?.contextInfo ||
|
||
u.buttonsResponseMessage?.contextInfo ||
|
||
u.listResponseMessage?.contextInfo ||
|
||
u.templateButtonReplyMessage?.contextInfo ||
|
||
null
|
||
);
|
||
}
|
||
|
||
function extractMentionsFromUpsert(msg: proto.IWebMessageInfo): string[] {
|
||
const fromBody = extractMentions(msg.message);
|
||
const outer = (msg as proto.IWebMessageInfo & { messageContextInfo?: { mentionedJid?: string[] } })
|
||
.messageContextInfo?.mentionedJid;
|
||
const outerList = Array.isArray(outer) ? outer.map((j) => String(j || "").trim()).filter(Boolean) : [];
|
||
const merged = [...fromBody, ...outerList];
|
||
const seen = new Set<string>();
|
||
const out: string[] = [];
|
||
for (const j of merged) {
|
||
const key = jidPhone(j) || j;
|
||
if (seen.has(key)) continue;
|
||
seen.add(key);
|
||
out.push(j);
|
||
}
|
||
return out;
|
||
}
|
||
|
||
function jidPhone(jid: string): string {
|
||
const head = String(jid || "").split("@")[0]?.split(":")[0] || "";
|
||
return head.replace(/\D/g, "");
|
||
}
|
||
|
||
function jidsSameUser(a: string, b: string): boolean {
|
||
const na = jidNormalizedUser(String(a || "").trim());
|
||
const nb = jidNormalizedUser(String(b || "").trim());
|
||
if (na && nb && na === nb) return true;
|
||
const pa = jidPhone(a);
|
||
const pb = jidPhone(b);
|
||
return pa.length >= 6 && pa === pb;
|
||
}
|
||
|
||
function extractMentions(m: proto.IMessage | null | undefined): string[] {
|
||
const ctx = messageContextInfo(m);
|
||
const raw = ctx?.mentionedJid;
|
||
if (!Array.isArray(raw)) return [];
|
||
return raw.map((j) => String(j || "").trim()).filter(Boolean);
|
||
}
|
||
|
||
function extractQuoteContext(m: proto.IMessage | null | undefined): { participant: string; stanzaId: string } {
|
||
const ctx = messageContextInfo(m);
|
||
return {
|
||
participant: String(ctx?.participant || "").trim(),
|
||
stanzaId: String(ctx?.stanzaId || "").trim(),
|
||
};
|
||
}
|
||
|
||
function resolveSenderJid(key: proto.IMessageKey): string {
|
||
const participant = String(key.participant || "").trim();
|
||
const participantAlt = String((key as any).participantAlt || "").trim();
|
||
if (participantAlt && participant.toLowerCase().endsWith("@lid")) {
|
||
return jidNormalizedUser(participantAlt);
|
||
}
|
||
if (participant) return jidNormalizedUser(participant);
|
||
return "";
|
||
}
|
||
|
||
function messageMentionsBot(
|
||
sock: ReturnType<typeof makeWASocket> | null,
|
||
mentions: string[],
|
||
botJid: string,
|
||
): boolean {
|
||
const bot = String(botJid || "").trim();
|
||
if (!bot || !mentions.length) return false;
|
||
const lidMapping = (sock as any)?.signalRepository?.lidMapping;
|
||
for (const m of mentions) {
|
||
const mention = String(m || "").trim();
|
||
if (!mention) continue;
|
||
try {
|
||
if (areJidsSameUser(mention, bot)) return true;
|
||
} catch {
|
||
// ignore
|
||
}
|
||
if (jidsSameUser(mention, bot)) return true;
|
||
if (lidMapping && typeof lidMapping.getPNForLID === "function" && mention.toLowerCase().endsWith("@lid")) {
|
||
try {
|
||
const pn = lidMapping.getPNForLID(mention);
|
||
if (pn && (areJidsSameUser(String(pn), bot) || jidsSameUser(String(pn), bot))) return true;
|
||
} catch {
|
||
// ignore
|
||
}
|
||
}
|
||
if (lidMapping && typeof lidMapping.getLIDForPN === "function" && bot.includes("@s.whatsapp")) {
|
||
try {
|
||
const lid = lidMapping.getLIDForPN(bot);
|
||
if (lid && (areJidsSameUser(mention, String(lid)) || jidsSameUser(mention, String(lid)))) return true;
|
||
} catch {
|
||
// ignore
|
||
}
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
function isStatusOrBroadcastJid(jid: string): boolean {
|
||
const low = String(jid || "").toLowerCase();
|
||
return low === "status@broadcast" || low.endsWith("@broadcast");
|
||
}
|
||
|
||
function buildInboundPayload(params: {
|
||
chatId: string;
|
||
userId: string;
|
||
text: string;
|
||
raw: unknown;
|
||
isGroup: boolean;
|
||
mentions: string[];
|
||
groupName?: string;
|
||
botJid?: string;
|
||
mentionsBot?: boolean;
|
||
}): Json {
|
||
const metadata: Json = {
|
||
source: "whatsapp_baileys",
|
||
raw: params.raw,
|
||
mentions_bot: params.mentionsBot === true,
|
||
};
|
||
if (params.groupName) metadata.group_name = params.groupName;
|
||
if (params.botJid) metadata.bot_jid = params.botJid;
|
||
if (params.mentions.length) metadata.mentioned_jids = params.mentions;
|
||
return {
|
||
channel: "whatsapp",
|
||
account_id: ACCOUNT_ID,
|
||
user_id: params.userId,
|
||
chat_id: params.chatId,
|
||
text: params.text,
|
||
is_group: params.isGroup,
|
||
mentions: params.mentions,
|
||
metadata,
|
||
};
|
||
}
|
||
|
||
function decodeBase64Payload(raw: string): { mime: string; data: Buffer } | null {
|
||
const s = String(raw || "").trim();
|
||
if (!s) return null;
|
||
let mime = "application/octet-stream";
|
||
let payload = s;
|
||
const m = s.match(/^data:([^;,]+);base64,(.*)$/i);
|
||
if (m) {
|
||
mime = String(m[1] || mime).trim() || mime;
|
||
payload = String(m[2] || "").trim();
|
||
}
|
||
try {
|
||
const data = Buffer.from(payload.replace(/\s+/g, ""), "base64");
|
||
if (!data.length) return null;
|
||
return { mime, data };
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
function readReplyMetadata(reply: Json): Json {
|
||
const meta = (reply as any).metadata;
|
||
return meta && typeof meta === "object" ? (meta as Json) : {};
|
||
}
|
||
|
||
function readMentionJids(meta: Json): string[] {
|
||
const raw = (meta as any).mention_jids ?? (meta as any).mentionJids;
|
||
if (!Array.isArray(raw)) {
|
||
const single = String((meta as any).reply_to_user_id || (meta as any).replyToUserId || "").trim();
|
||
return single ? [single] : [];
|
||
}
|
||
return raw.map((j) => String(j || "").trim()).filter(Boolean);
|
||
}
|
||
|
||
function buildMentionPrefix(mentionJids: string[]): string {
|
||
const tags = mentionJids
|
||
.map((j) => {
|
||
const user = String(j || "").split("@")[0]?.trim();
|
||
return user ? `@${user}` : "";
|
||
})
|
||
.filter(Boolean);
|
||
return tags.length ? `${tags.join(" ")} ` : "";
|
||
}
|
||
|
||
function buildQuotedMessage(params: {
|
||
chatId: string;
|
||
stanzaId: string;
|
||
participant: string;
|
||
quoteText: string;
|
||
}): proto.IWebMessageInfo | undefined {
|
||
const stanzaId = String(params.stanzaId || "").trim();
|
||
const chatId = String(params.chatId || "").trim();
|
||
if (!stanzaId || !chatId) return undefined;
|
||
const participant = String(params.participant || "").trim();
|
||
const quoteText = String(params.quoteText || "").trim() || "...";
|
||
return {
|
||
key: {
|
||
remoteJid: chatId,
|
||
fromMe: false,
|
||
id: stanzaId,
|
||
...(participant ? { participant } : {}),
|
||
},
|
||
message: {
|
||
conversation: quoteText,
|
||
},
|
||
} as proto.IWebMessageInfo;
|
||
}
|
||
|
||
function buildTextSendOptions(params: {
|
||
deliverTo: string;
|
||
text: string;
|
||
reply: Json;
|
||
}): { content: { text: string; mentions?: string[] }; quoted?: proto.IWebMessageInfo } {
|
||
const meta = readReplyMetadata(params.reply);
|
||
const mentionJids = readMentionJids(meta);
|
||
const body = String(params.text || "").trim();
|
||
const prefix = mentionJids.length ? buildMentionPrefix(mentionJids) : "";
|
||
const content: { text: string; mentions?: string[] } = { text: prefix ? `${prefix}${body}` : body };
|
||
if (mentionJids.length) content.mentions = mentionJids;
|
||
|
||
const quoted = buildQuotedMessage({
|
||
chatId: String((meta as any).quote_remote_jid || (meta as any).quoteRemoteJid || params.deliverTo).trim(),
|
||
stanzaId: String((meta as any).quote_stanza_id || (meta as any).quoteStanzaId || "").trim(),
|
||
participant: String((meta as any).quote_participant || (meta as any).quoteParticipant || "").trim(),
|
||
quoteText: String((meta as any).quote_text || (meta as any).quoteText || "").trim(),
|
||
});
|
||
|
||
return quoted ? { content, quoted } : { content };
|
||
}
|
||
|
||
function shouldSendOutboundText(text: string): boolean {
|
||
const t = String(text || "").trim();
|
||
if (!t) return false;
|
||
const normalized = t.toLowerCase().replace(/(/g, "(").replace(/)/g, ")");
|
||
if (normalized === "(silent)" || normalized === "[silent]" || normalized === "no_reply" || normalized === "no reply" || normalized === "静默") {
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
async function sendReplyWithAttachments(params: {
|
||
sock: ReturnType<typeof makeWASocket> | null;
|
||
deliverTo: string;
|
||
text: string;
|
||
reply: Json;
|
||
}): Promise<void> {
|
||
const s = params.sock;
|
||
if (!s) return;
|
||
const reply = params.reply || {};
|
||
const outText = String(params.text || "").trim();
|
||
const mediaPath = String((reply as any).media_path || (reply as any).mediaPath || "").trim();
|
||
const mediaUrl = String((reply as any).media_url || (reply as any).mediaUrl || "").trim();
|
||
const attachments = Array.isArray((reply as any).attachments) ? ((reply as any).attachments as Json[]) : [];
|
||
const textOpts = buildTextSendOptions({ deliverTo: params.deliverTo, text: outText, reply });
|
||
|
||
const sendMediaRef = async (source: string): Promise<boolean> => {
|
||
if (!source) return false;
|
||
const msg: Json = { document: source as any };
|
||
if (outText) (msg as any).caption = textOpts.content.text;
|
||
await s.sendMessage(params.deliverTo, msg as any, textOpts.quoted ? { quoted: textOpts.quoted } : undefined);
|
||
return true;
|
||
};
|
||
|
||
if (await sendMediaRef(mediaPath || mediaUrl)) return;
|
||
|
||
for (const att of attachments) {
|
||
if (!att || typeof att !== "object") continue;
|
||
const raw = String(
|
||
(att as any).data_base64 || (att as any).media_base64 || (att as any).image_base64 || (att as any).data || "",
|
||
).trim();
|
||
if (!raw) continue;
|
||
const decoded = decodeBase64Payload(raw);
|
||
if (!decoded) continue;
|
||
const msg: Json = {
|
||
document: decoded.data as any,
|
||
mimetype: decoded.mime,
|
||
fileName: String((att as any).name || (att as any).filename || "attachment.bin"),
|
||
};
|
||
if (outText) (msg as any).caption = textOpts.content.text;
|
||
await s.sendMessage(params.deliverTo, msg as any, textOpts.quoted ? { quoted: textOpts.quoted } : undefined);
|
||
return;
|
||
}
|
||
|
||
if (outText) {
|
||
try {
|
||
await s.sendMessage(
|
||
params.deliverTo,
|
||
textOpts.content as any,
|
||
textOpts.quoted ? { quoted: textOpts.quoted } : undefined,
|
||
);
|
||
} catch (err) {
|
||
log(`send reply failed (${String(err)}); retry plain text`);
|
||
await s.sendMessage(params.deliverTo, { text: outText });
|
||
}
|
||
}
|
||
}
|
||
|
||
async function postInbound(payload: Json): Promise<Json> {
|
||
const url = `${LOCAL_BASE_URL.replace(/\/+$/, "")}/inbound/whatsapp`;
|
||
const res = await fetch(url, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify(payload),
|
||
});
|
||
const text = await res.text();
|
||
if (!res.ok) {
|
||
throw new Error(`inbound ${res.status}: ${text.slice(0, 300)}`);
|
||
}
|
||
return text ? (JSON.parse(text) as Json) : {};
|
||
}
|
||
|
||
function sleep(ms: number): Promise<void> {
|
||
return new Promise((r) => setTimeout(r, ms));
|
||
}
|
||
|
||
function _ipLooksHijackedOrUnroutable(ip: string): boolean {
|
||
const s = String(ip || "").trim();
|
||
if (!s) return false;
|
||
if (s.startsWith("198.18.") || s.startsWith("198.19.")) return true; // reserved benchmark range
|
||
if (s.startsWith("0.") || s.startsWith("127.") || s.startsWith("10.")) return true;
|
||
if (s.startsWith("192.168.") || s.startsWith("169.254.")) return true;
|
||
if (/^172\.(1[6-9]|2\d|3[0-1])\./.test(s)) return true;
|
||
return false;
|
||
}
|
||
|
||
async function logNetworkHints(): Promise<void> {
|
||
try {
|
||
const a = await dns.resolve4("web.whatsapp.com").catch(() => []);
|
||
const aaaa = await dns.resolve6("web.whatsapp.com").catch(() => []);
|
||
const sample = [...a, ...aaaa].slice(0, 6);
|
||
if (sample.length > 0) {
|
||
log(`dns web.whatsapp.com => ${sample.join(", ")}`);
|
||
const bad = sample.find(_ipLooksHijackedOrUnroutable);
|
||
if (bad) {
|
||
log(
|
||
`WARNING: DNS looks suspicious (e.g. ${bad}). This often causes WhatsApp Web handshake failures. Try switching system DNS (1.1.1.1/8.8.8.8) or check proxy/hosts rules.`,
|
||
);
|
||
}
|
||
}
|
||
} catch {
|
||
// best-effort
|
||
}
|
||
const httpProxy = process.env.HTTP_PROXY || process.env.http_proxy || "";
|
||
const httpsProxy = process.env.HTTPS_PROXY || process.env.https_proxy || "";
|
||
if (httpProxy || httpsProxy) {
|
||
log(`proxy env detected HTTP_PROXY=${httpProxy ? "set" : "unset"} HTTPS_PROXY=${httpsProxy ? "set" : "unset"}`);
|
||
}
|
||
if (PROXY_URL) {
|
||
log(`proxy configured for Baileys via PROXY_URL=${PROXY_URL}`);
|
||
}
|
||
}
|
||
|
||
function makeDeduper(params: { ttlMs: number; max: number }) {
|
||
const seen = new Map<string, number>();
|
||
const prune = (now: number): void => {
|
||
for (const [k, ts] of seen) {
|
||
if (now - ts > params.ttlMs) {
|
||
seen.delete(k);
|
||
}
|
||
}
|
||
if (seen.size <= params.max) return;
|
||
const ordered = Array.from(seen.entries()).sort((a, b) => a[1] - b[1]);
|
||
const drop = ordered.slice(0, Math.max(0, ordered.length - params.max));
|
||
for (const [k] of drop) seen.delete(k);
|
||
};
|
||
return {
|
||
has: (id: string): boolean => {
|
||
const now = Date.now();
|
||
prune(now);
|
||
const ts = seen.get(id);
|
||
return typeof ts === "number" && now - ts <= params.ttlMs;
|
||
},
|
||
add: (id: string): void => {
|
||
const now = Date.now();
|
||
seen.set(id, now);
|
||
prune(now);
|
||
},
|
||
};
|
||
}
|
||
|
||
async function main(): Promise<void> {
|
||
ensureDir(STATE_DIR);
|
||
const { state, saveCreds } = await loadAuthState(STATE_DIR);
|
||
const { version } = await fetchLatestBaileysVersion();
|
||
const dedupe = makeDeduper({ ttlMs: 10 * 60 * 1000, max: 20_000 });
|
||
|
||
let reconnectAttempt = 0;
|
||
let sock: ReturnType<typeof makeWASocket> | null = null;
|
||
const wsAgent = PROXY_URL ? new HttpsProxyAgent(PROXY_URL) : undefined;
|
||
const groupNameCache = new Map<string, { name: string; ts: number }>();
|
||
const GROUP_NAME_TTL_MS = 10 * 60 * 1000;
|
||
|
||
const resolveGroupName = async (chatId: string): Promise<string> => {
|
||
const key = String(chatId || "").trim();
|
||
if (!key) return "";
|
||
const cached = groupNameCache.get(key);
|
||
const now = Date.now();
|
||
if (cached && now - cached.ts < GROUP_NAME_TTL_MS) return cached.name;
|
||
const s = sock;
|
||
if (!s) return cached?.name || "";
|
||
try {
|
||
const meta = await s.groupMetadata(key);
|
||
const name = String(meta?.subject || "").trim();
|
||
groupNameCache.set(key, { name, ts: now });
|
||
return name;
|
||
} catch {
|
||
return cached?.name || "";
|
||
}
|
||
};
|
||
|
||
const connectOnce = async () => {
|
||
sock = makeWASocket({
|
||
auth: state,
|
||
browser: Browsers.windows("oclaw"),
|
||
version,
|
||
agent: wsAgent as any,
|
||
printQRInTerminal: false,
|
||
generateHighQualityLinkPreview: false,
|
||
});
|
||
|
||
sock.ev.on("creds.update", saveCreds);
|
||
|
||
sock.ev.on("connection.update", async (update) => {
|
||
if (update.qr) {
|
||
log("QR received. Scan it from WhatsApp -> Linked devices.");
|
||
printQrToTerminal(update.qr);
|
||
}
|
||
if (update.connection === "open") {
|
||
reconnectAttempt = 0;
|
||
const me = sock?.user?.id ? jidNormalizedUser(sock.user.id) : "";
|
||
log(`connected. me=${me || "unknown"} loginOnly=${LOGIN_ONLY}`);
|
||
if (LOGIN_ONLY) {
|
||
log("login-only mode: exiting after successful link.");
|
||
process.exit(0);
|
||
}
|
||
}
|
||
if (update.connection === "close") {
|
||
const statusCode = (update.lastDisconnect?.error as any)?.output?.statusCode as number | undefined;
|
||
const reason = statusCode ? (DisconnectReason as any)[statusCode] || String(statusCode) : "unknown";
|
||
const errText = String(update.lastDisconnect?.error || "").slice(0, 200);
|
||
log(`disconnected reason=${reason} statusCode=${String(statusCode || "")} err=${errText}`);
|
||
if (statusCode === DisconnectReason.loggedOut) {
|
||
log("logged out: delete data/channel_sidecar/whatsapp/state/auth to re-link.");
|
||
return;
|
||
}
|
||
reconnectAttempt += 1;
|
||
const base = Math.min(30_000, 1000 * Math.pow(2, Math.min(6, reconnectAttempt)));
|
||
const jitter = Math.floor(Math.random() * 500);
|
||
const delay = base + jitter;
|
||
log(`reconnecting in ${delay}ms attempt=${reconnectAttempt}`);
|
||
await sleep(delay);
|
||
await connectOnce();
|
||
}
|
||
});
|
||
|
||
sock.ev.on("messages.upsert", async (upsert) => {
|
||
const msgs = Array.isArray(upsert.messages) ? upsert.messages : [];
|
||
for (const msg of msgs) {
|
||
try {
|
||
const key = msg.key;
|
||
const id = String(key.id || "").trim();
|
||
const remoteJid = String(key.remoteJid || "").trim();
|
||
if (!id || !remoteJid) continue;
|
||
if (isStatusOrBroadcastJid(remoteJid)) continue;
|
||
if (key.fromMe) continue;
|
||
if (dedupe.has(id)) continue;
|
||
dedupe.add(id);
|
||
|
||
const text = pickText(msg.message);
|
||
if (!text) continue;
|
||
|
||
const isGroup = remoteJid.endsWith("@g.us");
|
||
const participantRaw = String(key.participant || "").trim();
|
||
const participantAlt = String((key as any).participantAlt || "").trim();
|
||
const userId = isGroup
|
||
? resolveSenderJid(key) || jidNormalizedUser(remoteJid)
|
||
: jidNormalizedUser(remoteJid);
|
||
const chatId = jidNormalizedUser(remoteJid);
|
||
const mentions = extractMentionsFromUpsert(msg);
|
||
const quote = extractQuoteContext(msg.message);
|
||
const botJidRaw = sock?.user?.id ? String(sock.user.id).trim() : "";
|
||
const botJid = botJidRaw ? jidNormalizedUser(botJidRaw) : "";
|
||
const mentionsBot = messageMentionsBot(sock, mentions, botJidRaw);
|
||
const isReplyToBot = Boolean(
|
||
botJidRaw &&
|
||
quote.participant &&
|
||
(messageMentionsBot(sock, [quote.participant], botJidRaw) || jidsSameUser(botJidRaw, quote.participant)),
|
||
);
|
||
|
||
const groupName = isGroup ? await resolveGroupName(chatId) : "";
|
||
|
||
const raw = {
|
||
id,
|
||
remoteJid,
|
||
participant: participantRaw || null,
|
||
participantAlt: participantAlt || null,
|
||
pushName: (msg as any).pushName || null,
|
||
messageTimestamp: (msg as any).messageTimestamp || null,
|
||
quotedParticipant: quote.participant || null,
|
||
quotedStanzaId: quote.stanzaId || null,
|
||
isReplyToBot,
|
||
mentionsBot,
|
||
mentionedJids: mentions,
|
||
};
|
||
|
||
const inbound = buildInboundPayload({
|
||
chatId,
|
||
userId,
|
||
text,
|
||
raw,
|
||
isGroup,
|
||
mentions,
|
||
groupName: groupName || undefined,
|
||
botJid: botJidRaw || botJid || undefined,
|
||
mentionsBot,
|
||
});
|
||
if (VERBOSE || isGroup) {
|
||
log(
|
||
`inbound group=${isGroup} chat=${chatId} user=${userId} mentions=${mentions.length} mentionsBot=${mentionsBot} replyToBot=${isReplyToBot} textLen=${text.length} mention0=${mentions[0] || ""}`,
|
||
);
|
||
} else if (VERBOSE) {
|
||
log(`inbound posting chat=${chatId} user=${userId} textLen=${text.length}`);
|
||
}
|
||
const out = await postInbound(inbound);
|
||
const replies = Array.isArray(out.replies) ? (out.replies as Json[]) : [];
|
||
if (isGroup || VERBOSE) log(`inbound ok chat=${chatId} replies=${replies.length}`);
|
||
for (const r of replies) {
|
||
const outText = String((r as any).text || "").trim();
|
||
if (!shouldSendOutboundText(outText) && !(Array.isArray((r as any).attachments) && (r as any).attachments.length)) {
|
||
if (VERBOSE) log(`skip outbound reply chat=${chatId} (nonsend text)`);
|
||
continue;
|
||
}
|
||
const deliverTo = String((r as any).chat_id || chatId).trim() || chatId;
|
||
await sendReplyWithAttachments({ sock, deliverTo, text: outText, reply: r });
|
||
}
|
||
} catch (err) {
|
||
log(`handle message error: ${String(err)}`);
|
||
}
|
||
}
|
||
});
|
||
};
|
||
|
||
log(`runner started local=${LOCAL_BASE_URL} stateDir=${STATE_DIR} verbose=${VERBOSE} node=${process.version}`);
|
||
await logNetworkHints();
|
||
await connectOnce();
|
||
}
|
||
|
||
void main();
|
||
|