fix(whatsapp): reconnect on crypto unhandled rejection
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import { chunkText, resolveTextChunkLimit } from "../auto-reply/chunk.js";
|
||||
import { hasControlCommand } from "../auto-reply/command-detection.js";
|
||||
import { formatAgentEnvelope } from "../auto-reply/envelope.js";
|
||||
import {
|
||||
normalizeGroupActivation,
|
||||
@@ -26,6 +25,7 @@ import {
|
||||
import { logVerbose, shouldLogVerbose } from "../globals.js";
|
||||
import { emitHeartbeatEvent } from "../infra/heartbeat-events.js";
|
||||
import { enqueueSystemEvent } from "../infra/system-events.js";
|
||||
import { registerUnhandledRejectionHandler } from "../infra/unhandled-rejections.js";
|
||||
import { createSubsystemLogger, getChildLogger } from "../logging.js";
|
||||
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
|
||||
import { isSelfChatMode, jidToE164, normalizeE164 } from "../utils.js";
|
||||
@@ -49,6 +49,45 @@ const whatsappInboundLog = whatsappLog.child("inbound");
|
||||
const whatsappOutboundLog = whatsappLog.child("outbound");
|
||||
const whatsappHeartbeatLog = whatsappLog.child("heartbeat");
|
||||
|
||||
const isLikelyWhatsAppCryptoError = (reason: unknown) => {
|
||||
const formatReason = (value: unknown): string => {
|
||||
if (value == null) return "";
|
||||
if (typeof value === "string") return value;
|
||||
if (value instanceof Error) {
|
||||
return `${value.message}\n${value.stack ?? ""}`;
|
||||
}
|
||||
if (typeof value === "object") {
|
||||
try {
|
||||
return JSON.stringify(value);
|
||||
} catch {
|
||||
return Object.prototype.toString.call(value);
|
||||
}
|
||||
}
|
||||
if (typeof value === "number") return String(value);
|
||||
if (typeof value === "boolean") return String(value);
|
||||
if (typeof value === "bigint") return String(value);
|
||||
if (typeof value === "symbol") return value.description ?? value.toString();
|
||||
if (typeof value === "function")
|
||||
return value.name ? `[function ${value.name}]` : "[function]";
|
||||
return Object.prototype.toString.call(value);
|
||||
};
|
||||
const raw =
|
||||
reason instanceof Error
|
||||
? `${reason.message}\n${reason.stack ?? ""}`
|
||||
: formatReason(reason);
|
||||
const haystack = raw.toLowerCase();
|
||||
const hasAuthError =
|
||||
haystack.includes("unsupported state or unable to authenticate data") ||
|
||||
haystack.includes("bad mac");
|
||||
if (!hasAuthError) return false;
|
||||
return (
|
||||
haystack.includes("@whiskeysockets/baileys") ||
|
||||
haystack.includes("baileys") ||
|
||||
haystack.includes("noise-handler") ||
|
||||
haystack.includes("aesdecryptgcm")
|
||||
);
|
||||
};
|
||||
|
||||
// Send via the active gateway-backed listener. The monitor already owns the single
|
||||
// Baileys session, so use its send API directly.
|
||||
async function sendWithIpcFallback(
|
||||
@@ -849,23 +888,35 @@ export async function monitorWebProvider(
|
||||
);
|
||||
};
|
||||
|
||||
const resolveCommandAllowFrom = () => {
|
||||
const resolveOwnerList = (selfE164?: string | null) => {
|
||||
const allowFrom = mentionConfig.allowFrom;
|
||||
const raw =
|
||||
Array.isArray(allowFrom) && allowFrom.length > 0 ? allowFrom : [];
|
||||
Array.isArray(allowFrom) && allowFrom.length > 0
|
||||
? allowFrom
|
||||
: selfE164
|
||||
? [selfE164]
|
||||
: [];
|
||||
return raw
|
||||
.filter((entry): entry is string => Boolean(entry && entry !== "*"))
|
||||
.map((entry) => normalizeE164(entry))
|
||||
.filter((entry): entry is string => Boolean(entry));
|
||||
};
|
||||
|
||||
const isCommandAuthorized = (msg: WebInboundMsg) => {
|
||||
const allowFrom = resolveCommandAllowFrom();
|
||||
if (allowFrom.length === 0) return true;
|
||||
if (mentionConfig.allowFrom?.includes("*")) return true;
|
||||
const isOwnerSender = (msg: WebInboundMsg) => {
|
||||
const sender = normalizeE164(msg.senderE164 ?? "");
|
||||
if (!sender) return false;
|
||||
return allowFrom.includes(sender);
|
||||
const owners = resolveOwnerList(msg.selfE164 ?? undefined);
|
||||
return owners.includes(sender);
|
||||
};
|
||||
|
||||
const isStatusCommand = (body: string) => {
|
||||
const trimmed = body.trim().toLowerCase();
|
||||
if (!trimmed) return false;
|
||||
return (
|
||||
trimmed === "/status" ||
|
||||
trimmed === "status" ||
|
||||
trimmed.startsWith("/status ")
|
||||
);
|
||||
};
|
||||
|
||||
const stripMentionsForCommand = (text: string, selfE164?: string | null) => {
|
||||
@@ -912,6 +963,7 @@ export async function monitorWebProvider(
|
||||
let lastMessageAt: number | null = null;
|
||||
let handledMessages = 0;
|
||||
let _lastInboundMsg: WebInboundMsg | null = null;
|
||||
let unregisterUnhandled: (() => void) | null = null;
|
||||
|
||||
// Watchdog to detect stuck message processing (e.g., event emitter died)
|
||||
// Should be significantly longer than the reply heartbeat interval to avoid false positives
|
||||
@@ -1182,7 +1234,6 @@ export async function monitorWebProvider(
|
||||
SenderName: msg.senderName,
|
||||
SenderE164: msg.senderE164,
|
||||
WasMentioned: msg.wasMentioned,
|
||||
CommandAuthorized: isCommandAuthorized(msg),
|
||||
Surface: "whatsapp",
|
||||
},
|
||||
{
|
||||
@@ -1323,15 +1374,12 @@ export async function monitorWebProvider(
|
||||
noteGroupMember(conversationId, msg.senderE164, msg.senderName);
|
||||
const commandBody = stripMentionsForCommand(msg.body, msg.selfE164);
|
||||
const activationCommand = parseActivationCommand(commandBody);
|
||||
const commandAuthorized = isCommandAuthorized(msg);
|
||||
const statusCommand = hasControlCommand(commandBody);
|
||||
const hasAnyMention = (msg.mentionedJids?.length ?? 0) > 0;
|
||||
const isOwner = isOwnerSender(msg);
|
||||
const statusCommand = isStatusCommand(commandBody);
|
||||
const shouldBypassMention =
|
||||
commandAuthorized &&
|
||||
(activationCommand.hasCommand || statusCommand) &&
|
||||
!hasAnyMention;
|
||||
isOwner && (activationCommand.hasCommand || statusCommand);
|
||||
|
||||
if (activationCommand.hasCommand && !commandAuthorized) {
|
||||
if (activationCommand.hasCommand && !isOwner) {
|
||||
logVerbose(
|
||||
`Ignoring /activation from non-owner in group ${conversationId}`,
|
||||
);
|
||||
@@ -1393,9 +1441,27 @@ export async function monitorWebProvider(
|
||||
);
|
||||
|
||||
setActiveWebListener(listener);
|
||||
unregisterUnhandled = registerUnhandledRejectionHandler((reason) => {
|
||||
if (!isLikelyWhatsAppCryptoError(reason)) return false;
|
||||
const errorStr = formatError(reason);
|
||||
reconnectLogger.warn(
|
||||
{ connectionId, error: errorStr },
|
||||
"web reconnect: unhandled rejection from WhatsApp socket; forcing reconnect",
|
||||
);
|
||||
listener.signalClose?.({
|
||||
status: 499,
|
||||
isLoggedOut: false,
|
||||
error: reason,
|
||||
});
|
||||
return true;
|
||||
});
|
||||
|
||||
const closeListener = async () => {
|
||||
setActiveWebListener(null);
|
||||
if (unregisterUnhandled) {
|
||||
unregisterUnhandled();
|
||||
unregisterUnhandled = null;
|
||||
}
|
||||
if (heartbeat) clearInterval(heartbeat);
|
||||
if (watchdogTimer) clearInterval(watchdogTimer);
|
||||
if (backgroundTasks.size > 0) {
|
||||
|
||||
Reference in New Issue
Block a user