@openclaw/whatsapp 2026.5.2 → 2026.5.3-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/access-control-BQ3tCkH8.js +288 -0
- package/dist/account-config-CNQ33oav.js +47 -0
- package/dist/account-ids-DG18xddX.js +5 -0
- package/dist/accounts-BDqgpFEB.js +116 -0
- package/dist/action-runtime-DTGNgG-i.js +74 -0
- package/dist/action-runtime-api.js +2 -0
- package/dist/action-runtime.runtime.js +2 -0
- package/dist/active-listener-N7GFKsuN.js +11 -0
- package/dist/agent-tools-login-DpRxLNRB.js +89 -0
- package/dist/api.js +148 -0
- package/dist/audio-preflight.runtime-Bcdsl_ri.js +7 -0
- package/dist/auth-presence.js +32 -0
- package/dist/auth-store-BjCb50Ry.js +381 -0
- package/dist/channel-DQ-5Xoj9.js +542 -0
- package/dist/channel-config-api.js +2 -0
- package/dist/channel-plugin-api.js +2 -0
- package/dist/channel-react-action-Df20qAy9.js +42 -0
- package/dist/channel.runtime-DKXRnA8x.js +55 -0
- package/dist/channel.setup-DOHR2BGp.js +20 -0
- package/dist/command-policy-O8sRnqUV.js +8 -0
- package/dist/config-schema-Dmo66Ubb.js +27 -0
- package/dist/connection-controller-BiHAsTM8.js +596 -0
- package/dist/connection-controller-registry-D_iIGz5m.js +22 -0
- package/dist/constants-DLGCIbfl.js +4 -0
- package/dist/constants.js +2 -0
- package/dist/contract-api.js +19 -0
- package/dist/creds-files-_2V8jzQa.js +19 -0
- package/dist/directory-config-DVs4ffXu.js +37 -0
- package/dist/directory-contract-api.js +2 -0
- package/dist/doctor-A9tXm9Om.js +48 -0
- package/dist/doctor-contract-api.js +10 -0
- package/dist/group-session-contract-B8QBn0I2.js +15 -0
- package/dist/identity-QHAlV7Fu.js +75 -0
- package/dist/index.js +18 -0
- package/dist/legacy-session-surface-api.js +8 -0
- package/dist/legacy-state-migrations-api.js +2 -0
- package/dist/light-runtime-api.js +5 -0
- package/dist/login-DymdrKca.js +52 -0
- package/dist/login-qr-DIS0Iv-7.js +378 -0
- package/dist/login-qr-api.js +2 -0
- package/dist/login-qr-runtime.js +16 -0
- package/dist/monitor-C4Fhg_lD.js +3349 -0
- package/dist/normalize-CWYiGkXw.js +2 -0
- package/dist/normalize-target-BPVHNsM5.js +74 -0
- package/dist/outbound-adapter-BssVnZdz.js +29 -0
- package/dist/outbound-base-Bq1UYEK5.js +119 -0
- package/dist/outbound-media-contract-CGss_QHy.js +163 -0
- package/dist/outbound-payload-test-api.js +2 -0
- package/dist/quoted-message-TENQzyxV.js +108 -0
- package/dist/reaction-level-CRVEVAfi.js +16 -0
- package/dist/reply-resolver.runtime-BRCgoO4C.js +2 -0
- package/dist/resolve-outbound-target-CN-Wo6IY.js +39 -0
- package/dist/rolldown-runtime-RkAeH_Qm.js +27 -0
- package/dist/runtime-6fZ67abR.js +8 -0
- package/dist/runtime-api.js +15 -0
- package/dist/runtime-group-policy-Gp1lQ0at.js +11 -0
- package/dist/secret-contract-api.js +5 -0
- package/dist/security-contract-DqH8JDX6.js +26 -0
- package/dist/security-contract-api.js +2 -0
- package/dist/send-CIyLFxdZ.js +234 -0
- package/dist/session-contract-B0efQf9P.js +27 -0
- package/dist/session-errors-CSuKZPub.js +63 -0
- package/dist/session.runtime-C1eSE_KK.js +13 -0
- package/dist/setup-core-DZKvpzbH.js +46 -0
- package/dist/setup-entry.js +23 -0
- package/dist/setup-plugin-api.js +2 -0
- package/dist/setup-surface-ieFnIW4g.js +344 -0
- package/dist/shared-D_wzSVar.js +307 -0
- package/dist/state-migrations-DnCdvoYD.js +39 -0
- package/dist/targets.js +2 -0
- package/dist/test-api.js +3 -0
- package/dist/text-runtime-C1zNiZeO.js +116 -0
- package/package.json +14 -6
- package/action-runtime-api.ts +0 -1
- package/action-runtime.runtime.ts +0 -1
- package/api.ts +0 -213
- package/auth-presence.ts +0 -80
- package/channel-config-api.ts +0 -1
- package/channel-plugin-api.ts +0 -3
- package/config-api.ts +0 -4
- package/constants.ts +0 -1
- package/contract-api.ts +0 -29
- package/directory-contract-api.ts +0 -4
- package/doctor-contract-api.ts +0 -8
- package/index.test.ts +0 -13
- package/index.ts +0 -16
- package/legacy-session-surface-api.ts +0 -6
- package/legacy-state-migrations-api.ts +0 -1
- package/light-runtime-api.ts +0 -12
- package/login-qr-api.ts +0 -1
- package/login-qr-runtime.ts +0 -23
- package/outbound-payload-test-api.ts +0 -1
- package/runtime-api.ts +0 -84
- package/secret-contract-api.ts +0 -4
- package/security-contract-api.ts +0 -4
- package/setup-entry.test.ts +0 -21
- package/setup-entry.ts +0 -21
- package/setup-plugin-api.ts +0 -3
- package/src/account-config.ts +0 -77
- package/src/account-ids.ts +0 -13
- package/src/account-types.ts +0 -5
- package/src/accounts.test.ts +0 -182
- package/src/accounts.ts +0 -176
- package/src/accounts.whatsapp-auth.test.ts +0 -59
- package/src/action-runtime-target-auth.ts +0 -27
- package/src/action-runtime.test.ts +0 -330
- package/src/action-runtime.ts +0 -76
- package/src/active-listener.test.ts +0 -65
- package/src/active-listener.ts +0 -17
- package/src/agent-tools-login.test.ts +0 -81
- package/src/agent-tools-login.ts +0 -113
- package/src/approval-auth.test.ts +0 -24
- package/src/approval-auth.ts +0 -27
- package/src/auth-store.runtime.ts +0 -1
- package/src/auth-store.test.ts +0 -311
- package/src/auth-store.ts +0 -502
- package/src/auto-reply/config.runtime.ts +0 -16
- package/src/auto-reply/constants.ts +0 -1
- package/src/auto-reply/deliver-reply.test.ts +0 -843
- package/src/auto-reply/deliver-reply.ts +0 -279
- package/src/auto-reply/loggers.ts +0 -6
- package/src/auto-reply/mentions.ts +0 -131
- package/src/auto-reply/monitor/ack-reaction.test.ts +0 -170
- package/src/auto-reply/monitor/ack-reaction.ts +0 -99
- package/src/auto-reply/monitor/audio-preflight.runtime.ts +0 -9
- package/src/auto-reply/monitor/broadcast.ts +0 -153
- package/src/auto-reply/monitor/commands.ts +0 -19
- package/src/auto-reply/monitor/echo.ts +0 -64
- package/src/auto-reply/monitor/group-activation.runtime.ts +0 -1
- package/src/auto-reply/monitor/group-activation.test.ts +0 -189
- package/src/auto-reply/monitor/group-activation.ts +0 -73
- package/src/auto-reply/monitor/group-gating.audio-preflight.test.ts +0 -103
- package/src/auto-reply/monitor/group-gating.runtime.ts +0 -8
- package/src/auto-reply/monitor/group-gating.ts +0 -217
- package/src/auto-reply/monitor/group-members.test.ts +0 -56
- package/src/auto-reply/monitor/group-members.ts +0 -65
- package/src/auto-reply/monitor/inbound-context.test.ts +0 -97
- package/src/auto-reply/monitor/inbound-context.ts +0 -92
- package/src/auto-reply/monitor/inbound-dispatch.runtime.ts +0 -21
- package/src/auto-reply/monitor/inbound-dispatch.test.ts +0 -895
- package/src/auto-reply/monitor/inbound-dispatch.ts +0 -422
- package/src/auto-reply/monitor/last-route.test.ts +0 -37
- package/src/auto-reply/monitor/last-route.ts +0 -61
- package/src/auto-reply/monitor/message-line.runtime.ts +0 -38
- package/src/auto-reply/monitor/message-line.ts +0 -54
- package/src/auto-reply/monitor/on-message.audio-preflight.test.ts +0 -354
- package/src/auto-reply/monitor/on-message.ts +0 -291
- package/src/auto-reply/monitor/peer.ts +0 -17
- package/src/auto-reply/monitor/process-message.audio-preflight.test.ts +0 -420
- package/src/auto-reply/monitor/process-message.test.ts +0 -355
- package/src/auto-reply/monitor/process-message.ts +0 -531
- package/src/auto-reply/monitor/runtime-api.ts +0 -30
- package/src/auto-reply/monitor-state.test.ts +0 -64
- package/src/auto-reply/monitor-state.ts +0 -106
- package/src/auto-reply/monitor.ts +0 -600
- package/src/auto-reply/reply-resolver.runtime.ts +0 -1
- package/src/auto-reply/types.ts +0 -48
- package/src/auto-reply/util.ts +0 -63
- package/src/auto-reply/web-auto-reply-monitor.test.ts +0 -687
- package/src/auto-reply/web-auto-reply-utils.test.ts +0 -392
- package/src/auto-reply.broadcast-groups.combined.test.ts +0 -246
- package/src/auto-reply.broadcast-groups.test-harness.ts +0 -47
- package/src/auto-reply.impl.ts +0 -6
- package/src/auto-reply.test-harness.ts +0 -421
- package/src/auto-reply.ts +0 -1
- package/src/auto-reply.web-auto-reply.compresses-common-formats-jpeg-cap.test.ts +0 -427
- package/src/auto-reply.web-auto-reply.connection-and-logging.e2e.test.ts +0 -908
- package/src/auto-reply.web-auto-reply.last-route.test.ts +0 -184
- package/src/channel-actions.runtime.ts +0 -7
- package/src/channel-actions.test.ts +0 -216
- package/src/channel-actions.ts +0 -84
- package/src/channel-outbound.test.ts +0 -131
- package/src/channel-outbound.ts +0 -36
- package/src/channel-react-action.runtime.ts +0 -7
- package/src/channel-react-action.test.ts +0 -294
- package/src/channel-react-action.ts +0 -84
- package/src/channel.runtime.ts +0 -117
- package/src/channel.setup.test.ts +0 -461
- package/src/channel.setup.ts +0 -28
- package/src/channel.ts +0 -339
- package/src/command-policy.ts +0 -7
- package/src/config-accessors.test.ts +0 -34
- package/src/config-accessors.ts +0 -22
- package/src/config-schema.test.ts +0 -121
- package/src/config-schema.ts +0 -6
- package/src/config-ui-hints.ts +0 -24
- package/src/connection-controller-registry.test.ts +0 -27
- package/src/connection-controller-registry.ts +0 -49
- package/src/connection-controller.test.ts +0 -282
- package/src/connection-controller.ts +0 -679
- package/src/creds-files.ts +0 -19
- package/src/creds-persistence.ts +0 -102
- package/src/directory-config.test.ts +0 -60
- package/src/directory-config.ts +0 -40
- package/src/directory-contract.test.ts +0 -56
- package/src/doctor-contract.ts +0 -11
- package/src/doctor.test.ts +0 -74
- package/src/doctor.ts +0 -56
- package/src/group-intro.ts +0 -15
- package/src/group-policy.test.ts +0 -36
- package/src/group-policy.ts +0 -40
- package/src/group-session-contract.ts +0 -20
- package/src/group-session-key.test.ts +0 -53
- package/src/group-session-key.ts +0 -41
- package/src/heartbeat.ts +0 -34
- package/src/identity.ts +0 -164
- package/src/inbound/access-control.test-harness.ts +0 -37
- package/src/inbound/access-control.test.ts +0 -375
- package/src/inbound/access-control.ts +0 -237
- package/src/inbound/dedupe.ts +0 -132
- package/src/inbound/extract.test.ts +0 -282
- package/src/inbound/extract.ts +0 -488
- package/src/inbound/lifecycle.ts +0 -39
- package/src/inbound/media.node.test.ts +0 -83
- package/src/inbound/media.ts +0 -102
- package/src/inbound/monitor.ts +0 -890
- package/src/inbound/runtime-api.ts +0 -7
- package/src/inbound/save-media.runtime.ts +0 -1
- package/src/inbound/send-api.test.ts +0 -319
- package/src/inbound/send-api.ts +0 -145
- package/src/inbound/send-result.ts +0 -61
- package/src/inbound/types.ts +0 -106
- package/src/inbound-context.contract.test.ts +0 -32
- package/src/inbound-policy.ts +0 -231
- package/src/inbound.media.test.ts +0 -320
- package/src/inbound.test.ts +0 -290
- package/src/inbound.ts +0 -9
- package/src/login-qr.test.ts +0 -483
- package/src/login-qr.ts +0 -542
- package/src/login.coverage.test.ts +0 -133
- package/src/login.test.ts +0 -89
- package/src/login.ts +0 -70
- package/src/logout.test.ts +0 -162
- package/src/media.test.ts +0 -480
- package/src/media.ts +0 -10
- package/src/monitor-inbox.allows-messages-from-senders-allowfrom-list.test-support.ts +0 -400
- package/src/monitor-inbox.append-upsert.test-support.ts +0 -133
- package/src/monitor-inbox.behavior.test.ts +0 -5
- package/src/monitor-inbox.blocks-messages-from-unauthorized-senders-not-allowfrom.test-support.ts +0 -362
- package/src/monitor-inbox.captures-media-path-image-messages.test-support.ts +0 -306
- package/src/monitor-inbox.streams-inbound-messages.test-support.ts +0 -729
- package/src/monitor-inbox.test-harness.ts +0 -304
- package/src/normalize-target.ts +0 -123
- package/src/normalize.ts +0 -7
- package/src/outbound-adapter.poll.test.ts +0 -62
- package/src/outbound-adapter.sendpayload.test.ts +0 -195
- package/src/outbound-adapter.ts +0 -36
- package/src/outbound-base.test.ts +0 -560
- package/src/outbound-base.ts +0 -244
- package/src/outbound-media-contract.ts +0 -278
- package/src/outbound-media.runtime.ts +0 -36
- package/src/outbound-payload.contract.test.ts +0 -61
- package/src/outbound-send-deps.ts +0 -1
- package/src/outbound-test-support.ts +0 -16
- package/src/pairing-security.test-harness.ts +0 -59
- package/src/qr-image.ts +0 -1
- package/src/qr-terminal.ts +0 -1
- package/src/quoted-message.test.ts +0 -83
- package/src/quoted-message.ts +0 -184
- package/src/reaction-level.test.ts +0 -94
- package/src/reaction-level.ts +0 -21
- package/src/reconnect.test.ts +0 -51
- package/src/reconnect.ts +0 -55
- package/src/resolve-outbound-target.test.ts +0 -288
- package/src/resolve-outbound-target.ts +0 -58
- package/src/resolve-target.test.ts +0 -120
- package/src/runtime-api.ts +0 -59
- package/src/runtime-group-policy.ts +0 -16
- package/src/runtime.ts +0 -9
- package/src/security-contract.ts +0 -47
- package/src/security-fix.ts +0 -71
- package/src/send.test.ts +0 -534
- package/src/send.ts +0 -292
- package/src/session-contract.test.ts +0 -31
- package/src/session-contract.ts +0 -43
- package/src/session-errors.ts +0 -125
- package/src/session-route.test.ts +0 -41
- package/src/session-route.ts +0 -32
- package/src/session.runtime.ts +0 -8
- package/src/session.test.ts +0 -559
- package/src/session.ts +0 -330
- package/src/setup-core.ts +0 -52
- package/src/setup-finalize.ts +0 -446
- package/src/setup-surface.test.ts +0 -367
- package/src/setup-surface.ts +0 -69
- package/src/setup-test-helpers.ts +0 -216
- package/src/shared.ts +0 -291
- package/src/socket-timing.test.ts +0 -49
- package/src/socket-timing.ts +0 -38
- package/src/state-migrations.ts +0 -54
- package/src/status-issues.test.ts +0 -137
- package/src/status-issues.ts +0 -185
- package/src/system-prompt.test.ts +0 -199
- package/src/system-prompt.ts +0 -31
- package/src/targets-runtime.ts +0 -180
- package/src/test-helpers.ts +0 -690
- package/src/text-runtime.test.ts +0 -162
- package/src/text-runtime.ts +0 -11
- package/src/vcard.ts +0 -84
- package/targets.ts +0 -5
- package/test-api.ts +0 -2
- package/tsconfig.json +0 -16
|
@@ -0,0 +1,3349 @@
|
|
|
1
|
+
import { a as resolveWhatsAppAccount, s as resolveWhatsAppMediaMaxBytes } from "./accounts-BDqgpFEB.js";
|
|
2
|
+
import { n as isWhatsAppNewsletterJid, t as isWhatsAppGroupJid$1 } from "./normalize-target-BPVHNsM5.js";
|
|
3
|
+
import { t as resolveWhatsAppReactionLevel } from "./reaction-level-CRVEVAfi.js";
|
|
4
|
+
import "./normalize-CWYiGkXw.js";
|
|
5
|
+
import { n as getStatusCode, t as formatError } from "./session-errors-CSuKZPub.js";
|
|
6
|
+
import { a as markdownToWhatsApp, i as jidToE164, o as resolveJidToE164, r as isSelfChatMode, s as toWhatsappJid, t as text_runtime_exports } from "./text-runtime-C1zNiZeO.js";
|
|
7
|
+
import { i as prepareWhatsAppOutboundMedia, o as sendWhatsAppOutboundWithRetry, r as normalizeWhatsAppPayloadTextPreservingIndentation, t as normalizeWhatsAppOutboundPayload } from "./outbound-media-contract-CGss_QHy.js";
|
|
8
|
+
import { r as sendReactionWhatsApp } from "./send-CIyLFxdZ.js";
|
|
9
|
+
import { n as cacheInboundMessageMeta, r as lookupInboundMessageMeta, t as buildQuotedMessageOptions } from "./quoted-message-TENQzyxV.js";
|
|
10
|
+
import { _ as readWebSelfIdentityForDecision, a as getWebAuthAgeMs, h as readWebSelfId, r as WhatsAppAuthUnstableError, s as logoutWeb } from "./auth-store-BjCb50Ry.js";
|
|
11
|
+
import { a as getSelfIdentity, c as resolveComparableIdentity, i as getReplyContext, n as getMentionIdentities, o as getSenderIdentity, r as getPrimaryIdentityId, s as identitiesOverlap, t as getComparableIdentityValues } from "./identity-QHAlV7Fu.js";
|
|
12
|
+
import { i as resolveWhatsAppInboundPolicy, n as checkInboundAccessControl, r as resolveWhatsAppCommandAuthorized } from "./access-control-BQ3tCkH8.js";
|
|
13
|
+
import { c as waitForWaConnection, d as computeBackoff, f as newConnectionId, h as sleepWithAbort, l as resolveWhatsAppSocketTiming, m as resolveReconnectPolicy, n as WhatsAppConnectionController, o as createWaSocket, p as resolveHeartbeatSeconds, u as DEFAULT_RECONNECT_POLICY } from "./connection-controller-BiHAsTM8.js";
|
|
14
|
+
import { resolveAccountEntry } from "openclaw/plugin-sdk/account-core";
|
|
15
|
+
import { getChildLogger, normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
|
|
16
|
+
import { createAckReactionHandle, logAckFailure, removeAckReactionHandleAfterReply, shouldAckReactionForWhatsApp } from "openclaw/plugin-sdk/channel-feedback";
|
|
17
|
+
import { formatCliCommand } from "openclaw/plugin-sdk/cli-runtime";
|
|
18
|
+
import { resolveMarkdownTableMode as resolveMarkdownTableMode$1 } from "openclaw/plugin-sdk/markdown-table-runtime";
|
|
19
|
+
import { createSubsystemLogger, defaultRuntime, formatDurationPrecise, getChildLogger as getChildLogger$1, logVerbose, logVerbose as logVerbose$1, registerUnhandledRejectionHandler, shouldLogVerbose, shouldLogVerbose as shouldLogVerbose$1 } from "openclaw/plugin-sdk/runtime-env";
|
|
20
|
+
import { getAgentScopedMediaLocalRoots } from "openclaw/plugin-sdk/media-runtime";
|
|
21
|
+
import { LocalMediaAccessError, getDefaultLocalRoots, loadWebMedia as loadWebMedia$1, loadWebMediaRaw, optimizeImageToJpeg, optimizeImageToPng } from "openclaw/plugin-sdk/web-media";
|
|
22
|
+
import { chunkMarkdownTextWithMode } from "openclaw/plugin-sdk/reply-chunking";
|
|
23
|
+
import { isReasoningReplyPayload, resolveSendableOutboundReplyParts, sendMediaWithLeadingCaption } from "openclaw/plugin-sdk/reply-payload";
|
|
24
|
+
import { DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$1, DEFAULT_MAIN_KEY, buildAgentMainSessionKey, buildAgentSessionKey, buildGroupHistoryKey, deriveLastRoutePolicy, normalizeAccountId as normalizeAccountId$1, normalizeAgentId, resolveAgentRoute, resolveInboundLastRouteSessionKey, resolveThreadSessionKeys } from "openclaw/plugin-sdk/routing";
|
|
25
|
+
import { recordInboundSession } from "openclaw/plugin-sdk/conversation-runtime";
|
|
26
|
+
import { evaluateSupplementalContextVisibility, filterSupplementalContextItems, resolvePinnedMainDmOwnerFromAllowlist } from "openclaw/plugin-sdk/security-runtime";
|
|
27
|
+
import { loadSessionStore, resolveStorePath, updateLastRoute, updateSessionStore } from "openclaw/plugin-sdk/session-store-runtime";
|
|
28
|
+
import { dispatchReplyWithBufferedBlockDispatcher, finalizeInboundContext, resolveChunkMode, resolveTextChunkLimit } from "openclaw/plugin-sdk/reply-runtime";
|
|
29
|
+
import { createInboundDebouncer, resolveInboundDebounceMs } from "openclaw/plugin-sdk/channel-inbound-debounce";
|
|
30
|
+
import { hasControlCommand, hasControlCommand as hasControlCommand$1, shouldComputeCommandAuthorized } from "openclaw/plugin-sdk/command-detection";
|
|
31
|
+
import { drainPendingDeliveries } from "openclaw/plugin-sdk/delivery-queue-runtime";
|
|
32
|
+
import { DEFAULT_GROUP_HISTORY_LIMIT, buildHistoryContextFromEntries, recordPendingHistoryEntryIfEnabled } from "openclaw/plugin-sdk/reply-history";
|
|
33
|
+
import { enqueueSystemEvent } from "openclaw/plugin-sdk/system-event-runtime";
|
|
34
|
+
import { DisconnectReason as DisconnectReason$1, downloadMediaMessage, extractMessageContent, getContentType, isJidGroup, normalizeMessageContent, normalizeMessageContent as normalizeMessageContent$1 } from "@whiskeysockets/baileys";
|
|
35
|
+
import { recordChannelActivity } from "openclaw/plugin-sdk/channel-activity-runtime";
|
|
36
|
+
import { formatLocationText, resolveInboundSessionEnvelopeContext } from "openclaw/plugin-sdk/channel-inbound";
|
|
37
|
+
import { createClaimableDedupe } from "openclaw/plugin-sdk/persistent-dedupe";
|
|
38
|
+
import { saveMediaBuffer } from "openclaw/plugin-sdk/media-store";
|
|
39
|
+
import { getRuntimeConfig as getRuntimeConfig$1, getRuntimeConfigSourceSnapshot } from "openclaw/plugin-sdk/runtime-config-snapshot";
|
|
40
|
+
import { resolveChannelContextVisibilityMode } from "openclaw/plugin-sdk/context-visibility-runtime";
|
|
41
|
+
import { buildMentionRegexes, implicitMentionKindWhen, normalizeMentionText, resolveInboundMentionDecision } from "openclaw/plugin-sdk/channel-mention-gating";
|
|
42
|
+
import { createConnectedChannelStatusPatch, createTransportActivityStatusPatch } from "openclaw/plugin-sdk/gateway-runtime";
|
|
43
|
+
import { normalizeGroupActivation, parseActivationCommand } from "openclaw/plugin-sdk/group-activation";
|
|
44
|
+
import { createInternalHookEvent, deriveInboundMessageHookContext, fireAndForgetBoundedHook, toInternalMessageReceivedContext, toPluginMessageContext, toPluginMessageReceivedEvent, triggerInternalHook } from "openclaw/plugin-sdk/hook-runtime";
|
|
45
|
+
import { hasVisibleInboundReplyDispatch, runInboundReplyTurn } from "openclaw/plugin-sdk/inbound-reply-dispatch";
|
|
46
|
+
import { getGlobalHookRunner } from "openclaw/plugin-sdk/plugin-runtime";
|
|
47
|
+
import { resolveBatchedReplyThreadingPolicy } from "openclaw/plugin-sdk/reply-reference";
|
|
48
|
+
import { resolveIdentityNamePrefix as resolveIdentityNamePrefix$1 } from "openclaw/plugin-sdk/agent-runtime";
|
|
49
|
+
import { formatInboundEnvelope, formatInboundEnvelope as formatInboundEnvelope$1 } from "openclaw/plugin-sdk/channel-envelope";
|
|
50
|
+
import { toLocationContext } from "openclaw/plugin-sdk/channel-location";
|
|
51
|
+
import { createChannelReplyPipeline } from "openclaw/plugin-sdk/channel-reply-pipeline";
|
|
52
|
+
//#region extensions/whatsapp/src/inbound/dedupe.ts
|
|
53
|
+
const RECENT_WEB_MESSAGE_TTL_MS = 20 * 6e4;
|
|
54
|
+
const RECENT_WEB_MESSAGE_MAX = 5e3;
|
|
55
|
+
const RECENT_OUTBOUND_MESSAGE_TTL_MS = 20 * 6e4;
|
|
56
|
+
const RECENT_OUTBOUND_MESSAGE_MAX = 5e3;
|
|
57
|
+
const claimableInboundMessages = createClaimableDedupe({
|
|
58
|
+
ttlMs: RECENT_WEB_MESSAGE_TTL_MS,
|
|
59
|
+
memoryMaxSize: RECENT_WEB_MESSAGE_MAX
|
|
60
|
+
});
|
|
61
|
+
const recentOutboundMessages = createRecentMessageCache({
|
|
62
|
+
ttlMs: RECENT_OUTBOUND_MESSAGE_TTL_MS,
|
|
63
|
+
maxSize: RECENT_OUTBOUND_MESSAGE_MAX
|
|
64
|
+
});
|
|
65
|
+
function createRecentMessageCache(options) {
|
|
66
|
+
const ttlMs = Math.max(0, options.ttlMs);
|
|
67
|
+
const maxSize = Math.max(0, Math.floor(options.maxSize));
|
|
68
|
+
const cache = /* @__PURE__ */ new Map();
|
|
69
|
+
const prune = (now) => {
|
|
70
|
+
if (ttlMs > 0) {
|
|
71
|
+
const cutoff = now - ttlMs;
|
|
72
|
+
for (const [key, timestamp] of cache) if (timestamp < cutoff) cache.delete(key);
|
|
73
|
+
}
|
|
74
|
+
while (cache.size > maxSize) {
|
|
75
|
+
const oldest = cache.keys().next().value;
|
|
76
|
+
if (!oldest) break;
|
|
77
|
+
cache.delete(oldest);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
const peek = (key, now = Date.now()) => {
|
|
81
|
+
if (!key) return false;
|
|
82
|
+
const timestamp = cache.get(key);
|
|
83
|
+
if (timestamp === void 0) return false;
|
|
84
|
+
if (ttlMs > 0 && now - timestamp >= ttlMs) {
|
|
85
|
+
cache.delete(key);
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
return true;
|
|
89
|
+
};
|
|
90
|
+
return {
|
|
91
|
+
check: (key, now = Date.now()) => {
|
|
92
|
+
if (!key) return false;
|
|
93
|
+
const existed = peek(key, now);
|
|
94
|
+
cache.delete(key);
|
|
95
|
+
cache.set(key, now);
|
|
96
|
+
prune(now);
|
|
97
|
+
return existed;
|
|
98
|
+
},
|
|
99
|
+
peek,
|
|
100
|
+
clear: () => cache.clear()
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
var WhatsAppRetryableInboundError = class extends Error {
|
|
104
|
+
constructor(message, options) {
|
|
105
|
+
super(message, options);
|
|
106
|
+
this.name = "WhatsAppRetryableInboundError";
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
function buildMessageKey(params) {
|
|
110
|
+
const accountId = params.accountId.trim();
|
|
111
|
+
const remoteJid = params.remoteJid.trim();
|
|
112
|
+
const messageId = params.messageId.trim();
|
|
113
|
+
if (!accountId || !remoteJid || !messageId || messageId === "unknown") return null;
|
|
114
|
+
return `${accountId}:${remoteJid}:${messageId}`;
|
|
115
|
+
}
|
|
116
|
+
function resetWebInboundDedupe() {
|
|
117
|
+
claimableInboundMessages.clearMemory();
|
|
118
|
+
recentOutboundMessages.clear();
|
|
119
|
+
}
|
|
120
|
+
async function claimRecentInboundMessage(key) {
|
|
121
|
+
return (await claimableInboundMessages.claim(key)).kind === "claimed";
|
|
122
|
+
}
|
|
123
|
+
async function commitRecentInboundMessage(key) {
|
|
124
|
+
await claimableInboundMessages.commit(key);
|
|
125
|
+
}
|
|
126
|
+
function releaseRecentInboundMessage(key, error) {
|
|
127
|
+
claimableInboundMessages.release(key, { error });
|
|
128
|
+
}
|
|
129
|
+
function rememberRecentOutboundMessage(params) {
|
|
130
|
+
const key = buildMessageKey(params);
|
|
131
|
+
if (!key) return;
|
|
132
|
+
recentOutboundMessages.check(key);
|
|
133
|
+
}
|
|
134
|
+
function isRecentOutboundMessage(params) {
|
|
135
|
+
const key = buildMessageKey(params);
|
|
136
|
+
if (!key) return false;
|
|
137
|
+
return recentOutboundMessages.peek(key);
|
|
138
|
+
}
|
|
139
|
+
//#endregion
|
|
140
|
+
//#region extensions/whatsapp/src/vcard.ts
|
|
141
|
+
const ALLOWED_VCARD_KEYS = new Set([
|
|
142
|
+
"FN",
|
|
143
|
+
"N",
|
|
144
|
+
"TEL"
|
|
145
|
+
]);
|
|
146
|
+
function parseVcard(vcard) {
|
|
147
|
+
if (!vcard) return { phones: [] };
|
|
148
|
+
const lines = vcard.split(/\r?\n/);
|
|
149
|
+
let nameFromN;
|
|
150
|
+
let nameFromFn;
|
|
151
|
+
const phones = [];
|
|
152
|
+
for (const rawLine of lines) {
|
|
153
|
+
const line = rawLine.trim();
|
|
154
|
+
if (!line) continue;
|
|
155
|
+
const colonIndex = line.indexOf(":");
|
|
156
|
+
if (colonIndex === -1) continue;
|
|
157
|
+
const key = line.slice(0, colonIndex).toUpperCase();
|
|
158
|
+
const rawValue = line.slice(colonIndex + 1).trim();
|
|
159
|
+
if (!rawValue) continue;
|
|
160
|
+
const baseKey = normalizeVcardKey(key);
|
|
161
|
+
if (!baseKey || !ALLOWED_VCARD_KEYS.has(baseKey)) continue;
|
|
162
|
+
const value = cleanVcardValue(rawValue);
|
|
163
|
+
if (!value) continue;
|
|
164
|
+
if (baseKey === "FN" && !nameFromFn) {
|
|
165
|
+
nameFromFn = normalizeVcardName(value);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (baseKey === "N" && !nameFromN) {
|
|
169
|
+
nameFromN = normalizeVcardName(value);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (baseKey === "TEL") {
|
|
173
|
+
const phone = normalizeVcardPhone(value);
|
|
174
|
+
if (phone) phones.push(phone);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
name: nameFromFn ?? nameFromN,
|
|
179
|
+
phones
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
function normalizeVcardKey(key) {
|
|
183
|
+
const [primary] = key.split(";");
|
|
184
|
+
if (!primary) return;
|
|
185
|
+
const segments = primary.split(".");
|
|
186
|
+
return segments[segments.length - 1] || void 0;
|
|
187
|
+
}
|
|
188
|
+
function cleanVcardValue(value) {
|
|
189
|
+
return value.replace(/\\n/gi, " ").replace(/\\,/g, ",").replace(/\\;/g, ";").trim();
|
|
190
|
+
}
|
|
191
|
+
function normalizeVcardName(value) {
|
|
192
|
+
return value.replace(/;/g, " ").replace(/\s+/g, " ").trim();
|
|
193
|
+
}
|
|
194
|
+
function normalizeVcardPhone(value) {
|
|
195
|
+
const trimmed = value.trim();
|
|
196
|
+
if (!trimmed) return "";
|
|
197
|
+
if (normalizeLowercaseStringOrEmpty(trimmed).startsWith("tel:")) return trimmed.slice(4).trim();
|
|
198
|
+
return trimmed;
|
|
199
|
+
}
|
|
200
|
+
//#endregion
|
|
201
|
+
//#region extensions/whatsapp/src/inbound/extract.ts
|
|
202
|
+
const MESSAGE_WRAPPER_KEYS = [
|
|
203
|
+
"botInvokeMessage",
|
|
204
|
+
"ephemeralMessage",
|
|
205
|
+
"viewOnceMessage",
|
|
206
|
+
"viewOnceMessageV2",
|
|
207
|
+
"viewOnceMessageV2Extension",
|
|
208
|
+
"documentWithCaptionMessage",
|
|
209
|
+
"groupMentionedMessage"
|
|
210
|
+
];
|
|
211
|
+
const MESSAGE_CONTENT_KEYS = [
|
|
212
|
+
"conversation",
|
|
213
|
+
"extendedTextMessage",
|
|
214
|
+
"imageMessage",
|
|
215
|
+
"videoMessage",
|
|
216
|
+
"audioMessage",
|
|
217
|
+
"documentMessage",
|
|
218
|
+
"stickerMessage",
|
|
219
|
+
"locationMessage",
|
|
220
|
+
"liveLocationMessage",
|
|
221
|
+
"contactMessage",
|
|
222
|
+
"contactsArrayMessage",
|
|
223
|
+
"buttonsResponseMessage",
|
|
224
|
+
"listResponseMessage",
|
|
225
|
+
"templateButtonReplyMessage",
|
|
226
|
+
"interactiveResponseMessage",
|
|
227
|
+
"buttonsMessage",
|
|
228
|
+
"listMessage"
|
|
229
|
+
];
|
|
230
|
+
function fallbackNormalizeMessageContent(message) {
|
|
231
|
+
let current = message;
|
|
232
|
+
while (current && typeof current === "object") {
|
|
233
|
+
let unwrapped = false;
|
|
234
|
+
for (const key of MESSAGE_WRAPPER_KEYS) {
|
|
235
|
+
const candidate = current[key];
|
|
236
|
+
if (candidate && typeof candidate === "object" && "message" in candidate && candidate.message) {
|
|
237
|
+
current = candidate.message;
|
|
238
|
+
unwrapped = true;
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (!unwrapped) break;
|
|
243
|
+
}
|
|
244
|
+
return current;
|
|
245
|
+
}
|
|
246
|
+
function normalizeMessage(message) {
|
|
247
|
+
if (typeof normalizeMessageContent === "function") return normalizeMessageContent(message);
|
|
248
|
+
return fallbackNormalizeMessageContent(message);
|
|
249
|
+
}
|
|
250
|
+
function fallbackGetContentType(message) {
|
|
251
|
+
const normalized = fallbackNormalizeMessageContent(message);
|
|
252
|
+
if (!normalized || typeof normalized !== "object") return;
|
|
253
|
+
for (const key of MESSAGE_CONTENT_KEYS) if (normalized[key] != null) return key;
|
|
254
|
+
}
|
|
255
|
+
function getMessageContentType(message) {
|
|
256
|
+
if (typeof getContentType === "function") return getContentType(message);
|
|
257
|
+
return fallbackGetContentType(message);
|
|
258
|
+
}
|
|
259
|
+
function extractMessage(message) {
|
|
260
|
+
if (typeof extractMessageContent === "function") return extractMessageContent(message);
|
|
261
|
+
const normalized = fallbackNormalizeMessageContent(message);
|
|
262
|
+
const contentType = fallbackGetContentType(normalized);
|
|
263
|
+
if (!normalized || !contentType || contentType === "conversation") return normalized;
|
|
264
|
+
const candidate = normalized[contentType];
|
|
265
|
+
return candidate && typeof candidate === "object" ? candidate : normalized;
|
|
266
|
+
}
|
|
267
|
+
function getFutureProofInnerMessage(message) {
|
|
268
|
+
const contentType = getMessageContentType(message);
|
|
269
|
+
const candidate = contentType ? message[contentType] : void 0;
|
|
270
|
+
if (candidate && typeof candidate === "object" && "message" in candidate && candidate.message && typeof candidate.message === "object") {
|
|
271
|
+
const inner = normalizeMessage(candidate.message);
|
|
272
|
+
if (inner) {
|
|
273
|
+
const innerType = getMessageContentType(inner);
|
|
274
|
+
if (innerType && innerType !== contentType) return inner;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
function buildMessageChain(message) {
|
|
279
|
+
const chain = [];
|
|
280
|
+
let current = normalizeMessage(message);
|
|
281
|
+
while (current && chain.length < 4) {
|
|
282
|
+
chain.push(current);
|
|
283
|
+
current = getFutureProofInnerMessage(current);
|
|
284
|
+
}
|
|
285
|
+
return chain;
|
|
286
|
+
}
|
|
287
|
+
function unwrapMessage$1(message) {
|
|
288
|
+
return buildMessageChain(message).at(-1);
|
|
289
|
+
}
|
|
290
|
+
function extractContextInfoFromMessage(message) {
|
|
291
|
+
const contentType = getMessageContentType(message);
|
|
292
|
+
const candidate = contentType ? message[contentType] : void 0;
|
|
293
|
+
const contextInfo = candidate && typeof candidate === "object" && "contextInfo" in candidate ? candidate.contextInfo : void 0;
|
|
294
|
+
if (contextInfo) return contextInfo;
|
|
295
|
+
const fallback = message.extendedTextMessage?.contextInfo ?? message.imageMessage?.contextInfo ?? message.videoMessage?.contextInfo ?? message.documentMessage?.contextInfo ?? message.audioMessage?.contextInfo ?? message.stickerMessage?.contextInfo ?? message.buttonsResponseMessage?.contextInfo ?? message.listResponseMessage?.contextInfo ?? message.templateButtonReplyMessage?.contextInfo ?? message.interactiveResponseMessage?.contextInfo ?? message.buttonsMessage?.contextInfo ?? message.listMessage?.contextInfo;
|
|
296
|
+
if (fallback) return fallback;
|
|
297
|
+
for (const value of Object.values(message)) {
|
|
298
|
+
if (!value || typeof value !== "object") continue;
|
|
299
|
+
if ("contextInfo" in value) {
|
|
300
|
+
const candidateContext = value.contextInfo;
|
|
301
|
+
if (candidateContext) return candidateContext;
|
|
302
|
+
}
|
|
303
|
+
if ("message" in value) {
|
|
304
|
+
const inner = value.message;
|
|
305
|
+
if (inner) {
|
|
306
|
+
const innerCtx = extractContextInfo(inner);
|
|
307
|
+
if (innerCtx) return innerCtx;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
function extractContextInfo(message) {
|
|
313
|
+
for (const candidate of buildMessageChain(message)) {
|
|
314
|
+
const contextInfo = extractContextInfoFromMessage(candidate);
|
|
315
|
+
if (contextInfo) return contextInfo;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
function extractMentionedJids(rawMessage) {
|
|
319
|
+
const message = unwrapMessage$1(rawMessage);
|
|
320
|
+
if (!message) return;
|
|
321
|
+
const flattened = [
|
|
322
|
+
message.extendedTextMessage?.contextInfo?.mentionedJid,
|
|
323
|
+
message.imageMessage?.contextInfo?.mentionedJid,
|
|
324
|
+
message.videoMessage?.contextInfo?.mentionedJid,
|
|
325
|
+
message.documentMessage?.contextInfo?.mentionedJid,
|
|
326
|
+
message.audioMessage?.contextInfo?.mentionedJid,
|
|
327
|
+
message.stickerMessage?.contextInfo?.mentionedJid,
|
|
328
|
+
message.buttonsResponseMessage?.contextInfo?.mentionedJid,
|
|
329
|
+
message.listResponseMessage?.contextInfo?.mentionedJid
|
|
330
|
+
].flatMap((arr) => arr ?? []).filter(Boolean);
|
|
331
|
+
if (flattened.length === 0) return;
|
|
332
|
+
return Array.from(new Set(flattened));
|
|
333
|
+
}
|
|
334
|
+
function extractText(rawMessage) {
|
|
335
|
+
const message = unwrapMessage$1(rawMessage);
|
|
336
|
+
if (!message) return;
|
|
337
|
+
const extracted = extractMessage(message);
|
|
338
|
+
const candidates = [message, extracted && extracted !== message ? extracted : void 0];
|
|
339
|
+
for (const candidate of candidates) {
|
|
340
|
+
if (!candidate) continue;
|
|
341
|
+
if (typeof candidate.conversation === "string" && candidate.conversation.trim()) return candidate.conversation.trim();
|
|
342
|
+
const extended = candidate.extendedTextMessage?.text;
|
|
343
|
+
if (extended?.trim()) return extended.trim();
|
|
344
|
+
const caption = candidate.imageMessage?.caption ?? candidate.videoMessage?.caption ?? candidate.documentMessage?.caption;
|
|
345
|
+
if (caption?.trim()) return caption.trim();
|
|
346
|
+
}
|
|
347
|
+
const contactPlaceholder = extractContactPlaceholder(message) ?? (extracted && extracted !== message ? extractContactPlaceholder(extracted) : void 0);
|
|
348
|
+
if (contactPlaceholder) return contactPlaceholder;
|
|
349
|
+
}
|
|
350
|
+
function extractMediaPlaceholder(rawMessage) {
|
|
351
|
+
const message = unwrapMessage$1(rawMessage);
|
|
352
|
+
if (!message) return;
|
|
353
|
+
if (message.imageMessage) return "<media:image>";
|
|
354
|
+
if (message.videoMessage) return "<media:video>";
|
|
355
|
+
if (message.audioMessage) return "<media:audio>";
|
|
356
|
+
if (message.documentMessage) return "<media:document>";
|
|
357
|
+
if (message.stickerMessage) return "<media:sticker>";
|
|
358
|
+
}
|
|
359
|
+
function extractContactPlaceholder(rawMessage) {
|
|
360
|
+
const contactContext = extractContactContext(rawMessage);
|
|
361
|
+
if (!contactContext) return;
|
|
362
|
+
if (contactContext.kind === "contact") return "<contact>";
|
|
363
|
+
const suffix = contactContext.total === 1 ? "contact" : "contacts";
|
|
364
|
+
return `<contacts: ${contactContext.total} ${suffix}>`;
|
|
365
|
+
}
|
|
366
|
+
function extractContactContext(rawMessage) {
|
|
367
|
+
const message = unwrapMessage$1(rawMessage);
|
|
368
|
+
if (!message) return;
|
|
369
|
+
const contact = message.contactMessage ?? void 0;
|
|
370
|
+
if (contact) {
|
|
371
|
+
const { name, phones } = describeContact({
|
|
372
|
+
displayName: contact.displayName,
|
|
373
|
+
vcard: contact.vcard
|
|
374
|
+
});
|
|
375
|
+
return {
|
|
376
|
+
kind: "contact",
|
|
377
|
+
total: 1,
|
|
378
|
+
contacts: [{
|
|
379
|
+
name,
|
|
380
|
+
phones
|
|
381
|
+
}]
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
const contactsArray = message.contactsArrayMessage?.contacts ?? void 0;
|
|
385
|
+
if (!contactsArray || contactsArray.length === 0) return;
|
|
386
|
+
return {
|
|
387
|
+
kind: "contacts",
|
|
388
|
+
total: contactsArray.length,
|
|
389
|
+
contacts: contactsArray.map((entry) => describeContact({
|
|
390
|
+
displayName: entry.displayName,
|
|
391
|
+
vcard: entry.vcard
|
|
392
|
+
}))
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
function describeContact(input) {
|
|
396
|
+
const displayName = (input.displayName ?? "").trim();
|
|
397
|
+
const parsed = parseVcard(input.vcard ?? void 0);
|
|
398
|
+
return {
|
|
399
|
+
name: displayName || parsed.name,
|
|
400
|
+
phones: parsed.phones
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
function extractLocationData(rawMessage) {
|
|
404
|
+
const message = unwrapMessage$1(rawMessage);
|
|
405
|
+
if (!message) return null;
|
|
406
|
+
const live = message.liveLocationMessage ?? void 0;
|
|
407
|
+
if (live) {
|
|
408
|
+
const latitudeRaw = live.degreesLatitude;
|
|
409
|
+
const longitudeRaw = live.degreesLongitude;
|
|
410
|
+
if (latitudeRaw != null && longitudeRaw != null) {
|
|
411
|
+
const latitude = latitudeRaw;
|
|
412
|
+
const longitude = longitudeRaw;
|
|
413
|
+
if (Number.isFinite(latitude) && Number.isFinite(longitude)) return {
|
|
414
|
+
latitude,
|
|
415
|
+
longitude,
|
|
416
|
+
accuracy: live.accuracyInMeters ?? void 0,
|
|
417
|
+
caption: live.caption ?? void 0,
|
|
418
|
+
source: "live",
|
|
419
|
+
isLive: true
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
const location = message.locationMessage ?? void 0;
|
|
424
|
+
if (location) {
|
|
425
|
+
const latitudeRaw = location.degreesLatitude;
|
|
426
|
+
const longitudeRaw = location.degreesLongitude;
|
|
427
|
+
if (latitudeRaw != null && longitudeRaw != null) {
|
|
428
|
+
const latitude = latitudeRaw;
|
|
429
|
+
const longitude = longitudeRaw;
|
|
430
|
+
if (Number.isFinite(latitude) && Number.isFinite(longitude)) {
|
|
431
|
+
const isLive = Boolean(location.isLive);
|
|
432
|
+
return {
|
|
433
|
+
latitude,
|
|
434
|
+
longitude,
|
|
435
|
+
accuracy: location.accuracyInMeters ?? void 0,
|
|
436
|
+
name: location.name ?? void 0,
|
|
437
|
+
address: location.address ?? void 0,
|
|
438
|
+
caption: location.comment ?? void 0,
|
|
439
|
+
source: isLive ? "live" : location.name || location.address ? "place" : "pin",
|
|
440
|
+
isLive
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
function describeReplyContext(rawMessage) {
|
|
448
|
+
const message = unwrapMessage$1(rawMessage);
|
|
449
|
+
if (!message) return null;
|
|
450
|
+
const contextInfo = extractContextInfo(message);
|
|
451
|
+
const quoted = normalizeMessage(contextInfo?.quotedMessage);
|
|
452
|
+
if (!quoted) return null;
|
|
453
|
+
const location = extractLocationData(quoted);
|
|
454
|
+
const locationText = location ? formatLocationText(location) : void 0;
|
|
455
|
+
let body = [extractText(quoted), locationText].filter(Boolean).join("\n").trim();
|
|
456
|
+
if (!body) body = extractMediaPlaceholder(quoted);
|
|
457
|
+
if (!body) {
|
|
458
|
+
const quotedType = quoted ? getMessageContentType(quoted) : void 0;
|
|
459
|
+
logVerbose(`Quoted message missing extractable body${quotedType ? ` (type ${quotedType})` : ""}`);
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
462
|
+
const senderJid = contextInfo?.participant ?? void 0;
|
|
463
|
+
const sender = resolveComparableIdentity({
|
|
464
|
+
jid: senderJid,
|
|
465
|
+
label: senderJid ? jidToE164(senderJid) ?? senderJid : "unknown sender"
|
|
466
|
+
});
|
|
467
|
+
return {
|
|
468
|
+
id: contextInfo?.stanzaId || void 0,
|
|
469
|
+
body,
|
|
470
|
+
sender
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
function hasInteractiveResponseContent(message) {
|
|
474
|
+
if (!message) return false;
|
|
475
|
+
return Boolean(message.buttonsResponseMessage || message.listResponseMessage || message.templateButtonReplyMessage || message.interactiveResponseMessage);
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Fast check that a Baileys message carries user-visible inbound content
|
|
479
|
+
* (text, media, contact, location, button/list selection). Returns false for
|
|
480
|
+
* protocol/receipt/typing notifications that arrive on the same
|
|
481
|
+
* `messages.upsert` stream as real messages but should not trigger pairing
|
|
482
|
+
* access-control side effects.
|
|
483
|
+
*/
|
|
484
|
+
function hasInboundUserContent(rawMessage) {
|
|
485
|
+
if (!rawMessage) return false;
|
|
486
|
+
if (extractText(rawMessage)) return true;
|
|
487
|
+
if (extractMediaPlaceholder(rawMessage)) return true;
|
|
488
|
+
if (extractLocationData(rawMessage)) return true;
|
|
489
|
+
for (const candidate of buildMessageChain(rawMessage)) if (hasInteractiveResponseContent(candidate)) return true;
|
|
490
|
+
return false;
|
|
491
|
+
}
|
|
492
|
+
//#endregion
|
|
493
|
+
//#region extensions/whatsapp/src/inbound/lifecycle.ts
|
|
494
|
+
function attachEmitterListener(emitter, event, listener) {
|
|
495
|
+
emitter.on(event, listener);
|
|
496
|
+
return () => {
|
|
497
|
+
if (typeof emitter.off === "function") {
|
|
498
|
+
emitter.off(event, listener);
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
if (typeof emitter.removeListener === "function") emitter.removeListener(event, listener);
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
function closeInboundMonitorSocket(sock) {
|
|
505
|
+
if (typeof sock.end === "function") {
|
|
506
|
+
sock.end(/* @__PURE__ */ new Error("OpenClaw WhatsApp listener close"));
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
sock.ws?.close?.();
|
|
510
|
+
}
|
|
511
|
+
//#endregion
|
|
512
|
+
//#region extensions/whatsapp/src/inbound/media.ts
|
|
513
|
+
function unwrapMessage(message) {
|
|
514
|
+
return normalizeMessageContent$1(message);
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Resolve the MIME type for an inbound media message.
|
|
518
|
+
* Falls back to WhatsApp's standard formats when Baileys omits the MIME.
|
|
519
|
+
*/
|
|
520
|
+
function resolveMediaMimetype(message) {
|
|
521
|
+
const explicit = message.imageMessage?.mimetype ?? message.videoMessage?.mimetype ?? message.documentMessage?.mimetype ?? message.audioMessage?.mimetype ?? message.stickerMessage?.mimetype ?? void 0;
|
|
522
|
+
if (explicit) return explicit;
|
|
523
|
+
if (message.audioMessage) return "audio/ogg; codecs=opus";
|
|
524
|
+
if (message.imageMessage) return "image/jpeg";
|
|
525
|
+
if (message.videoMessage) return "video/mp4";
|
|
526
|
+
if (message.stickerMessage) return "image/webp";
|
|
527
|
+
}
|
|
528
|
+
async function downloadInboundMedia(msg, sock) {
|
|
529
|
+
const message = unwrapMessage(msg.message);
|
|
530
|
+
if (!message) return;
|
|
531
|
+
const mimetype = resolveMediaMimetype(message);
|
|
532
|
+
const fileName = message.documentMessage?.fileName ?? void 0;
|
|
533
|
+
if (!message.imageMessage && !message.videoMessage && !message.documentMessage && !message.audioMessage && !message.stickerMessage) return;
|
|
534
|
+
try {
|
|
535
|
+
return {
|
|
536
|
+
buffer: await downloadMediaMessage(msg, "buffer", {}, {
|
|
537
|
+
reuploadRequest: sock.updateMediaMessage,
|
|
538
|
+
logger: sock.logger
|
|
539
|
+
}),
|
|
540
|
+
mimetype,
|
|
541
|
+
fileName
|
|
542
|
+
};
|
|
543
|
+
} catch (err) {
|
|
544
|
+
logVerbose(`downloadMediaMessage failed: ${String(err)}`);
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
async function downloadQuotedInboundMedia(msg, sock) {
|
|
549
|
+
const contextInfo = extractContextInfo(unwrapMessage(msg.message));
|
|
550
|
+
if (!contextInfo?.quotedMessage) return;
|
|
551
|
+
const quotedMessage = contextInfo.quotedMessage;
|
|
552
|
+
return downloadInboundMedia({
|
|
553
|
+
key: {
|
|
554
|
+
id: contextInfo?.stanzaId || void 0,
|
|
555
|
+
remoteJid: contextInfo.remoteJid ?? msg.key?.remoteJid ?? void 0,
|
|
556
|
+
participant: contextInfo?.participant ?? void 0,
|
|
557
|
+
fromMe: false
|
|
558
|
+
},
|
|
559
|
+
message: quotedMessage,
|
|
560
|
+
messageTimestamp: msg.messageTimestamp
|
|
561
|
+
}, sock);
|
|
562
|
+
}
|
|
563
|
+
//#endregion
|
|
564
|
+
//#region extensions/whatsapp/src/inbound/outbound-mentions.ts
|
|
565
|
+
const CODE_FENCE_RE = /```[\s\S]*?```/g;
|
|
566
|
+
const INLINE_CODE_RE = /`[^`\n]+`/g;
|
|
567
|
+
const OUTBOUND_MENTION_RE = /@(\+?\d+)/g;
|
|
568
|
+
const KNOWN_USER_JID_RE = /^(\d+)(?::\d+)?@(s\.whatsapp\.net|hosted|lid|hosted\.lid|c\.us)$/i;
|
|
569
|
+
const PHONE_JID_DOMAIN_RE = /^(s\.whatsapp\.net|hosted|c\.us)$/i;
|
|
570
|
+
const LID_JID_DOMAIN_RE = /^(lid|hosted\.lid)$/i;
|
|
571
|
+
function isWhatsAppGroupJid(jid) {
|
|
572
|
+
return jid.endsWith("@g.us");
|
|
573
|
+
}
|
|
574
|
+
function mayContainWhatsAppOutboundMention(text) {
|
|
575
|
+
return /@\+?\d/.test(text);
|
|
576
|
+
}
|
|
577
|
+
function collectCodeRanges(text) {
|
|
578
|
+
const ranges = [];
|
|
579
|
+
for (const match of text.matchAll(CODE_FENCE_RE)) ranges.push({
|
|
580
|
+
start: match.index,
|
|
581
|
+
end: match.index + match[0].length
|
|
582
|
+
});
|
|
583
|
+
for (const match of text.matchAll(INLINE_CODE_RE)) {
|
|
584
|
+
const start = match.index;
|
|
585
|
+
if (ranges.some((range) => start >= range.start && start < range.end)) continue;
|
|
586
|
+
ranges.push({
|
|
587
|
+
start,
|
|
588
|
+
end: start + match[0].length
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
return ranges.toSorted((a, b) => a.start - b.start);
|
|
592
|
+
}
|
|
593
|
+
function isInRange(index, ranges) {
|
|
594
|
+
return ranges.some((range) => index >= range.start && index < range.end);
|
|
595
|
+
}
|
|
596
|
+
function normalizeKnownUserJid(value) {
|
|
597
|
+
const trimmed = value.replace(/^whatsapp:/i, "").trim();
|
|
598
|
+
const jidMatch = trimmed.match(KNOWN_USER_JID_RE);
|
|
599
|
+
if (jidMatch) {
|
|
600
|
+
const domain = jidMatch[2].toLowerCase() === "c.us" ? "s.whatsapp.net" : jidMatch[2].toLowerCase();
|
|
601
|
+
return `${jidMatch[1]}@${domain}`;
|
|
602
|
+
}
|
|
603
|
+
const digits = trimmed.startsWith("+") ? trimmed.replace(/\D/g, "") : /^\d+$/.test(trimmed) ? trimmed : "";
|
|
604
|
+
return digits ? `${digits}@s.whatsapp.net` : null;
|
|
605
|
+
}
|
|
606
|
+
function extractKnownJidParts(value) {
|
|
607
|
+
const normalized = normalizeKnownUserJid(value);
|
|
608
|
+
if (!normalized) return null;
|
|
609
|
+
const match = normalized.match(/^(\d+)@(.+)$/);
|
|
610
|
+
return match ? {
|
|
611
|
+
user: match[1],
|
|
612
|
+
domain: match[2]
|
|
613
|
+
} : null;
|
|
614
|
+
}
|
|
615
|
+
function extractPhoneDigits(value) {
|
|
616
|
+
if (!value) return null;
|
|
617
|
+
const trimmed = value.replace(/^whatsapp:/i, "").trim();
|
|
618
|
+
if (trimmed.startsWith("+") || /^\d+$/.test(trimmed)) return trimmed.replace(/\D/g, "") || null;
|
|
619
|
+
const parts = extractKnownJidParts(trimmed);
|
|
620
|
+
return parts && PHONE_JID_DOMAIN_RE.test(parts.domain) ? parts.user : null;
|
|
621
|
+
}
|
|
622
|
+
function extractLidDigits(value) {
|
|
623
|
+
if (!value) return null;
|
|
624
|
+
const parts = extractKnownJidParts(value);
|
|
625
|
+
return parts && LID_JID_DOMAIN_RE.test(parts.domain) ? parts.user : null;
|
|
626
|
+
}
|
|
627
|
+
function isLidJid(jid) {
|
|
628
|
+
const parts = extractKnownJidParts(jid);
|
|
629
|
+
return Boolean(parts && LID_JID_DOMAIN_RE.test(parts.domain));
|
|
630
|
+
}
|
|
631
|
+
function lidReplacementText(jid) {
|
|
632
|
+
const parts = extractKnownJidParts(jid);
|
|
633
|
+
if (!parts || !LID_JID_DOMAIN_RE.test(parts.domain)) return;
|
|
634
|
+
return `@${parts.user}`;
|
|
635
|
+
}
|
|
636
|
+
function participantValues(participant) {
|
|
637
|
+
return typeof participant === "string" ? { id: participant } : participant;
|
|
638
|
+
}
|
|
639
|
+
function chooseMentionJid(participant) {
|
|
640
|
+
const values = participantValues(participant);
|
|
641
|
+
const idJid = normalizeKnownUserJid(values.id ?? "");
|
|
642
|
+
const lidJid = normalizeKnownUserJid(values.lid ?? "");
|
|
643
|
+
return (idJid && isLidJid(idJid) ? idJid : null) ?? (lidJid && isLidJid(lidJid) ? lidJid : null) ?? idJid ?? lidJid ?? normalizeKnownUserJid(values.phoneNumber ?? "") ?? normalizeKnownUserJid(values.e164 ?? "");
|
|
644
|
+
}
|
|
645
|
+
function buildMentionTargetMaps(participants) {
|
|
646
|
+
const byPhone = /* @__PURE__ */ new Map();
|
|
647
|
+
const byLid = /* @__PURE__ */ new Map();
|
|
648
|
+
for (const participant of participants) {
|
|
649
|
+
const mentionJid = chooseMentionJid(participant);
|
|
650
|
+
if (!mentionJid) continue;
|
|
651
|
+
const target = {
|
|
652
|
+
mentionJid,
|
|
653
|
+
...isLidJid(mentionJid) ? { replacementText: lidReplacementText(mentionJid) } : {}
|
|
654
|
+
};
|
|
655
|
+
const values = participantValues(participant);
|
|
656
|
+
for (const value of [
|
|
657
|
+
values.id,
|
|
658
|
+
values.phoneNumber,
|
|
659
|
+
values.e164
|
|
660
|
+
]) {
|
|
661
|
+
const digits = extractPhoneDigits(value);
|
|
662
|
+
if (digits && !byPhone.has(digits)) byPhone.set(digits, target);
|
|
663
|
+
}
|
|
664
|
+
for (const value of [values.id, values.lid]) {
|
|
665
|
+
const digits = extractLidDigits(value);
|
|
666
|
+
if (digits && !byLid.has(digits)) byLid.set(digits, target);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
return {
|
|
670
|
+
byPhone,
|
|
671
|
+
byLid
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
function shouldSkipMentionAt(text, index, end, codeRanges) {
|
|
675
|
+
if (isInRange(index, codeRanges)) return true;
|
|
676
|
+
const previous = index > 0 ? text[index - 1] : "";
|
|
677
|
+
const next = text[end] ?? "";
|
|
678
|
+
return Boolean(previous && /[\w@]/.test(previous) || next && /[\w@]/.test(next));
|
|
679
|
+
}
|
|
680
|
+
function resolveWhatsAppOutboundMentions(params) {
|
|
681
|
+
if (!isWhatsAppGroupJid(params.chatJid) || !mayContainWhatsAppOutboundMention(params.text) || !params.participants?.length) return {
|
|
682
|
+
text: params.text,
|
|
683
|
+
mentionedJids: []
|
|
684
|
+
};
|
|
685
|
+
const { byPhone, byLid } = buildMentionTargetMaps(params.participants);
|
|
686
|
+
if (byPhone.size === 0 && byLid.size === 0) return {
|
|
687
|
+
text: params.text,
|
|
688
|
+
mentionedJids: []
|
|
689
|
+
};
|
|
690
|
+
const codeRanges = collectCodeRanges(params.text);
|
|
691
|
+
const replacements = [];
|
|
692
|
+
const mentionedJids = [];
|
|
693
|
+
const seenMentionJids = /* @__PURE__ */ new Set();
|
|
694
|
+
for (const match of params.text.matchAll(OUTBOUND_MENTION_RE)) {
|
|
695
|
+
const start = match.index;
|
|
696
|
+
const token = match[0];
|
|
697
|
+
if (shouldSkipMentionAt(params.text, start, start + token.length, codeRanges)) continue;
|
|
698
|
+
const digits = match[1].replace(/\D/g, "");
|
|
699
|
+
const target = token.startsWith("@+") ? byPhone.get(digits) ?? byLid.get(digits) : byLid.get(digits) ?? byPhone.get(digits);
|
|
700
|
+
if (!target) continue;
|
|
701
|
+
if (!seenMentionJids.has(target.mentionJid)) {
|
|
702
|
+
seenMentionJids.add(target.mentionJid);
|
|
703
|
+
mentionedJids.push(target.mentionJid);
|
|
704
|
+
}
|
|
705
|
+
if (target.replacementText && target.replacementText !== token) replacements.push({
|
|
706
|
+
start,
|
|
707
|
+
end: start + token.length,
|
|
708
|
+
text: target.replacementText
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
if (replacements.length === 0) return {
|
|
712
|
+
text: params.text,
|
|
713
|
+
mentionedJids
|
|
714
|
+
};
|
|
715
|
+
let text = "";
|
|
716
|
+
let cursor = 0;
|
|
717
|
+
for (const replacement of replacements) {
|
|
718
|
+
text += params.text.slice(cursor, replacement.start);
|
|
719
|
+
text += replacement.text;
|
|
720
|
+
cursor = replacement.end;
|
|
721
|
+
}
|
|
722
|
+
text += params.text.slice(cursor);
|
|
723
|
+
return {
|
|
724
|
+
text,
|
|
725
|
+
mentionedJids
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
function addWhatsAppOutboundMentionsToContent(content, mentionedJids) {
|
|
729
|
+
return mentionedJids.length > 0 ? {
|
|
730
|
+
...content,
|
|
731
|
+
mentions: [...mentionedJids]
|
|
732
|
+
} : content;
|
|
733
|
+
}
|
|
734
|
+
//#endregion
|
|
735
|
+
//#region extensions/whatsapp/src/inbound/send-result.ts
|
|
736
|
+
function normalizeKey(key) {
|
|
737
|
+
const id = typeof key?.id === "string" ? key.id.trim() : "";
|
|
738
|
+
if (!id) return;
|
|
739
|
+
return {
|
|
740
|
+
id,
|
|
741
|
+
remoteJid: key?.remoteJid,
|
|
742
|
+
fromMe: key?.fromMe,
|
|
743
|
+
participant: key?.participant
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
function normalizeWhatsAppSendResult(result, kind) {
|
|
747
|
+
const key = normalizeKey(result?.key);
|
|
748
|
+
return {
|
|
749
|
+
kind,
|
|
750
|
+
messageId: key?.id ?? "unknown",
|
|
751
|
+
messageIds: key ? [key.id] : [],
|
|
752
|
+
keys: key ? [key] : [],
|
|
753
|
+
providerAccepted: Boolean(key)
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
function combineWhatsAppSendResults(kind, results) {
|
|
757
|
+
const messageIds = [...new Set(results.flatMap((result) => result.messageIds))];
|
|
758
|
+
const keys = results.flatMap((result) => result.keys);
|
|
759
|
+
return {
|
|
760
|
+
kind,
|
|
761
|
+
messageId: messageIds[0] ?? "unknown",
|
|
762
|
+
messageIds,
|
|
763
|
+
keys,
|
|
764
|
+
providerAccepted: results.some((result) => result.providerAccepted)
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
//#endregion
|
|
768
|
+
//#region extensions/whatsapp/src/inbound/send-api.ts
|
|
769
|
+
function recordWhatsAppOutbound(accountId) {
|
|
770
|
+
recordChannelActivity({
|
|
771
|
+
channel: "whatsapp",
|
|
772
|
+
accountId,
|
|
773
|
+
direction: "outbound"
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
function createWebSendApi(params) {
|
|
777
|
+
const resolveMentions = async (jid, text) => params.resolveOutboundMentions ? await params.resolveOutboundMentions({
|
|
778
|
+
jid,
|
|
779
|
+
text
|
|
780
|
+
}) : {
|
|
781
|
+
text,
|
|
782
|
+
mentionedJids: []
|
|
783
|
+
};
|
|
784
|
+
return {
|
|
785
|
+
sendMessage: async (to, text, mediaBuffer, mediaType, sendOptions) => {
|
|
786
|
+
const jid = toWhatsappJid(to);
|
|
787
|
+
let payload;
|
|
788
|
+
if (mediaBuffer) mediaType ??= "application/octet-stream";
|
|
789
|
+
const shouldSendAudioText = Boolean(mediaBuffer && mediaType?.startsWith("audio/") && text.trim());
|
|
790
|
+
const resolvedPayloadText = shouldSendAudioText ? {
|
|
791
|
+
text,
|
|
792
|
+
mentionedJids: []
|
|
793
|
+
} : await resolveMentions(jid, text);
|
|
794
|
+
if (mediaBuffer && mediaType) if (mediaType.startsWith("image/")) payload = {
|
|
795
|
+
image: mediaBuffer,
|
|
796
|
+
caption: resolvedPayloadText.text || void 0,
|
|
797
|
+
mimetype: mediaType
|
|
798
|
+
};
|
|
799
|
+
else if (mediaType.startsWith("audio/")) payload = {
|
|
800
|
+
audio: mediaBuffer,
|
|
801
|
+
ptt: true,
|
|
802
|
+
mimetype: mediaType
|
|
803
|
+
};
|
|
804
|
+
else if (mediaType.startsWith("video/")) {
|
|
805
|
+
const gifPlayback = sendOptions?.gifPlayback;
|
|
806
|
+
payload = {
|
|
807
|
+
video: mediaBuffer,
|
|
808
|
+
caption: resolvedPayloadText.text || void 0,
|
|
809
|
+
mimetype: mediaType,
|
|
810
|
+
...gifPlayback ? { gifPlayback: true } : {}
|
|
811
|
+
};
|
|
812
|
+
} else payload = {
|
|
813
|
+
document: mediaBuffer,
|
|
814
|
+
fileName: sendOptions?.fileName?.trim() || "file",
|
|
815
|
+
caption: resolvedPayloadText.text || void 0,
|
|
816
|
+
mimetype: mediaType
|
|
817
|
+
};
|
|
818
|
+
else payload = { text: resolvedPayloadText.text };
|
|
819
|
+
payload = addWhatsAppOutboundMentionsToContent(payload, resolvedPayloadText.mentionedJids);
|
|
820
|
+
const quotedOpts = buildQuotedMessageOptions({
|
|
821
|
+
messageId: sendOptions?.quotedMessageKey?.id,
|
|
822
|
+
remoteJid: sendOptions?.quotedMessageKey?.remoteJid,
|
|
823
|
+
fromMe: sendOptions?.quotedMessageKey?.fromMe,
|
|
824
|
+
participant: sendOptions?.quotedMessageKey?.participant,
|
|
825
|
+
messageText: sendOptions?.quotedMessageKey?.messageText
|
|
826
|
+
});
|
|
827
|
+
const results = [normalizeWhatsAppSendResult(quotedOpts ? await params.sock.sendMessage(jid, payload, quotedOpts) : await params.sock.sendMessage(jid, payload), mediaBuffer ? "media" : "text")];
|
|
828
|
+
if (shouldSendAudioText) {
|
|
829
|
+
const resolvedAudioText = await resolveMentions(jid, text);
|
|
830
|
+
const textPayload = addWhatsAppOutboundMentionsToContent({ text: resolvedAudioText.text }, resolvedAudioText.mentionedJids);
|
|
831
|
+
const textResult = quotedOpts ? await params.sock.sendMessage(jid, textPayload, quotedOpts) : await params.sock.sendMessage(jid, textPayload);
|
|
832
|
+
results.push(normalizeWhatsAppSendResult(textResult, "text"));
|
|
833
|
+
}
|
|
834
|
+
recordWhatsAppOutbound(sendOptions?.accountId ?? params.defaultAccountId);
|
|
835
|
+
return combineWhatsAppSendResults(mediaBuffer ? "media" : "text", results);
|
|
836
|
+
},
|
|
837
|
+
sendPoll: async (to, poll) => {
|
|
838
|
+
const jid = toWhatsappJid(to);
|
|
839
|
+
const result = await params.sock.sendMessage(jid, { poll: {
|
|
840
|
+
name: poll.question,
|
|
841
|
+
values: poll.options,
|
|
842
|
+
selectableCount: poll.maxSelections ?? 1
|
|
843
|
+
} });
|
|
844
|
+
recordWhatsAppOutbound(params.defaultAccountId);
|
|
845
|
+
return normalizeWhatsAppSendResult(result, "poll");
|
|
846
|
+
},
|
|
847
|
+
sendReaction: async (chatJid, messageId, emoji, fromMe, participant) => {
|
|
848
|
+
const jid = toWhatsappJid(chatJid);
|
|
849
|
+
return normalizeWhatsAppSendResult(await params.sock.sendMessage(jid, { react: {
|
|
850
|
+
text: emoji,
|
|
851
|
+
key: {
|
|
852
|
+
remoteJid: jid,
|
|
853
|
+
id: messageId,
|
|
854
|
+
fromMe,
|
|
855
|
+
participant: participant ? toWhatsappJid(participant) : void 0
|
|
856
|
+
}
|
|
857
|
+
} }), "reaction");
|
|
858
|
+
},
|
|
859
|
+
sendComposingTo: async (to) => {
|
|
860
|
+
const jid = toWhatsappJid(to);
|
|
861
|
+
if (isWhatsAppNewsletterJid(jid)) return;
|
|
862
|
+
await params.sock.sendPresenceUpdate("composing", jid);
|
|
863
|
+
}
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
//#endregion
|
|
867
|
+
//#region extensions/whatsapp/src/inbound/monitor.ts
|
|
868
|
+
const LOGGED_OUT_STATUS = DisconnectReason$1?.loggedOut ?? 401;
|
|
869
|
+
const RECONNECT_IN_PROGRESS_ERROR = "no active socket - reconnection in progress";
|
|
870
|
+
const GROUP_META_TTL_MS = 300 * 1e3;
|
|
871
|
+
function rememberGroupMetadataCacheEntry(cache, jid, entry) {
|
|
872
|
+
if (cache.has(jid)) cache.delete(jid);
|
|
873
|
+
cache.set(jid, entry);
|
|
874
|
+
while (cache.size > 500) {
|
|
875
|
+
const oldest = cache.keys().next();
|
|
876
|
+
if (oldest.done) break;
|
|
877
|
+
cache.delete(oldest.value);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
function readGroupMetadataCacheEntry(cache, jid) {
|
|
881
|
+
const entry = cache.get(jid);
|
|
882
|
+
if (!entry) return null;
|
|
883
|
+
if (entry.expires <= Date.now()) {
|
|
884
|
+
cache.delete(jid);
|
|
885
|
+
return null;
|
|
886
|
+
}
|
|
887
|
+
cache.delete(jid);
|
|
888
|
+
cache.set(jid, entry);
|
|
889
|
+
return entry;
|
|
890
|
+
}
|
|
891
|
+
function logWhatsAppVerbose(enabled, message) {
|
|
892
|
+
if (!enabled) return;
|
|
893
|
+
defaultRuntime.log(message);
|
|
894
|
+
}
|
|
895
|
+
function isGroupJid(jid) {
|
|
896
|
+
return (typeof isJidGroup === "function" ? isJidGroup(jid) : jid.endsWith("@g.us")) === true;
|
|
897
|
+
}
|
|
898
|
+
function isRetryableSendDisconnectError(err) {
|
|
899
|
+
return /closed|reset|timed\s*out|disconnect|no active socket/i.test(formatError(err));
|
|
900
|
+
}
|
|
901
|
+
function shouldClearSocketRefAfterSendFailure(err) {
|
|
902
|
+
return /closed|reset|disconnect|no active socket/i.test(formatError(err));
|
|
903
|
+
}
|
|
904
|
+
function isNonEmptyString(value) {
|
|
905
|
+
return Boolean(value);
|
|
906
|
+
}
|
|
907
|
+
async function attachWebInboxToSocket(options) {
|
|
908
|
+
const inboundLogger = getChildLogger({ module: "web-inbound" });
|
|
909
|
+
const inboundConsoleLog = createSubsystemLogger("gateway/channels/whatsapp").child("inbound");
|
|
910
|
+
const sock = options.sock;
|
|
911
|
+
const connectedAtMs = Date.now();
|
|
912
|
+
if (options.socketRef) options.socketRef.current = sock;
|
|
913
|
+
const getCurrentSock = () => options.socketRef ? options.socketRef.current : sock;
|
|
914
|
+
const shouldRetryDisconnect = () => options.shouldRetryDisconnect?.() === true;
|
|
915
|
+
const disconnectRetryPolicy = options.disconnectRetryPolicy ?? DEFAULT_RECONNECT_POLICY;
|
|
916
|
+
const sendRetryMaxAttempts = disconnectRetryPolicy.maxAttempts > 0 ? disconnectRetryPolicy.maxAttempts : DEFAULT_RECONNECT_POLICY.maxAttempts;
|
|
917
|
+
let onCloseResolve = null;
|
|
918
|
+
const onClose = new Promise((resolve) => {
|
|
919
|
+
onCloseResolve = resolve;
|
|
920
|
+
});
|
|
921
|
+
const resolveClose = (reason) => {
|
|
922
|
+
if (!onCloseResolve) return;
|
|
923
|
+
const resolver = onCloseResolve;
|
|
924
|
+
onCloseResolve = null;
|
|
925
|
+
resolver(reason);
|
|
926
|
+
};
|
|
927
|
+
const presence = options.selfChatMode ? "unavailable" : "available";
|
|
928
|
+
try {
|
|
929
|
+
await sock.sendPresenceUpdate(presence);
|
|
930
|
+
logWhatsAppVerbose(options.verbose, `Sent global '${presence}' presence on connect`);
|
|
931
|
+
} catch (err) {
|
|
932
|
+
logWhatsAppVerbose(options.verbose, `Failed to send '${presence}' presence on connect: ${String(err)}`);
|
|
933
|
+
}
|
|
934
|
+
const selfIdentity = await readWebSelfIdentityForDecision(options.authDir, sock.user);
|
|
935
|
+
if (selfIdentity.outcome === "unstable") throw new WhatsAppAuthUnstableError("WhatsApp auth state is still stabilizing; retrying inbox attach.");
|
|
936
|
+
const self = selfIdentity.identity;
|
|
937
|
+
const finalizeInboundDedupe = async (entries, error) => {
|
|
938
|
+
const dedupeKeys = [...new Set(entries.map((entry) => entry.dedupeKey).filter(isNonEmptyString))];
|
|
939
|
+
if (dedupeKeys.length === 0) return;
|
|
940
|
+
if (error instanceof WhatsAppRetryableInboundError) {
|
|
941
|
+
dedupeKeys.forEach((dedupeKey) => releaseRecentInboundMessage(dedupeKey, error));
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
await Promise.all(dedupeKeys.map((dedupeKey) => commitRecentInboundMessage(dedupeKey)));
|
|
945
|
+
};
|
|
946
|
+
const debouncer = createInboundDebouncer({
|
|
947
|
+
debounceMs: options.debounceMs ?? 0,
|
|
948
|
+
buildKey: (msg) => {
|
|
949
|
+
const sender = msg.sender;
|
|
950
|
+
const senderKey = msg.chatType === "group" ? getPrimaryIdentityId(sender ?? null) ?? msg.senderJid ?? msg.senderE164 ?? msg.senderName ?? msg.from : msg.from;
|
|
951
|
+
if (!senderKey) return null;
|
|
952
|
+
const conversationKey = msg.chatType === "group" ? msg.chatId : msg.from;
|
|
953
|
+
return `${msg.accountId}:${conversationKey}:${senderKey}`;
|
|
954
|
+
},
|
|
955
|
+
shouldDebounce: options.shouldDebounce,
|
|
956
|
+
onFlush: async (entries) => {
|
|
957
|
+
const last = entries.at(-1);
|
|
958
|
+
if (!last) return;
|
|
959
|
+
try {
|
|
960
|
+
if (entries.length === 1) {
|
|
961
|
+
await options.onMessage(last);
|
|
962
|
+
await finalizeInboundDedupe(entries);
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
const mentioned = /* @__PURE__ */ new Set();
|
|
966
|
+
for (const entry of entries) for (const jid of entry.mentions ?? entry.mentionedJids ?? []) mentioned.add(jid);
|
|
967
|
+
const combinedBody = entries.map((entry) => entry.body).filter(Boolean).join("\n");
|
|
968
|
+
const combinedMessage = {
|
|
969
|
+
...last,
|
|
970
|
+
body: combinedBody,
|
|
971
|
+
mentions: mentioned.size > 0 ? Array.from(mentioned) : void 0,
|
|
972
|
+
mentionedJids: mentioned.size > 0 ? Array.from(mentioned) : void 0,
|
|
973
|
+
isBatched: true
|
|
974
|
+
};
|
|
975
|
+
await options.onMessage(combinedMessage);
|
|
976
|
+
await finalizeInboundDedupe(entries);
|
|
977
|
+
} catch (error) {
|
|
978
|
+
await finalizeInboundDedupe(entries, error);
|
|
979
|
+
throw error;
|
|
980
|
+
}
|
|
981
|
+
},
|
|
982
|
+
onError: (err) => {
|
|
983
|
+
inboundLogger.error({ error: String(err) }, "failed handling inbound web message");
|
|
984
|
+
inboundConsoleLog.error(`Failed handling inbound web message: ${String(err)}`);
|
|
985
|
+
}
|
|
986
|
+
});
|
|
987
|
+
const groupMetadataCache = options.groupMetadataCache ?? /* @__PURE__ */ new Map();
|
|
988
|
+
const groupMetaCache = /* @__PURE__ */ new Map();
|
|
989
|
+
const lidLookup = sock.signalRepository?.lidMapping;
|
|
990
|
+
const resolveInboundJid = async (jid) => resolveJidToE164(jid, {
|
|
991
|
+
authDir: options.authDir,
|
|
992
|
+
lidLookup
|
|
993
|
+
});
|
|
994
|
+
const rememberOutboundMessage = (remoteJid, result) => {
|
|
995
|
+
const messageId = typeof result === "object" && result && "key" in result ? result.key?.id ?? "" : "";
|
|
996
|
+
if (!messageId) return;
|
|
997
|
+
rememberRecentOutboundMessage({
|
|
998
|
+
accountId: options.accountId,
|
|
999
|
+
remoteJid,
|
|
1000
|
+
messageId
|
|
1001
|
+
});
|
|
1002
|
+
};
|
|
1003
|
+
const sendTrackedMessage = async (jid, content, sendOptions) => {
|
|
1004
|
+
let lastErr = new Error(RECONNECT_IN_PROGRESS_ERROR);
|
|
1005
|
+
for (let attempt = 1;; attempt++) {
|
|
1006
|
+
const currentSock = getCurrentSock();
|
|
1007
|
+
if (currentSock) try {
|
|
1008
|
+
const result = sendOptions ? await currentSock.sendMessage(jid, content, sendOptions) : await currentSock.sendMessage(jid, content);
|
|
1009
|
+
rememberOutboundMessage(jid, result);
|
|
1010
|
+
return result;
|
|
1011
|
+
} catch (err) {
|
|
1012
|
+
if (!shouldRetryDisconnect() || !isRetryableSendDisconnectError(err)) throw err;
|
|
1013
|
+
lastErr = err;
|
|
1014
|
+
if (shouldClearSocketRefAfterSendFailure(err) && options.socketRef?.current === currentSock) options.socketRef.current = null;
|
|
1015
|
+
}
|
|
1016
|
+
else if (!shouldRetryDisconnect()) throw lastErr;
|
|
1017
|
+
if (attempt >= sendRetryMaxAttempts) throw lastErr;
|
|
1018
|
+
const delayMs = computeBackoff(disconnectRetryPolicy, attempt);
|
|
1019
|
+
logWhatsAppVerbose(options.verbose, `Waiting ${delayMs}ms for WhatsApp reconnect before retrying send to ${jid}: ${formatError(lastErr)}`);
|
|
1020
|
+
try {
|
|
1021
|
+
await sleepWithAbort(delayMs, options.disconnectRetryAbortSignal);
|
|
1022
|
+
} catch {
|
|
1023
|
+
throw lastErr;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
};
|
|
1027
|
+
const summarizeGroupMeta = async (meta) => {
|
|
1028
|
+
const participantEntries = await Promise.all(meta.participants?.map(async (p) => {
|
|
1029
|
+
const mapped = await resolveInboundJid(p.id);
|
|
1030
|
+
return {
|
|
1031
|
+
display: mapped ?? p.id,
|
|
1032
|
+
mention: {
|
|
1033
|
+
id: p.id,
|
|
1034
|
+
lid: p.lid,
|
|
1035
|
+
phoneNumber: p.phoneNumber,
|
|
1036
|
+
e164: mapped
|
|
1037
|
+
}
|
|
1038
|
+
};
|
|
1039
|
+
}) ?? []);
|
|
1040
|
+
const participants = participantEntries.map((entry) => entry.display).filter(Boolean);
|
|
1041
|
+
const mentionParticipants = participantEntries.map((entry) => entry.mention);
|
|
1042
|
+
return {
|
|
1043
|
+
subject: meta.subject,
|
|
1044
|
+
participants,
|
|
1045
|
+
mentionParticipants,
|
|
1046
|
+
expires: Date.now() + GROUP_META_TTL_MS
|
|
1047
|
+
};
|
|
1048
|
+
};
|
|
1049
|
+
const summarizeGroupMetaForReconnectCache = (meta) => ({
|
|
1050
|
+
subject: meta.subject,
|
|
1051
|
+
expires: Date.now() + GROUP_META_TTL_MS
|
|
1052
|
+
});
|
|
1053
|
+
const getGroupMeta = async (jid) => {
|
|
1054
|
+
const cached = readGroupMetadataCacheEntry(groupMetaCache, jid);
|
|
1055
|
+
if (cached) return cached;
|
|
1056
|
+
try {
|
|
1057
|
+
const entry = await summarizeGroupMeta(await (getCurrentSock() ?? sock).groupMetadata(jid));
|
|
1058
|
+
rememberGroupMetadataCacheEntry(groupMetadataCache, jid, {
|
|
1059
|
+
subject: entry.subject,
|
|
1060
|
+
expires: entry.expires
|
|
1061
|
+
});
|
|
1062
|
+
rememberGroupMetadataCacheEntry(groupMetaCache, jid, entry);
|
|
1063
|
+
return entry;
|
|
1064
|
+
} catch (err) {
|
|
1065
|
+
const hydrated = readGroupMetadataCacheEntry(groupMetadataCache, jid);
|
|
1066
|
+
if (hydrated) {
|
|
1067
|
+
rememberGroupMetadataCacheEntry(groupMetaCache, jid, hydrated);
|
|
1068
|
+
logWhatsAppVerbose(options.verbose, `Using cached group metadata for ${jid} after fetch failure: ${String(err)}`);
|
|
1069
|
+
return hydrated;
|
|
1070
|
+
}
|
|
1071
|
+
logWhatsAppVerbose(options.verbose, `Failed to fetch group metadata for ${jid}: ${String(err)}`);
|
|
1072
|
+
return { expires: Date.now() + GROUP_META_TTL_MS };
|
|
1073
|
+
}
|
|
1074
|
+
};
|
|
1075
|
+
const resolveOutboundMentionsForGroup = async (jid, text) => {
|
|
1076
|
+
if (!isGroupJid(jid) || !mayContainWhatsAppOutboundMention(text)) return {
|
|
1077
|
+
text,
|
|
1078
|
+
mentionedJids: []
|
|
1079
|
+
};
|
|
1080
|
+
return resolveWhatsAppOutboundMentions({
|
|
1081
|
+
chatJid: jid,
|
|
1082
|
+
text,
|
|
1083
|
+
participants: (await getGroupMeta(jid)).mentionParticipants
|
|
1084
|
+
});
|
|
1085
|
+
};
|
|
1086
|
+
const applyOutboundMentionsToContent = async (jid, content) => {
|
|
1087
|
+
if ("text" in content && typeof content.text === "string") {
|
|
1088
|
+
const resolved = await resolveOutboundMentionsForGroup(jid, content.text);
|
|
1089
|
+
return addWhatsAppOutboundMentionsToContent({
|
|
1090
|
+
...content,
|
|
1091
|
+
text: resolved.text
|
|
1092
|
+
}, resolved.mentionedJids);
|
|
1093
|
+
}
|
|
1094
|
+
const caption = content.caption;
|
|
1095
|
+
if (typeof caption === "string") {
|
|
1096
|
+
const resolved = await resolveOutboundMentionsForGroup(jid, caption);
|
|
1097
|
+
return addWhatsAppOutboundMentionsToContent({
|
|
1098
|
+
...content,
|
|
1099
|
+
caption: resolved.text
|
|
1100
|
+
}, resolved.mentionedJids);
|
|
1101
|
+
}
|
|
1102
|
+
return content;
|
|
1103
|
+
};
|
|
1104
|
+
const normalizeInboundMessage = async (msg) => {
|
|
1105
|
+
const id = msg.key?.id ?? void 0;
|
|
1106
|
+
const remoteJid = msg.key?.remoteJid;
|
|
1107
|
+
if (!remoteJid) return null;
|
|
1108
|
+
if (remoteJid.endsWith("@status") || remoteJid.endsWith("@broadcast")) return null;
|
|
1109
|
+
const group = isGroupJid(remoteJid);
|
|
1110
|
+
if (Boolean(msg.key?.fromMe) && id && isRecentOutboundMessage({
|
|
1111
|
+
accountId: options.accountId,
|
|
1112
|
+
remoteJid,
|
|
1113
|
+
messageId: id
|
|
1114
|
+
})) {
|
|
1115
|
+
logWhatsAppVerbose(options.verbose, `Skipping recent outbound WhatsApp echo ${id} for ${remoteJid}`);
|
|
1116
|
+
return null;
|
|
1117
|
+
}
|
|
1118
|
+
if (!hasInboundUserContent(msg.message ?? void 0)) return null;
|
|
1119
|
+
const participantJid = msg.key?.participant ?? void 0;
|
|
1120
|
+
const from = group ? remoteJid : await resolveInboundJid(remoteJid);
|
|
1121
|
+
if (!from) return null;
|
|
1122
|
+
const senderE164 = group ? participantJid ? await resolveInboundJid(participantJid) : null : from;
|
|
1123
|
+
let groupSubject;
|
|
1124
|
+
let groupParticipants;
|
|
1125
|
+
if (group) {
|
|
1126
|
+
const meta = await getGroupMeta(remoteJid);
|
|
1127
|
+
groupSubject = meta.subject;
|
|
1128
|
+
groupParticipants = meta.participants;
|
|
1129
|
+
}
|
|
1130
|
+
const messageTimestampMs = msg.messageTimestamp ? Number(msg.messageTimestamp) * 1e3 : void 0;
|
|
1131
|
+
const access = await checkInboundAccessControl({
|
|
1132
|
+
cfg: options.cfg,
|
|
1133
|
+
accountId: options.accountId,
|
|
1134
|
+
from,
|
|
1135
|
+
selfE164: self.e164 ?? null,
|
|
1136
|
+
senderE164,
|
|
1137
|
+
group,
|
|
1138
|
+
pushName: msg.pushName ?? void 0,
|
|
1139
|
+
isFromMe: Boolean(msg.key?.fromMe),
|
|
1140
|
+
messageTimestampMs,
|
|
1141
|
+
connectedAtMs,
|
|
1142
|
+
verbose: options.verbose,
|
|
1143
|
+
sock: { sendMessage: (jid, content) => sendTrackedMessage(jid, content) },
|
|
1144
|
+
remoteJid
|
|
1145
|
+
});
|
|
1146
|
+
if (!access.allowed) return null;
|
|
1147
|
+
return {
|
|
1148
|
+
id,
|
|
1149
|
+
remoteJid,
|
|
1150
|
+
group,
|
|
1151
|
+
participantJid,
|
|
1152
|
+
from,
|
|
1153
|
+
senderE164,
|
|
1154
|
+
groupSubject,
|
|
1155
|
+
groupParticipants,
|
|
1156
|
+
messageTimestampMs,
|
|
1157
|
+
access
|
|
1158
|
+
};
|
|
1159
|
+
};
|
|
1160
|
+
const maybeMarkInboundAsRead = async (inbound) => {
|
|
1161
|
+
const { id, remoteJid, participantJid, access } = inbound;
|
|
1162
|
+
if (id && !access.isSelfChat && options.sendReadReceipts !== false) try {
|
|
1163
|
+
await sock.readMessages([{
|
|
1164
|
+
remoteJid,
|
|
1165
|
+
id,
|
|
1166
|
+
participant: participantJid,
|
|
1167
|
+
fromMe: false
|
|
1168
|
+
}]);
|
|
1169
|
+
const suffix = participantJid ? ` (participant ${participantJid})` : "";
|
|
1170
|
+
logWhatsAppVerbose(options.verbose, `Marked message ${id} as read for ${remoteJid}${suffix}`);
|
|
1171
|
+
} catch (err) {
|
|
1172
|
+
logWhatsAppVerbose(options.verbose, `Failed to mark message ${id} read: ${String(err)}`);
|
|
1173
|
+
}
|
|
1174
|
+
else if (id && access.isSelfChat && options.verbose) logWhatsAppVerbose(options.verbose, `Self-chat mode: skipping read receipt for ${id}`);
|
|
1175
|
+
};
|
|
1176
|
+
const enrichInboundMessage = async (msg) => {
|
|
1177
|
+
const location = extractLocationData(msg.message ?? void 0);
|
|
1178
|
+
const locationText = location ? formatLocationText(location) : void 0;
|
|
1179
|
+
const contactContext = extractContactContext(msg.message ?? void 0);
|
|
1180
|
+
let body = extractText(msg.message ?? void 0);
|
|
1181
|
+
if (locationText) body = [body, locationText].filter(Boolean).join("\n").trim();
|
|
1182
|
+
if (!body) {
|
|
1183
|
+
body = extractMediaPlaceholder(msg.message ?? void 0);
|
|
1184
|
+
if (!body) return null;
|
|
1185
|
+
}
|
|
1186
|
+
const replyContext = describeReplyContext(msg.message);
|
|
1187
|
+
let mediaPath;
|
|
1188
|
+
let mediaType;
|
|
1189
|
+
let mediaFileName;
|
|
1190
|
+
const saveInboundMedia = async (inboundMedia) => {
|
|
1191
|
+
if (!inboundMedia) return;
|
|
1192
|
+
const maxBytes = (typeof options.mediaMaxMb === "number" && options.mediaMaxMb > 0 ? options.mediaMaxMb : 50) * 1024 * 1024;
|
|
1193
|
+
mediaPath = (await saveMediaBuffer(inboundMedia.buffer, inboundMedia.mimetype, "inbound", maxBytes, inboundMedia.fileName)).path;
|
|
1194
|
+
mediaType = inboundMedia.mimetype;
|
|
1195
|
+
mediaFileName = inboundMedia.fileName;
|
|
1196
|
+
};
|
|
1197
|
+
try {
|
|
1198
|
+
await saveInboundMedia(await downloadInboundMedia(msg, sock));
|
|
1199
|
+
if (!mediaPath && replyContext) await saveInboundMedia(await downloadQuotedInboundMedia(msg, sock));
|
|
1200
|
+
} catch (err) {
|
|
1201
|
+
logWhatsAppVerbose(options.verbose, `Inbound media download failed: ${String(err)}`);
|
|
1202
|
+
}
|
|
1203
|
+
return {
|
|
1204
|
+
body,
|
|
1205
|
+
location: location ?? void 0,
|
|
1206
|
+
contactContext,
|
|
1207
|
+
replyContext,
|
|
1208
|
+
mediaPath,
|
|
1209
|
+
mediaType,
|
|
1210
|
+
mediaFileName
|
|
1211
|
+
};
|
|
1212
|
+
};
|
|
1213
|
+
const enqueueInboundMessage = async (msg, inbound, enriched) => {
|
|
1214
|
+
const chatJid = inbound.remoteJid;
|
|
1215
|
+
const sendComposing = async () => {
|
|
1216
|
+
const currentSock = getCurrentSock();
|
|
1217
|
+
if (!currentSock) return;
|
|
1218
|
+
try {
|
|
1219
|
+
await currentSock.sendPresenceUpdate("composing", chatJid);
|
|
1220
|
+
} catch (err) {
|
|
1221
|
+
logWhatsAppVerbose(options.verbose, `Presence update failed: ${String(err)}`);
|
|
1222
|
+
}
|
|
1223
|
+
};
|
|
1224
|
+
const reply = async (text, options) => {
|
|
1225
|
+
const resolved = await resolveOutboundMentionsForGroup(chatJid, text);
|
|
1226
|
+
return normalizeWhatsAppSendResult(await sendTrackedMessage(chatJid, addWhatsAppOutboundMentionsToContent({ text: resolved.text }, resolved.mentionedJids), options), "text");
|
|
1227
|
+
};
|
|
1228
|
+
const sendMedia = async (payload, options) => {
|
|
1229
|
+
return normalizeWhatsAppSendResult(await sendTrackedMessage(chatJid, await applyOutboundMentionsToContent(chatJid, payload), options), "media");
|
|
1230
|
+
};
|
|
1231
|
+
const timestamp = inbound.messageTimestampMs;
|
|
1232
|
+
const mentionedJids = extractMentionedJids(msg.message);
|
|
1233
|
+
const senderName = msg.pushName ?? void 0;
|
|
1234
|
+
inboundLogger.info({
|
|
1235
|
+
from: inbound.from,
|
|
1236
|
+
to: self.e164 ?? "me",
|
|
1237
|
+
body: enriched.body,
|
|
1238
|
+
mediaPath: enriched.mediaPath,
|
|
1239
|
+
mediaType: enriched.mediaType,
|
|
1240
|
+
mediaFileName: enriched.mediaFileName,
|
|
1241
|
+
timestamp
|
|
1242
|
+
}, "inbound message");
|
|
1243
|
+
const inboundMessage = {
|
|
1244
|
+
id: inbound.id,
|
|
1245
|
+
from: inbound.from,
|
|
1246
|
+
conversationId: inbound.from,
|
|
1247
|
+
to: self.e164 ?? "me",
|
|
1248
|
+
accountId: inbound.access.resolvedAccountId,
|
|
1249
|
+
accessControlPassed: true,
|
|
1250
|
+
body: enriched.body,
|
|
1251
|
+
pushName: senderName,
|
|
1252
|
+
timestamp,
|
|
1253
|
+
chatType: inbound.group ? "group" : "direct",
|
|
1254
|
+
chatId: inbound.remoteJid,
|
|
1255
|
+
sender: resolveComparableIdentity({
|
|
1256
|
+
jid: inbound.participantJid,
|
|
1257
|
+
e164: inbound.senderE164 ?? void 0,
|
|
1258
|
+
name: senderName
|
|
1259
|
+
}),
|
|
1260
|
+
senderJid: inbound.participantJid,
|
|
1261
|
+
senderE164: inbound.senderE164 ?? void 0,
|
|
1262
|
+
senderName,
|
|
1263
|
+
replyTo: enriched.replyContext ?? void 0,
|
|
1264
|
+
replyToId: enriched.replyContext?.id,
|
|
1265
|
+
replyToBody: enriched.replyContext?.body,
|
|
1266
|
+
replyToSender: enriched.replyContext?.sender?.label ?? void 0,
|
|
1267
|
+
replyToSenderJid: enriched.replyContext?.sender?.jid ?? void 0,
|
|
1268
|
+
replyToSenderE164: enriched.replyContext?.sender?.e164 ?? void 0,
|
|
1269
|
+
groupSubject: inbound.groupSubject,
|
|
1270
|
+
groupParticipants: inbound.groupParticipants,
|
|
1271
|
+
mentions: mentionedJids ?? void 0,
|
|
1272
|
+
mentionedJids: mentionedJids ?? void 0,
|
|
1273
|
+
self,
|
|
1274
|
+
selfJid: self.jid ?? void 0,
|
|
1275
|
+
selfLid: self.lid ?? void 0,
|
|
1276
|
+
selfE164: self.e164 ?? void 0,
|
|
1277
|
+
fromMe: Boolean(msg.key?.fromMe),
|
|
1278
|
+
location: enriched.location ?? void 0,
|
|
1279
|
+
untrustedStructuredContext: enriched.contactContext ? [{
|
|
1280
|
+
label: "WhatsApp contact",
|
|
1281
|
+
source: "whatsapp",
|
|
1282
|
+
type: enriched.contactContext.kind,
|
|
1283
|
+
payload: enriched.contactContext
|
|
1284
|
+
}] : void 0,
|
|
1285
|
+
sendComposing,
|
|
1286
|
+
reply,
|
|
1287
|
+
sendMedia,
|
|
1288
|
+
mediaPath: enriched.mediaPath,
|
|
1289
|
+
mediaType: enriched.mediaType,
|
|
1290
|
+
mediaFileName: enriched.mediaFileName,
|
|
1291
|
+
dedupeKey: inbound.id ? `${options.accountId}:${inbound.remoteJid}:${inbound.id}` : void 0
|
|
1292
|
+
};
|
|
1293
|
+
if (inboundMessage.id) cacheInboundMessageMeta(inboundMessage.accountId, inboundMessage.chatId, inboundMessage.id, {
|
|
1294
|
+
participant: inboundMessage.senderJid,
|
|
1295
|
+
participantE164: inboundMessage.chatType === "direct" ? inboundMessage.senderE164 : void 0,
|
|
1296
|
+
body: inboundMessage.body,
|
|
1297
|
+
fromMe: inboundMessage.fromMe
|
|
1298
|
+
});
|
|
1299
|
+
try {
|
|
1300
|
+
Promise.resolve(debouncer.enqueue(inboundMessage)).catch((err) => {
|
|
1301
|
+
inboundLogger.error({ error: String(err) }, "failed handling inbound web message");
|
|
1302
|
+
inboundConsoleLog.error(`Failed handling inbound web message: ${String(err)}`);
|
|
1303
|
+
});
|
|
1304
|
+
} catch (err) {
|
|
1305
|
+
inboundLogger.error({ error: String(err) }, "failed handling inbound web message");
|
|
1306
|
+
inboundConsoleLog.error(`Failed handling inbound web message: ${String(err)}`);
|
|
1307
|
+
}
|
|
1308
|
+
};
|
|
1309
|
+
const handleMessagesUpsert = async (upsert) => {
|
|
1310
|
+
if (upsert.type !== "notify" && upsert.type !== "append") return;
|
|
1311
|
+
for (const msg of upsert.messages ?? []) {
|
|
1312
|
+
recordChannelActivity({
|
|
1313
|
+
channel: "whatsapp",
|
|
1314
|
+
accountId: options.accountId,
|
|
1315
|
+
direction: "inbound"
|
|
1316
|
+
});
|
|
1317
|
+
const inbound = await normalizeInboundMessage(msg);
|
|
1318
|
+
if (!inbound) continue;
|
|
1319
|
+
await maybeMarkInboundAsRead(inbound);
|
|
1320
|
+
if (upsert.type === "append") {
|
|
1321
|
+
const APPEND_RECENT_GRACE_MS = 6e4;
|
|
1322
|
+
const msgTsRaw = msg.messageTimestamp;
|
|
1323
|
+
const msgTsNum = msgTsRaw != null ? Number(msgTsRaw) : NaN;
|
|
1324
|
+
if ((Number.isFinite(msgTsNum) ? msgTsNum * 1e3 : 0) < connectedAtMs - APPEND_RECENT_GRACE_MS) continue;
|
|
1325
|
+
}
|
|
1326
|
+
const enriched = await enrichInboundMessage(msg);
|
|
1327
|
+
if (!enriched) continue;
|
|
1328
|
+
const dedupeKey = inbound.id ? `${options.accountId}:${inbound.remoteJid}:${inbound.id}` : "";
|
|
1329
|
+
if (dedupeKey && !await claimRecentInboundMessage(dedupeKey)) continue;
|
|
1330
|
+
await enqueueInboundMessage(msg, inbound, enriched);
|
|
1331
|
+
}
|
|
1332
|
+
};
|
|
1333
|
+
const handleConnectionUpdate = (update) => {
|
|
1334
|
+
try {
|
|
1335
|
+
if (update.connection === "close") {
|
|
1336
|
+
if (options.socketRef?.current === sock) options.socketRef.current = null;
|
|
1337
|
+
const status = getStatusCode(update.lastDisconnect?.error);
|
|
1338
|
+
resolveClose({
|
|
1339
|
+
status,
|
|
1340
|
+
isLoggedOut: status === LOGGED_OUT_STATUS,
|
|
1341
|
+
error: update.lastDisconnect?.error
|
|
1342
|
+
});
|
|
1343
|
+
}
|
|
1344
|
+
} catch (err) {
|
|
1345
|
+
inboundLogger.error({ error: String(err) }, "connection.update handler error");
|
|
1346
|
+
resolveClose({
|
|
1347
|
+
status: void 0,
|
|
1348
|
+
isLoggedOut: false,
|
|
1349
|
+
error: err
|
|
1350
|
+
});
|
|
1351
|
+
}
|
|
1352
|
+
};
|
|
1353
|
+
const detachMessagesUpsert = attachEmitterListener(sock.ev, "messages.upsert", handleMessagesUpsert);
|
|
1354
|
+
const detachConnectionUpdate = attachEmitterListener(sock.ev, "connection.update", handleConnectionUpdate);
|
|
1355
|
+
(async () => {
|
|
1356
|
+
try {
|
|
1357
|
+
const groups = await sock.groupFetchAllParticipating();
|
|
1358
|
+
for (const [jid, meta] of Object.entries(groups ?? {})) if (meta) rememberGroupMetadataCacheEntry(groupMetadataCache, jid, summarizeGroupMetaForReconnectCache(meta));
|
|
1359
|
+
logWhatsAppVerbose(options.verbose, `Hydrated ${Object.keys(groups ?? {}).length} participating groups on connect`);
|
|
1360
|
+
} catch (err) {
|
|
1361
|
+
const error = String(err);
|
|
1362
|
+
inboundLogger.warn({ error }, "failed hydrating participating groups on connect");
|
|
1363
|
+
inboundConsoleLog.warn(`Failed hydrating participating groups on connect: ${error}`);
|
|
1364
|
+
logWhatsAppVerbose(options.verbose, `Failed to hydrate participating groups on connect: ${error}`);
|
|
1365
|
+
}
|
|
1366
|
+
})();
|
|
1367
|
+
return {
|
|
1368
|
+
close: async () => {
|
|
1369
|
+
try {
|
|
1370
|
+
detachMessagesUpsert();
|
|
1371
|
+
detachConnectionUpdate();
|
|
1372
|
+
closeInboundMonitorSocket(sock);
|
|
1373
|
+
} catch (err) {
|
|
1374
|
+
logWhatsAppVerbose(options.verbose, `Socket close failed: ${String(err)}`);
|
|
1375
|
+
}
|
|
1376
|
+
},
|
|
1377
|
+
onClose,
|
|
1378
|
+
signalClose: (reason) => {
|
|
1379
|
+
resolveClose(reason ?? {
|
|
1380
|
+
status: void 0,
|
|
1381
|
+
isLoggedOut: false,
|
|
1382
|
+
error: "closed"
|
|
1383
|
+
});
|
|
1384
|
+
},
|
|
1385
|
+
...createWebSendApi({
|
|
1386
|
+
sock: {
|
|
1387
|
+
sendMessage: (jid, content, options) => sendTrackedMessage(jid, content, options),
|
|
1388
|
+
sendPresenceUpdate: async (presence, jid) => {
|
|
1389
|
+
const currentSock = getCurrentSock();
|
|
1390
|
+
if (!currentSock) throw new Error(RECONNECT_IN_PROGRESS_ERROR);
|
|
1391
|
+
return currentSock.sendPresenceUpdate(presence, jid);
|
|
1392
|
+
}
|
|
1393
|
+
},
|
|
1394
|
+
defaultAccountId: options.accountId,
|
|
1395
|
+
resolveOutboundMentions: ({ jid, text }) => resolveOutboundMentionsForGroup(jid, text)
|
|
1396
|
+
})
|
|
1397
|
+
};
|
|
1398
|
+
}
|
|
1399
|
+
async function monitorWebInbox(options) {
|
|
1400
|
+
const sock = await createWaSocket(false, options.verbose, {
|
|
1401
|
+
authDir: options.authDir,
|
|
1402
|
+
...resolveWhatsAppSocketTiming(options.cfg)
|
|
1403
|
+
});
|
|
1404
|
+
await waitForWaConnection(sock);
|
|
1405
|
+
return attachWebInboxToSocket({
|
|
1406
|
+
...options,
|
|
1407
|
+
sock
|
|
1408
|
+
});
|
|
1409
|
+
}
|
|
1410
|
+
//#endregion
|
|
1411
|
+
//#region extensions/whatsapp/src/auto-reply/loggers.ts
|
|
1412
|
+
const whatsappLog = createSubsystemLogger("gateway/channels/whatsapp");
|
|
1413
|
+
const whatsappInboundLog = whatsappLog.child("inbound");
|
|
1414
|
+
const whatsappOutboundLog = whatsappLog.child("outbound");
|
|
1415
|
+
const whatsappHeartbeatLog = whatsappLog.child("heartbeat");
|
|
1416
|
+
//#endregion
|
|
1417
|
+
//#region extensions/whatsapp/src/auto-reply/mentions.ts
|
|
1418
|
+
function buildMentionConfig(cfg, agentId) {
|
|
1419
|
+
return {
|
|
1420
|
+
mentionRegexes: buildMentionRegexes(cfg, agentId),
|
|
1421
|
+
allowFrom: cfg.channels?.whatsapp?.allowFrom
|
|
1422
|
+
};
|
|
1423
|
+
}
|
|
1424
|
+
function resolveMentionTargets(msg, authDir) {
|
|
1425
|
+
return {
|
|
1426
|
+
normalizedMentions: getMentionIdentities(msg, authDir),
|
|
1427
|
+
self: getSelfIdentity(msg, authDir)
|
|
1428
|
+
};
|
|
1429
|
+
}
|
|
1430
|
+
function isBotMentionedFromTargets(msg, mentionCfg, targets) {
|
|
1431
|
+
const clean = (text) => normalizeMentionText(text);
|
|
1432
|
+
const explicitSelfChatOverride = typeof mentionCfg.isSelfChat === "boolean";
|
|
1433
|
+
const isGroupConversation = isWhatsAppGroupJid$1(msg.from);
|
|
1434
|
+
const isSelfChat = explicitSelfChatOverride ? Boolean(mentionCfg.isSelfChat) : isSelfChatMode(targets.self.e164, mentionCfg.allowFrom) && !isGroupConversation;
|
|
1435
|
+
const hasMentions = targets.normalizedMentions.length > 0;
|
|
1436
|
+
if (hasMentions && !isSelfChat) {
|
|
1437
|
+
for (const mention of targets.normalizedMentions) if (identitiesOverlap(targets.self, mention)) return true;
|
|
1438
|
+
return false;
|
|
1439
|
+
} else if (hasMentions && isSelfChat) {}
|
|
1440
|
+
const bodyClean = clean(msg.body);
|
|
1441
|
+
if (mentionCfg.mentionRegexes.some((re) => re.test(bodyClean))) return true;
|
|
1442
|
+
if (targets.self.e164) {
|
|
1443
|
+
const selfDigits = targets.self.e164.replace(/\D/g, "");
|
|
1444
|
+
if (selfDigits) {
|
|
1445
|
+
if (bodyClean.replace(/[^\d]/g, "").includes(selfDigits)) return true;
|
|
1446
|
+
const bodyNoSpace = msg.body.replace(/[\s-]/g, "");
|
|
1447
|
+
if (new RegExp(`\\+?${selfDigits}`, "i").test(bodyNoSpace)) return true;
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
return false;
|
|
1451
|
+
}
|
|
1452
|
+
function debugMention(msg, mentionCfg, authDir) {
|
|
1453
|
+
const mentionTargets = resolveMentionTargets(msg, authDir);
|
|
1454
|
+
return {
|
|
1455
|
+
wasMentioned: isBotMentionedFromTargets(msg, mentionCfg, mentionTargets),
|
|
1456
|
+
details: {
|
|
1457
|
+
from: msg.from,
|
|
1458
|
+
body: msg.body,
|
|
1459
|
+
bodyClean: normalizeMentionText(msg.body),
|
|
1460
|
+
mentionedJids: msg.mentions ?? msg.mentionedJids ?? null,
|
|
1461
|
+
normalizedMentionedJids: mentionTargets.normalizedMentions.length ? mentionTargets.normalizedMentions.map((identity) => getComparableIdentityValues(identity)) : null,
|
|
1462
|
+
selfJid: msg.self?.jid ?? msg.selfJid ?? null,
|
|
1463
|
+
selfLid: msg.self?.lid ?? msg.selfLid ?? null,
|
|
1464
|
+
selfE164: msg.self?.e164 ?? msg.selfE164 ?? null,
|
|
1465
|
+
resolvedSelf: mentionTargets.self
|
|
1466
|
+
}
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1469
|
+
function resolveOwnerList(mentionCfg, selfE164) {
|
|
1470
|
+
const allowFrom = mentionCfg.allowFrom;
|
|
1471
|
+
return (Array.isArray(allowFrom) && allowFrom.length > 0 ? allowFrom : selfE164 ? [selfE164] : []).filter((entry) => Boolean(entry && entry !== "*")).map((entry) => (0, text_runtime_exports.normalizeE164)(entry)).filter((entry) => Boolean(entry));
|
|
1472
|
+
}
|
|
1473
|
+
//#endregion
|
|
1474
|
+
//#region extensions/whatsapp/src/auto-reply/monitor-state.ts
|
|
1475
|
+
function cloneStatus(status) {
|
|
1476
|
+
return {
|
|
1477
|
+
...status,
|
|
1478
|
+
lastDisconnect: status.lastDisconnect ? { ...status.lastDisconnect } : null
|
|
1479
|
+
};
|
|
1480
|
+
}
|
|
1481
|
+
function isTerminalHealthState(healthState) {
|
|
1482
|
+
return healthState === "conflict" || healthState === "logged-out" || healthState === "stopped";
|
|
1483
|
+
}
|
|
1484
|
+
function createWebChannelStatusController(statusSink) {
|
|
1485
|
+
const status = {
|
|
1486
|
+
running: true,
|
|
1487
|
+
connected: false,
|
|
1488
|
+
reconnectAttempts: 0,
|
|
1489
|
+
lastConnectedAt: null,
|
|
1490
|
+
lastDisconnect: null,
|
|
1491
|
+
lastInboundAt: null,
|
|
1492
|
+
lastMessageAt: null,
|
|
1493
|
+
lastEventAt: null,
|
|
1494
|
+
lastError: null,
|
|
1495
|
+
healthState: "starting"
|
|
1496
|
+
};
|
|
1497
|
+
const emit = () => {
|
|
1498
|
+
statusSink?.(cloneStatus(status));
|
|
1499
|
+
};
|
|
1500
|
+
return {
|
|
1501
|
+
emit,
|
|
1502
|
+
snapshot: () => status,
|
|
1503
|
+
noteConnected(at = Date.now()) {
|
|
1504
|
+
Object.assign(status, createConnectedChannelStatusPatch(at));
|
|
1505
|
+
Object.assign(status, createTransportActivityStatusPatch(at));
|
|
1506
|
+
status.lastError = null;
|
|
1507
|
+
status.healthState = "healthy";
|
|
1508
|
+
emit();
|
|
1509
|
+
},
|
|
1510
|
+
noteInbound(at = Date.now()) {
|
|
1511
|
+
status.lastInboundAt = at;
|
|
1512
|
+
status.lastMessageAt = at;
|
|
1513
|
+
status.lastEventAt = at;
|
|
1514
|
+
Object.assign(status, createTransportActivityStatusPatch(at));
|
|
1515
|
+
if (status.connected) status.healthState = "healthy";
|
|
1516
|
+
emit();
|
|
1517
|
+
},
|
|
1518
|
+
noteTransportActivity(at = Date.now()) {
|
|
1519
|
+
if (status.lastTransportActivityAt === at) return;
|
|
1520
|
+
Object.assign(status, createTransportActivityStatusPatch(at));
|
|
1521
|
+
emit();
|
|
1522
|
+
},
|
|
1523
|
+
noteWatchdogStale(at = Date.now()) {
|
|
1524
|
+
status.lastEventAt = at;
|
|
1525
|
+
if (status.connected) status.healthState = "stale";
|
|
1526
|
+
emit();
|
|
1527
|
+
},
|
|
1528
|
+
noteReconnectAttempts(reconnectAttempts) {
|
|
1529
|
+
status.reconnectAttempts = reconnectAttempts;
|
|
1530
|
+
emit();
|
|
1531
|
+
},
|
|
1532
|
+
noteClose(params) {
|
|
1533
|
+
const at = params.at ?? Date.now();
|
|
1534
|
+
status.connected = false;
|
|
1535
|
+
status.lastEventAt = at;
|
|
1536
|
+
status.lastDisconnect = {
|
|
1537
|
+
at,
|
|
1538
|
+
status: params.statusCode,
|
|
1539
|
+
error: params.error,
|
|
1540
|
+
loggedOut: Boolean(params.loggedOut)
|
|
1541
|
+
};
|
|
1542
|
+
status.lastError = params.error ?? null;
|
|
1543
|
+
status.reconnectAttempts = params.reconnectAttempts;
|
|
1544
|
+
status.healthState = params.healthState;
|
|
1545
|
+
emit();
|
|
1546
|
+
},
|
|
1547
|
+
markStopped(at = Date.now()) {
|
|
1548
|
+
status.running = false;
|
|
1549
|
+
status.connected = false;
|
|
1550
|
+
status.lastEventAt = at;
|
|
1551
|
+
if (!isTerminalHealthState(status.healthState)) status.healthState = "stopped";
|
|
1552
|
+
emit();
|
|
1553
|
+
}
|
|
1554
|
+
};
|
|
1555
|
+
}
|
|
1556
|
+
//#endregion
|
|
1557
|
+
//#region extensions/whatsapp/src/auto-reply/monitor/echo.ts
|
|
1558
|
+
function createEchoTracker(params) {
|
|
1559
|
+
const recentlySent = /* @__PURE__ */ new Set();
|
|
1560
|
+
const maxItems = Math.max(1, params.maxItems ?? 100);
|
|
1561
|
+
const buildCombinedKey = (p) => `combined:${p.sessionKey}:${p.combinedBody}`;
|
|
1562
|
+
const trim = () => {
|
|
1563
|
+
while (recentlySent.size > maxItems) {
|
|
1564
|
+
const firstKey = recentlySent.values().next().value;
|
|
1565
|
+
if (!firstKey) break;
|
|
1566
|
+
recentlySent.delete(firstKey);
|
|
1567
|
+
}
|
|
1568
|
+
};
|
|
1569
|
+
const rememberText = (text, opts) => {
|
|
1570
|
+
if (!text) return;
|
|
1571
|
+
recentlySent.add(text);
|
|
1572
|
+
if (opts.combinedBody && opts.combinedBodySessionKey) recentlySent.add(buildCombinedKey({
|
|
1573
|
+
sessionKey: opts.combinedBodySessionKey,
|
|
1574
|
+
combinedBody: opts.combinedBody
|
|
1575
|
+
}));
|
|
1576
|
+
if (opts.logVerboseMessage) params.logVerbose?.(`Added to echo detection set (size now: ${recentlySent.size}): ${text.slice(0, 50)}...`);
|
|
1577
|
+
trim();
|
|
1578
|
+
};
|
|
1579
|
+
return {
|
|
1580
|
+
rememberText,
|
|
1581
|
+
has: (key) => recentlySent.has(key),
|
|
1582
|
+
forget: (key) => {
|
|
1583
|
+
recentlySent.delete(key);
|
|
1584
|
+
},
|
|
1585
|
+
buildCombinedKey
|
|
1586
|
+
};
|
|
1587
|
+
}
|
|
1588
|
+
//#endregion
|
|
1589
|
+
//#region extensions/whatsapp/src/group-session-key.ts
|
|
1590
|
+
function resolveWhatsAppGroupAccountThreadId(accountId) {
|
|
1591
|
+
return `whatsapp-account-${normalizeAccountId$1(accountId)}`;
|
|
1592
|
+
}
|
|
1593
|
+
function resolveWhatsAppLegacyGroupSessionKey(params) {
|
|
1594
|
+
const accountId = normalizeAccountId$1(params.accountId);
|
|
1595
|
+
if (!accountId || accountId === DEFAULT_ACCOUNT_ID$1 || !params.sessionKey.includes(":group:")) return null;
|
|
1596
|
+
const suffix = `:thread:${resolveWhatsAppGroupAccountThreadId(accountId)}`;
|
|
1597
|
+
return params.sessionKey.endsWith(suffix) ? params.sessionKey.slice(0, -suffix.length) : null;
|
|
1598
|
+
}
|
|
1599
|
+
function resolveWhatsAppGroupSessionRoute(route) {
|
|
1600
|
+
if (route.accountId === DEFAULT_ACCOUNT_ID$1 || !route.sessionKey.includes(":group:")) return route;
|
|
1601
|
+
const scopedSession = resolveThreadSessionKeys({
|
|
1602
|
+
baseSessionKey: route.sessionKey,
|
|
1603
|
+
threadId: resolveWhatsAppGroupAccountThreadId(route.accountId)
|
|
1604
|
+
});
|
|
1605
|
+
return {
|
|
1606
|
+
...route,
|
|
1607
|
+
sessionKey: scopedSession.sessionKey
|
|
1608
|
+
};
|
|
1609
|
+
}
|
|
1610
|
+
//#endregion
|
|
1611
|
+
//#region extensions/whatsapp/src/auto-reply/monitor/group-activation.ts
|
|
1612
|
+
function hasNamedWhatsAppAccounts(cfg) {
|
|
1613
|
+
return Object.keys(cfg.channels?.whatsapp?.accounts ?? {}).some((accountId) => normalizeAccountId$1(accountId) !== DEFAULT_ACCOUNT_ID$1);
|
|
1614
|
+
}
|
|
1615
|
+
function isActivationOnlyEntry(entry) {
|
|
1616
|
+
return entry?.groupActivation !== void 0 && typeof entry?.sessionId !== "string" && typeof entry?.updatedAt !== "number";
|
|
1617
|
+
}
|
|
1618
|
+
async function resolveGroupActivationFor(params) {
|
|
1619
|
+
const storePath = resolveStorePath(params.cfg.session?.store, { agentId: params.agentId });
|
|
1620
|
+
const store = loadSessionStore(storePath);
|
|
1621
|
+
const legacySessionKey = resolveWhatsAppLegacyGroupSessionKey({
|
|
1622
|
+
sessionKey: params.sessionKey,
|
|
1623
|
+
accountId: params.accountId
|
|
1624
|
+
});
|
|
1625
|
+
const legacyEntry = legacySessionKey ? store[legacySessionKey] : void 0;
|
|
1626
|
+
const scopedEntry = store[params.sessionKey];
|
|
1627
|
+
const activation = (normalizeAccountId$1(params.accountId) === DEFAULT_ACCOUNT_ID$1 && hasNamedWhatsAppAccounts(params.cfg) && isActivationOnlyEntry(scopedEntry) ? void 0 : scopedEntry?.groupActivation) ?? legacyEntry?.groupActivation;
|
|
1628
|
+
if (activation !== void 0 && scopedEntry?.groupActivation === void 0) await updateSessionStore(storePath, (nextStore) => {
|
|
1629
|
+
const nextScopedEntry = nextStore[params.sessionKey];
|
|
1630
|
+
if (nextScopedEntry?.groupActivation !== void 0) return;
|
|
1631
|
+
nextStore[params.sessionKey] = {
|
|
1632
|
+
...nextScopedEntry,
|
|
1633
|
+
groupActivation: activation
|
|
1634
|
+
};
|
|
1635
|
+
});
|
|
1636
|
+
const defaultActivation = !resolveWhatsAppInboundPolicy({
|
|
1637
|
+
cfg: params.cfg,
|
|
1638
|
+
accountId: params.accountId
|
|
1639
|
+
}).resolveConversationRequireMention(params.conversationId) ? "always" : "mention";
|
|
1640
|
+
return normalizeGroupActivation(activation) ?? defaultActivation;
|
|
1641
|
+
}
|
|
1642
|
+
//#endregion
|
|
1643
|
+
//#region extensions/whatsapp/src/auto-reply/monitor/ack-reaction.ts
|
|
1644
|
+
async function maybeSendAckReaction(params) {
|
|
1645
|
+
if (!params.msg.id) return null;
|
|
1646
|
+
if (resolveWhatsAppReactionLevel({
|
|
1647
|
+
cfg: params.cfg,
|
|
1648
|
+
accountId: params.accountId
|
|
1649
|
+
}).level === "off") return null;
|
|
1650
|
+
const ackConfig = params.cfg.channels?.whatsapp?.ackReaction;
|
|
1651
|
+
const emoji = (ackConfig?.emoji ?? "").trim();
|
|
1652
|
+
const directEnabled = ackConfig?.direct ?? true;
|
|
1653
|
+
const groupMode = ackConfig?.group ?? "mentions";
|
|
1654
|
+
const conversationIdForCheck = params.msg.conversationId ?? params.msg.from;
|
|
1655
|
+
const activation = params.msg.chatType === "group" ? await resolveGroupActivationFor({
|
|
1656
|
+
cfg: params.cfg,
|
|
1657
|
+
accountId: params.accountId,
|
|
1658
|
+
agentId: params.agentId,
|
|
1659
|
+
sessionKey: params.sessionKey,
|
|
1660
|
+
conversationId: conversationIdForCheck
|
|
1661
|
+
}) : null;
|
|
1662
|
+
const shouldSendReaction = () => shouldAckReactionForWhatsApp({
|
|
1663
|
+
emoji,
|
|
1664
|
+
isDirect: params.msg.chatType === "direct",
|
|
1665
|
+
isGroup: params.msg.chatType === "group",
|
|
1666
|
+
directEnabled,
|
|
1667
|
+
groupMode,
|
|
1668
|
+
wasMentioned: params.msg.wasMentioned === true,
|
|
1669
|
+
groupActivated: activation === "always"
|
|
1670
|
+
});
|
|
1671
|
+
if (!shouldSendReaction()) return null;
|
|
1672
|
+
params.info({
|
|
1673
|
+
chatId: params.msg.chatId,
|
|
1674
|
+
messageId: params.msg.id,
|
|
1675
|
+
emoji
|
|
1676
|
+
}, "sending ack reaction");
|
|
1677
|
+
const sender = getSenderIdentity(params.msg);
|
|
1678
|
+
const reactionOptions = {
|
|
1679
|
+
verbose: params.verbose,
|
|
1680
|
+
fromMe: false,
|
|
1681
|
+
...sender.jid ? { participant: sender.jid } : {},
|
|
1682
|
+
...params.accountId ? { accountId: params.accountId } : {},
|
|
1683
|
+
cfg: params.cfg
|
|
1684
|
+
};
|
|
1685
|
+
return createAckReactionHandle({
|
|
1686
|
+
ackReactionValue: emoji,
|
|
1687
|
+
send: () => sendReactionWhatsApp(params.msg.chatId, params.msg.id, emoji, reactionOptions),
|
|
1688
|
+
remove: () => sendReactionWhatsApp(params.msg.chatId, params.msg.id, "", reactionOptions),
|
|
1689
|
+
onSendError: (err) => {
|
|
1690
|
+
params.warn({
|
|
1691
|
+
error: formatError(err),
|
|
1692
|
+
chatId: params.msg.chatId,
|
|
1693
|
+
messageId: params.msg.id
|
|
1694
|
+
}, "failed to send ack reaction");
|
|
1695
|
+
logVerbose(`WhatsApp ack reaction failed for chat ${params.msg.chatId}: ${formatError(err)}`);
|
|
1696
|
+
}
|
|
1697
|
+
});
|
|
1698
|
+
}
|
|
1699
|
+
//#endregion
|
|
1700
|
+
//#region extensions/whatsapp/src/auto-reply/monitor/broadcast.ts
|
|
1701
|
+
function buildBroadcastRouteKeys(params) {
|
|
1702
|
+
const sessionKey = buildAgentSessionKey({
|
|
1703
|
+
agentId: params.agentId,
|
|
1704
|
+
channel: "whatsapp",
|
|
1705
|
+
accountId: params.route.accountId,
|
|
1706
|
+
peer: {
|
|
1707
|
+
kind: params.msg.chatType === "group" ? "group" : "direct",
|
|
1708
|
+
id: params.peerId
|
|
1709
|
+
},
|
|
1710
|
+
dmScope: params.cfg.session?.dmScope,
|
|
1711
|
+
identityLinks: params.cfg.session?.identityLinks
|
|
1712
|
+
});
|
|
1713
|
+
const mainSessionKey = buildAgentMainSessionKey({
|
|
1714
|
+
agentId: params.agentId,
|
|
1715
|
+
mainKey: DEFAULT_MAIN_KEY
|
|
1716
|
+
});
|
|
1717
|
+
return {
|
|
1718
|
+
sessionKey,
|
|
1719
|
+
mainSessionKey,
|
|
1720
|
+
lastRoutePolicy: deriveLastRoutePolicy({
|
|
1721
|
+
sessionKey,
|
|
1722
|
+
mainSessionKey
|
|
1723
|
+
})
|
|
1724
|
+
};
|
|
1725
|
+
}
|
|
1726
|
+
async function maybeBroadcastMessage(params) {
|
|
1727
|
+
const broadcastAgents = params.cfg.broadcast?.[params.peerId];
|
|
1728
|
+
if (!broadcastAgents || !Array.isArray(broadcastAgents)) return false;
|
|
1729
|
+
if (broadcastAgents.length === 0) return false;
|
|
1730
|
+
const strategy = params.cfg.broadcast?.strategy || "parallel";
|
|
1731
|
+
whatsappInboundLog.info(`Broadcasting message to ${broadcastAgents.length} agents (${strategy})`);
|
|
1732
|
+
const agentIds = params.cfg.agents?.list?.map((agent) => normalizeAgentId(agent.id));
|
|
1733
|
+
const hasKnownAgents = (agentIds?.length ?? 0) > 0;
|
|
1734
|
+
const groupHistorySnapshot = params.msg.chatType === "group" ? params.groupHistories.get(params.groupHistoryKey) ?? [] : void 0;
|
|
1735
|
+
const processForAgent = async (agentId) => {
|
|
1736
|
+
const normalizedAgentId = normalizeAgentId(agentId);
|
|
1737
|
+
if (hasKnownAgents && !agentIds?.includes(normalizedAgentId)) {
|
|
1738
|
+
whatsappInboundLog.warn(`Broadcast agent ${agentId} not found in agents.list; skipping`);
|
|
1739
|
+
return false;
|
|
1740
|
+
}
|
|
1741
|
+
const routeKeys = buildBroadcastRouteKeys({
|
|
1742
|
+
cfg: params.cfg,
|
|
1743
|
+
msg: params.msg,
|
|
1744
|
+
route: params.route,
|
|
1745
|
+
peerId: params.peerId,
|
|
1746
|
+
agentId: normalizedAgentId
|
|
1747
|
+
});
|
|
1748
|
+
const baseAgentRoute = {
|
|
1749
|
+
...params.route,
|
|
1750
|
+
agentId: normalizedAgentId,
|
|
1751
|
+
...routeKeys
|
|
1752
|
+
};
|
|
1753
|
+
const agentRoute = params.msg.chatType === "group" ? resolveWhatsAppGroupSessionRoute(baseAgentRoute) : baseAgentRoute;
|
|
1754
|
+
try {
|
|
1755
|
+
const opts = {
|
|
1756
|
+
groupHistory: groupHistorySnapshot,
|
|
1757
|
+
suppressGroupHistoryClear: true
|
|
1758
|
+
};
|
|
1759
|
+
if (params.preflightAudioTranscript !== void 0) opts.preflightAudioTranscript = params.preflightAudioTranscript;
|
|
1760
|
+
if (params.ackAlreadySent === true) opts.ackAlreadySent = true;
|
|
1761
|
+
if (params.ackReaction !== void 0) opts.ackReaction = params.ackReaction;
|
|
1762
|
+
return await params.processMessage(params.msg, agentRoute, params.groupHistoryKey, opts);
|
|
1763
|
+
} catch (err) {
|
|
1764
|
+
whatsappInboundLog.error(`Broadcast agent ${agentId} failed: ${formatError(err)}`);
|
|
1765
|
+
return false;
|
|
1766
|
+
}
|
|
1767
|
+
};
|
|
1768
|
+
if (strategy === "sequential") for (const agentId of broadcastAgents) await processForAgent(agentId);
|
|
1769
|
+
else await Promise.allSettled(broadcastAgents.map(processForAgent));
|
|
1770
|
+
if (params.msg.chatType === "group") params.groupHistories.set(params.groupHistoryKey, []);
|
|
1771
|
+
return true;
|
|
1772
|
+
}
|
|
1773
|
+
//#endregion
|
|
1774
|
+
//#region extensions/whatsapp/src/auto-reply/monitor/commands.ts
|
|
1775
|
+
function stripMentionsForCommand(text, mentionRegexes, selfE164) {
|
|
1776
|
+
let result = text;
|
|
1777
|
+
for (const re of mentionRegexes) result = result.replace(re, " ");
|
|
1778
|
+
if (selfE164) {
|
|
1779
|
+
const digits = selfE164.replace(/\D/g, "");
|
|
1780
|
+
if (digits) {
|
|
1781
|
+
const pattern = new RegExp(`\\+?${digits}`, "g");
|
|
1782
|
+
result = result.replace(pattern, " ");
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
return result.replace(/\s+/g, " ").trim();
|
|
1786
|
+
}
|
|
1787
|
+
//#endregion
|
|
1788
|
+
//#region extensions/whatsapp/src/auto-reply/monitor/group-members.ts
|
|
1789
|
+
function appendNormalizedUnique(entries, seen, ordered) {
|
|
1790
|
+
for (const entry of entries) {
|
|
1791
|
+
const normalized = (0, text_runtime_exports.normalizeE164)(entry) ?? entry;
|
|
1792
|
+
if (!normalized || seen.has(normalized)) continue;
|
|
1793
|
+
seen.add(normalized);
|
|
1794
|
+
ordered.push(normalized);
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
function noteGroupMember(groupMemberNames, conversationId, e164, name) {
|
|
1798
|
+
if (!e164 || !name) return;
|
|
1799
|
+
const key = (0, text_runtime_exports.normalizeE164)(e164) ?? e164;
|
|
1800
|
+
if (!key) return;
|
|
1801
|
+
let roster = groupMemberNames.get(conversationId);
|
|
1802
|
+
if (!roster) {
|
|
1803
|
+
roster = /* @__PURE__ */ new Map();
|
|
1804
|
+
groupMemberNames.set(conversationId, roster);
|
|
1805
|
+
}
|
|
1806
|
+
roster.set(key, name);
|
|
1807
|
+
}
|
|
1808
|
+
function formatGroupMembers(params) {
|
|
1809
|
+
const { participants, roster, fallbackE164 } = params;
|
|
1810
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1811
|
+
const ordered = [];
|
|
1812
|
+
if (participants?.length) appendNormalizedUnique(participants, seen, ordered);
|
|
1813
|
+
if (roster) appendNormalizedUnique(roster.keys(), seen, ordered);
|
|
1814
|
+
if (ordered.length === 0 && fallbackE164) {
|
|
1815
|
+
const normalized = (0, text_runtime_exports.normalizeE164)(fallbackE164) ?? fallbackE164;
|
|
1816
|
+
if (normalized) ordered.push(normalized);
|
|
1817
|
+
}
|
|
1818
|
+
if (ordered.length === 0) return;
|
|
1819
|
+
return ordered.map((entry) => {
|
|
1820
|
+
const name = roster?.get(entry);
|
|
1821
|
+
return name ? `${name} (${entry})` : entry;
|
|
1822
|
+
}).join(", ");
|
|
1823
|
+
}
|
|
1824
|
+
//#endregion
|
|
1825
|
+
//#region extensions/whatsapp/src/auto-reply/monitor/group-gating.ts
|
|
1826
|
+
function isOwnerSender(baseMentionConfig, msg) {
|
|
1827
|
+
const sender = (0, text_runtime_exports.normalizeE164)(getSenderIdentity(msg).e164 ?? "");
|
|
1828
|
+
if (!sender) return false;
|
|
1829
|
+
return resolveOwnerList(baseMentionConfig, getSelfIdentity(msg).e164 ?? void 0).includes(sender);
|
|
1830
|
+
}
|
|
1831
|
+
function recordPendingGroupHistoryEntry(params) {
|
|
1832
|
+
const senderIdentity = getSenderIdentity(params.msg);
|
|
1833
|
+
const sender = senderIdentity.name && senderIdentity.e164 ? `${senderIdentity.name} (${senderIdentity.e164})` : senderIdentity.name ?? senderIdentity.e164 ?? getPrimaryIdentityId(senderIdentity) ?? "Unknown";
|
|
1834
|
+
recordPendingHistoryEntryIfEnabled({
|
|
1835
|
+
historyMap: params.groupHistories,
|
|
1836
|
+
historyKey: params.groupHistoryKey,
|
|
1837
|
+
limit: params.groupHistoryLimit,
|
|
1838
|
+
entry: {
|
|
1839
|
+
sender,
|
|
1840
|
+
body: params.body ?? params.msg.body,
|
|
1841
|
+
timestamp: params.msg.timestamp,
|
|
1842
|
+
id: params.msg.id,
|
|
1843
|
+
senderJid: senderIdentity.jid ?? params.msg.senderJid
|
|
1844
|
+
}
|
|
1845
|
+
});
|
|
1846
|
+
}
|
|
1847
|
+
function skipGroupMessageAndStoreHistory(params, verboseMessage, body) {
|
|
1848
|
+
params.logVerbose(verboseMessage);
|
|
1849
|
+
recordPendingGroupHistoryEntry({
|
|
1850
|
+
msg: params.msg,
|
|
1851
|
+
body,
|
|
1852
|
+
groupHistories: params.groupHistories,
|
|
1853
|
+
groupHistoryKey: params.groupHistoryKey,
|
|
1854
|
+
groupHistoryLimit: params.groupHistoryLimit
|
|
1855
|
+
});
|
|
1856
|
+
return { shouldProcess: false };
|
|
1857
|
+
}
|
|
1858
|
+
async function applyGroupGating(params) {
|
|
1859
|
+
const sender = getSenderIdentity(params.msg);
|
|
1860
|
+
const self = getSelfIdentity(params.msg, params.authDir);
|
|
1861
|
+
const inboundPolicy = resolveWhatsAppInboundPolicy({
|
|
1862
|
+
cfg: params.cfg,
|
|
1863
|
+
accountId: params.msg.accountId,
|
|
1864
|
+
selfE164: self.e164 ?? null
|
|
1865
|
+
});
|
|
1866
|
+
const conversationGroupPolicy = inboundPolicy.resolveConversationGroupPolicy(params.conversationId);
|
|
1867
|
+
if (conversationGroupPolicy.allowlistEnabled && !conversationGroupPolicy.allowed) {
|
|
1868
|
+
params.logVerbose(`Skipping group message ${params.conversationId} (not in allowlist)`);
|
|
1869
|
+
return { shouldProcess: false };
|
|
1870
|
+
}
|
|
1871
|
+
noteGroupMember(params.groupMemberNames, params.groupHistoryKey, sender.e164 ?? void 0, sender.name ?? void 0);
|
|
1872
|
+
const baseMentionConfig = {
|
|
1873
|
+
...params.baseMentionConfig,
|
|
1874
|
+
allowFrom: inboundPolicy.configuredAllowFrom
|
|
1875
|
+
};
|
|
1876
|
+
const mentionConfig = {
|
|
1877
|
+
...buildMentionConfig(params.cfg, params.agentId),
|
|
1878
|
+
allowFrom: inboundPolicy.configuredAllowFrom
|
|
1879
|
+
};
|
|
1880
|
+
const mentionMsg = params.mentionText !== void 0 ? {
|
|
1881
|
+
...params.msg,
|
|
1882
|
+
body: params.mentionText
|
|
1883
|
+
} : params.msg;
|
|
1884
|
+
const commandBody = stripMentionsForCommand(mentionMsg.body, mentionConfig.mentionRegexes, self.e164);
|
|
1885
|
+
const activationCommand = parseActivationCommand(commandBody);
|
|
1886
|
+
const owner = isOwnerSender(baseMentionConfig, params.msg);
|
|
1887
|
+
const shouldBypassMention = owner && hasControlCommand$1(commandBody, params.cfg);
|
|
1888
|
+
if (activationCommand.hasCommand && !owner) return skipGroupMessageAndStoreHistory(params, `Ignoring /activation from non-owner in group ${params.conversationId}`);
|
|
1889
|
+
const mentionDebug = debugMention(mentionMsg, mentionConfig, params.authDir);
|
|
1890
|
+
params.replyLogger.debug({
|
|
1891
|
+
conversationId: params.conversationId,
|
|
1892
|
+
wasMentioned: mentionDebug.wasMentioned,
|
|
1893
|
+
...mentionDebug.details
|
|
1894
|
+
}, "group mention debug");
|
|
1895
|
+
const wasMentioned = mentionDebug.wasMentioned;
|
|
1896
|
+
const requireMention = await resolveGroupActivationFor({
|
|
1897
|
+
cfg: params.cfg,
|
|
1898
|
+
accountId: inboundPolicy.account.accountId,
|
|
1899
|
+
agentId: params.agentId,
|
|
1900
|
+
sessionKey: params.sessionKey,
|
|
1901
|
+
conversationId: params.conversationId
|
|
1902
|
+
}) !== "always";
|
|
1903
|
+
const replyContext = getReplyContext(params.msg, params.authDir);
|
|
1904
|
+
const mentionDecision = resolveInboundMentionDecision({
|
|
1905
|
+
facts: {
|
|
1906
|
+
canDetectMention: true,
|
|
1907
|
+
wasMentioned,
|
|
1908
|
+
implicitMentionKinds: implicitMentionKindWhen("quoted_bot", !(params.selfChatMode === true && identitiesOverlap(self, sender)) && identitiesOverlap(self, replyContext?.sender))
|
|
1909
|
+
},
|
|
1910
|
+
policy: {
|
|
1911
|
+
isGroup: true,
|
|
1912
|
+
requireMention,
|
|
1913
|
+
allowTextCommands: false,
|
|
1914
|
+
hasControlCommand: false,
|
|
1915
|
+
commandAuthorized: false
|
|
1916
|
+
}
|
|
1917
|
+
});
|
|
1918
|
+
const effectiveWasMentioned = mentionDecision.effectiveWasMentioned || shouldBypassMention;
|
|
1919
|
+
params.msg.wasMentioned = effectiveWasMentioned;
|
|
1920
|
+
if (!shouldBypassMention && requireMention && mentionDecision.shouldSkip) {
|
|
1921
|
+
if (params.deferMissingMention === true) {
|
|
1922
|
+
params.logVerbose(`Deferring group mention skip until audio preflight completes in ${params.conversationId}`);
|
|
1923
|
+
return {
|
|
1924
|
+
shouldProcess: false,
|
|
1925
|
+
needsMentionText: true
|
|
1926
|
+
};
|
|
1927
|
+
}
|
|
1928
|
+
return skipGroupMessageAndStoreHistory(params, `Group message stored for context (no mention detected) in ${params.conversationId}: ${mentionMsg.body}`, params.mentionText);
|
|
1929
|
+
}
|
|
1930
|
+
return { shouldProcess: true };
|
|
1931
|
+
}
|
|
1932
|
+
//#endregion
|
|
1933
|
+
//#region extensions/whatsapp/src/auto-reply/monitor/last-route.ts
|
|
1934
|
+
function trackBackgroundTask(backgroundTasks, task) {
|
|
1935
|
+
backgroundTasks.add(task);
|
|
1936
|
+
const cleanup = () => {
|
|
1937
|
+
backgroundTasks.delete(task);
|
|
1938
|
+
};
|
|
1939
|
+
task.then(cleanup, cleanup);
|
|
1940
|
+
}
|
|
1941
|
+
function updateLastRouteInBackground(params) {
|
|
1942
|
+
const storePath = resolveStorePath(params.cfg.session?.store, { agentId: params.storeAgentId });
|
|
1943
|
+
const task = updateLastRoute({
|
|
1944
|
+
storePath,
|
|
1945
|
+
sessionKey: params.sessionKey,
|
|
1946
|
+
deliveryContext: {
|
|
1947
|
+
channel: params.channel,
|
|
1948
|
+
to: params.to,
|
|
1949
|
+
accountId: params.accountId
|
|
1950
|
+
},
|
|
1951
|
+
ctx: params.ctx
|
|
1952
|
+
}).catch((err) => {
|
|
1953
|
+
params.warn({
|
|
1954
|
+
error: formatError(err),
|
|
1955
|
+
storePath,
|
|
1956
|
+
sessionKey: params.sessionKey,
|
|
1957
|
+
to: params.to
|
|
1958
|
+
}, "failed updating last route");
|
|
1959
|
+
});
|
|
1960
|
+
trackBackgroundTask(params.backgroundTasks, task);
|
|
1961
|
+
}
|
|
1962
|
+
//#endregion
|
|
1963
|
+
//#region extensions/whatsapp/src/auto-reply/monitor/peer.ts
|
|
1964
|
+
function resolvePeerId(msg) {
|
|
1965
|
+
if (msg.chatType === "group") return msg.conversationId ?? msg.from;
|
|
1966
|
+
const sender = getSenderIdentity(msg);
|
|
1967
|
+
if (sender.e164) return (0, text_runtime_exports.normalizeE164)(sender.e164) ?? sender.e164;
|
|
1968
|
+
if (msg.from.includes("@")) return jidToE164(msg.from) ?? msg.from;
|
|
1969
|
+
return (0, text_runtime_exports.normalizeE164)(msg.from) ?? msg.from;
|
|
1970
|
+
}
|
|
1971
|
+
//#endregion
|
|
1972
|
+
//#region extensions/whatsapp/src/system-prompt.ts
|
|
1973
|
+
function resolveWhatsAppGroupSystemPrompt(params) {
|
|
1974
|
+
if (!params.groupId) return;
|
|
1975
|
+
const groups = params.accountConfig?.groups;
|
|
1976
|
+
const specific = groups?.[params.groupId];
|
|
1977
|
+
if (specific != null && specific.systemPrompt != null) return specific.systemPrompt.trim() || void 0;
|
|
1978
|
+
const wildcard = groups?.["*"]?.systemPrompt;
|
|
1979
|
+
return wildcard != null ? wildcard.trim() || void 0 : void 0;
|
|
1980
|
+
}
|
|
1981
|
+
function resolveWhatsAppDirectSystemPrompt(params) {
|
|
1982
|
+
if (!params.peerId) return;
|
|
1983
|
+
const direct = params.accountConfig?.direct;
|
|
1984
|
+
const specific = direct?.[params.peerId];
|
|
1985
|
+
if (specific != null && specific.systemPrompt != null) return specific.systemPrompt.trim() || void 0;
|
|
1986
|
+
const wildcard = direct?.["*"]?.systemPrompt;
|
|
1987
|
+
return wildcard != null ? wildcard.trim() || void 0 : void 0;
|
|
1988
|
+
}
|
|
1989
|
+
//#endregion
|
|
1990
|
+
//#region extensions/whatsapp/src/auto-reply/util.ts
|
|
1991
|
+
function elide(text, limit = 400) {
|
|
1992
|
+
if (!text) return text;
|
|
1993
|
+
if (text.length <= limit) return text;
|
|
1994
|
+
return `${text.slice(0, limit)}… (truncated ${text.length - limit} chars)`;
|
|
1995
|
+
}
|
|
1996
|
+
function isLikelyWhatsAppCryptoError(reason) {
|
|
1997
|
+
const formatReason = (value) => {
|
|
1998
|
+
if (value == null) return "";
|
|
1999
|
+
if (typeof value === "string") return value;
|
|
2000
|
+
if (value instanceof Error) return `${value.message}\n${value.stack ?? ""}`;
|
|
2001
|
+
if (typeof value === "object") try {
|
|
2002
|
+
return JSON.stringify(value);
|
|
2003
|
+
} catch {
|
|
2004
|
+
return Object.prototype.toString.call(value);
|
|
2005
|
+
}
|
|
2006
|
+
if (typeof value === "number") return String(value);
|
|
2007
|
+
if (typeof value === "boolean") return String(value);
|
|
2008
|
+
if (typeof value === "bigint") return String(value);
|
|
2009
|
+
if (typeof value === "symbol") return value.description ?? value.toString();
|
|
2010
|
+
if (typeof value === "function") return value.name ? `[function ${value.name}]` : "[function]";
|
|
2011
|
+
return Object.prototype.toString.call(value);
|
|
2012
|
+
};
|
|
2013
|
+
const haystack = normalizeLowercaseStringOrEmpty(reason instanceof Error ? `${reason.message}\n${reason.stack ?? ""}` : formatReason(reason));
|
|
2014
|
+
if (!(haystack.includes("unsupported state or unable to authenticate data") || haystack.includes("bad mac"))) return false;
|
|
2015
|
+
return haystack.includes("@whiskeysockets/baileys") || haystack.includes("baileys") || haystack.includes("noise-handler") || haystack.includes("aesdecryptgcm");
|
|
2016
|
+
}
|
|
2017
|
+
//#endregion
|
|
2018
|
+
//#region extensions/whatsapp/src/auto-reply/deliver-reply.ts
|
|
2019
|
+
async function deliverWebReply(params) {
|
|
2020
|
+
const { replyResult, msg, maxMediaBytes, textLimit, replyLogger, connectionId, skipLog } = params;
|
|
2021
|
+
const replyStarted = Date.now();
|
|
2022
|
+
const sendResults = [];
|
|
2023
|
+
const rememberSendResult = (result) => {
|
|
2024
|
+
if (result) sendResults.push(result);
|
|
2025
|
+
};
|
|
2026
|
+
const finishDelivery = () => {
|
|
2027
|
+
return {
|
|
2028
|
+
results: sendResults,
|
|
2029
|
+
messageIds: [...new Set(sendResults.flatMap((result) => result.messageIds))],
|
|
2030
|
+
providerAccepted: sendResults.some((result) => result.providerAccepted)
|
|
2031
|
+
};
|
|
2032
|
+
};
|
|
2033
|
+
if (isReasoningReplyPayload(replyResult)) {
|
|
2034
|
+
whatsappOutboundLog.debug(`Suppressed reasoning payload to ${msg.from}`);
|
|
2035
|
+
return finishDelivery();
|
|
2036
|
+
}
|
|
2037
|
+
const tableMode = params.tableMode ?? "code";
|
|
2038
|
+
const chunkMode = params.chunkMode ?? "length";
|
|
2039
|
+
const normalizedReply = params.normalizedReplyResult ?? normalizeWhatsAppOutboundPayload(replyResult, { normalizeText: normalizeWhatsAppPayloadTextPreservingIndentation });
|
|
2040
|
+
const textChunks = chunkMarkdownTextWithMode(markdownToWhatsApp((0, text_runtime_exports.convertMarkdownTables)(normalizedReply.text ?? "", tableMode)), textLimit, chunkMode);
|
|
2041
|
+
const mediaList = normalizedReply.mediaUrls ?? [];
|
|
2042
|
+
const getQuote = () => {
|
|
2043
|
+
if (!replyResult.replyToId) return;
|
|
2044
|
+
const cached = lookupInboundMessageMeta(msg.accountId, msg.chatId, replyResult.replyToId);
|
|
2045
|
+
return buildQuotedMessageOptions({
|
|
2046
|
+
messageId: replyResult.replyToId,
|
|
2047
|
+
remoteJid: msg.chatId,
|
|
2048
|
+
fromMe: cached?.fromMe ?? false,
|
|
2049
|
+
participant: cached?.participant ?? (msg.chatType === "group" ? msg.senderJid : void 0),
|
|
2050
|
+
messageText: cached?.body ?? ""
|
|
2051
|
+
});
|
|
2052
|
+
};
|
|
2053
|
+
const sendWithRetry = async (fn, label, maxAttempts = 3) => {
|
|
2054
|
+
return await sendWhatsAppOutboundWithRetry({
|
|
2055
|
+
send: fn,
|
|
2056
|
+
maxAttempts,
|
|
2057
|
+
onRetry: ({ attempt, maxAttempts: retryMaxAttempts, backoffMs, errorText }) => {
|
|
2058
|
+
logVerbose(`Retrying ${label} to ${msg.from} after failure (${attempt}/${retryMaxAttempts - 1}) in ${backoffMs}ms: ${errorText}`);
|
|
2059
|
+
}
|
|
2060
|
+
});
|
|
2061
|
+
};
|
|
2062
|
+
if (mediaList.length === 0 && textChunks.length) {
|
|
2063
|
+
const totalChunks = textChunks.length;
|
|
2064
|
+
for (const [index, chunk] of textChunks.entries()) {
|
|
2065
|
+
const chunkStarted = Date.now();
|
|
2066
|
+
const quote = getQuote();
|
|
2067
|
+
rememberSendResult(await sendWithRetry(() => msg.reply(chunk, quote), "text"));
|
|
2068
|
+
if (!skipLog) {
|
|
2069
|
+
const durationMs = Date.now() - chunkStarted;
|
|
2070
|
+
whatsappOutboundLog.debug(`Sent chunk ${index + 1}/${totalChunks} to ${msg.from} (${durationMs.toFixed(0)}ms)`);
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
const delivery = finishDelivery();
|
|
2074
|
+
const logPayload = {
|
|
2075
|
+
correlationId: msg.id ?? newConnectionId(),
|
|
2076
|
+
connectionId: connectionId ?? null,
|
|
2077
|
+
to: msg.from,
|
|
2078
|
+
from: msg.to,
|
|
2079
|
+
text: elide(replyResult.text, 240),
|
|
2080
|
+
mediaUrl: null,
|
|
2081
|
+
mediaSizeBytes: null,
|
|
2082
|
+
mediaKind: null,
|
|
2083
|
+
durationMs: Date.now() - replyStarted
|
|
2084
|
+
};
|
|
2085
|
+
if (delivery.providerAccepted) replyLogger.info(logPayload, "auto-reply sent (text)");
|
|
2086
|
+
else replyLogger.warn(logPayload, "auto-reply text was not accepted by WhatsApp provider");
|
|
2087
|
+
return delivery;
|
|
2088
|
+
}
|
|
2089
|
+
const remainingText = [...textChunks];
|
|
2090
|
+
await sendMediaWithLeadingCaption({
|
|
2091
|
+
mediaUrls: mediaList,
|
|
2092
|
+
caption: remainingText.shift() || "",
|
|
2093
|
+
send: async ({ mediaUrl, caption }) => {
|
|
2094
|
+
const media = await prepareWhatsAppOutboundMedia(await loadWebMedia$1(mediaUrl, {
|
|
2095
|
+
maxBytes: maxMediaBytes,
|
|
2096
|
+
localRoots: params.mediaLocalRoots
|
|
2097
|
+
}), mediaUrl);
|
|
2098
|
+
if (shouldLogVerbose()) {
|
|
2099
|
+
logVerbose(`Web auto-reply media size: ${(media.buffer.length / (1024 * 1024)).toFixed(2)}MB`);
|
|
2100
|
+
logVerbose(`Web auto-reply media source: ${mediaUrl} (kind ${media.kind})`);
|
|
2101
|
+
}
|
|
2102
|
+
if (media.kind === "image") {
|
|
2103
|
+
const quote = getQuote();
|
|
2104
|
+
rememberSendResult(await sendWithRetry(() => msg.sendMedia({
|
|
2105
|
+
image: media.buffer,
|
|
2106
|
+
caption,
|
|
2107
|
+
mimetype: media.mimetype
|
|
2108
|
+
}, quote), "media:image"));
|
|
2109
|
+
} else if (media.kind === "audio") {
|
|
2110
|
+
const quote = getQuote();
|
|
2111
|
+
rememberSendResult(await sendWithRetry(() => msg.sendMedia({
|
|
2112
|
+
audio: media.buffer,
|
|
2113
|
+
ptt: true,
|
|
2114
|
+
mimetype: media.mimetype
|
|
2115
|
+
}, quote), "media:audio"));
|
|
2116
|
+
if (caption) rememberSendResult(await sendWithRetry(() => msg.reply(caption, quote), "media:audio-text"));
|
|
2117
|
+
} else if (media.kind === "video") {
|
|
2118
|
+
const quote = getQuote();
|
|
2119
|
+
rememberSendResult(await sendWithRetry(() => msg.sendMedia({
|
|
2120
|
+
video: media.buffer,
|
|
2121
|
+
caption,
|
|
2122
|
+
mimetype: media.mimetype
|
|
2123
|
+
}, quote), "media:video"));
|
|
2124
|
+
} else {
|
|
2125
|
+
const quote = getQuote();
|
|
2126
|
+
rememberSendResult(await sendWithRetry(() => msg.sendMedia({
|
|
2127
|
+
document: media.buffer,
|
|
2128
|
+
fileName: media.fileName,
|
|
2129
|
+
caption,
|
|
2130
|
+
mimetype: media.mimetype
|
|
2131
|
+
}, quote), "media:document"));
|
|
2132
|
+
}
|
|
2133
|
+
whatsappOutboundLog.info(`Sent media reply to ${msg.from} (${(media.buffer.length / (1024 * 1024)).toFixed(2)}MB)`);
|
|
2134
|
+
replyLogger.info({
|
|
2135
|
+
correlationId: msg.id ?? newConnectionId(),
|
|
2136
|
+
connectionId: connectionId ?? null,
|
|
2137
|
+
to: msg.from,
|
|
2138
|
+
from: msg.to,
|
|
2139
|
+
text: caption ?? null,
|
|
2140
|
+
mediaUrl,
|
|
2141
|
+
mediaSizeBytes: media.buffer.length,
|
|
2142
|
+
mediaKind: media.kind,
|
|
2143
|
+
durationMs: Date.now() - replyStarted
|
|
2144
|
+
}, "auto-reply sent (media)");
|
|
2145
|
+
},
|
|
2146
|
+
onError: async ({ error, mediaUrl, caption, isFirst }) => {
|
|
2147
|
+
whatsappOutboundLog.error(`Failed sending web media to ${msg.from}: ${formatError(error)}`);
|
|
2148
|
+
replyLogger.warn({
|
|
2149
|
+
err: error,
|
|
2150
|
+
mediaUrl
|
|
2151
|
+
}, "failed to send web media reply");
|
|
2152
|
+
if (!isFirst) return;
|
|
2153
|
+
const fallbackText = [remainingText.shift() ?? caption ?? "", "⚠️ Media failed."].filter(Boolean).join("\n");
|
|
2154
|
+
if (!fallbackText) return;
|
|
2155
|
+
whatsappOutboundLog.warn(`Media skipped; sent text-only to ${msg.from}`);
|
|
2156
|
+
rememberSendResult(await sendWithRetry(() => msg.reply(fallbackText, getQuote()), "media:fallback-text"));
|
|
2157
|
+
}
|
|
2158
|
+
});
|
|
2159
|
+
for (const chunk of remainingText) rememberSendResult(await sendWithRetry(() => msg.reply(chunk, getQuote()), "media:text"));
|
|
2160
|
+
return finishDelivery();
|
|
2161
|
+
}
|
|
2162
|
+
//#endregion
|
|
2163
|
+
//#region extensions/whatsapp/src/auto-reply/monitor/inbound-context.ts
|
|
2164
|
+
function isWhatsAppSupplementalSenderAllowed(params) {
|
|
2165
|
+
if (params.allowFrom.includes("*")) return true;
|
|
2166
|
+
const senderValues = new Set(getComparableIdentityValues(params.sender));
|
|
2167
|
+
if (senderValues.size === 0) return false;
|
|
2168
|
+
for (const entry of params.allowFrom) {
|
|
2169
|
+
const rawEntry = entry.trim();
|
|
2170
|
+
if (!rawEntry) continue;
|
|
2171
|
+
const normalizedEntry = (0, text_runtime_exports.normalizeE164)(rawEntry);
|
|
2172
|
+
if (normalizedEntry && senderValues.has(normalizedEntry) || senderValues.has(rawEntry)) return true;
|
|
2173
|
+
}
|
|
2174
|
+
return false;
|
|
2175
|
+
}
|
|
2176
|
+
function resolveVisibleWhatsAppGroupHistory(params) {
|
|
2177
|
+
if (params.groupPolicy !== "allowlist") return params.history;
|
|
2178
|
+
return filterSupplementalContextItems({
|
|
2179
|
+
items: params.history,
|
|
2180
|
+
mode: params.mode,
|
|
2181
|
+
kind: "history",
|
|
2182
|
+
isSenderAllowed: (entry) => isWhatsAppSupplementalSenderAllowed({
|
|
2183
|
+
allowFrom: params.groupAllowFrom,
|
|
2184
|
+
sender: entry.senderJid ? { jid: entry.senderJid } : null
|
|
2185
|
+
})
|
|
2186
|
+
}).items;
|
|
2187
|
+
}
|
|
2188
|
+
function resolveVisibleWhatsAppReplyContext(params) {
|
|
2189
|
+
const replyTo = getReplyContext(params.msg, params.authDir);
|
|
2190
|
+
if (!replyTo) return null;
|
|
2191
|
+
return evaluateSupplementalContextVisibility({
|
|
2192
|
+
mode: params.mode,
|
|
2193
|
+
kind: "quote",
|
|
2194
|
+
senderAllowed: params.msg.chatType !== "group" || params.groupPolicy !== "allowlist" ? true : isWhatsAppSupplementalSenderAllowed({
|
|
2195
|
+
allowFrom: params.groupAllowFrom,
|
|
2196
|
+
sender: replyTo.sender
|
|
2197
|
+
})
|
|
2198
|
+
}).include ? replyTo : null;
|
|
2199
|
+
}
|
|
2200
|
+
//#endregion
|
|
2201
|
+
//#region extensions/whatsapp/src/auto-reply/monitor/inbound-dispatch.ts
|
|
2202
|
+
function logWhatsAppReplyDeliveryError(params) {
|
|
2203
|
+
params.replyLogger.error({
|
|
2204
|
+
err: params.err,
|
|
2205
|
+
replyKind: params.info.kind,
|
|
2206
|
+
correlationId: params.msg.id ?? null,
|
|
2207
|
+
connectionId: params.connectionId,
|
|
2208
|
+
conversationId: params.conversationId,
|
|
2209
|
+
chatId: params.msg.chatId ?? null,
|
|
2210
|
+
to: params.msg.from ?? null,
|
|
2211
|
+
from: params.msg.to ?? null
|
|
2212
|
+
}, "auto-reply delivery failed");
|
|
2213
|
+
}
|
|
2214
|
+
function resolveWhatsAppDisableBlockStreaming(cfg) {
|
|
2215
|
+
if (typeof cfg.channels?.whatsapp?.blockStreaming !== "boolean") return;
|
|
2216
|
+
return !cfg.channels.whatsapp.blockStreaming;
|
|
2217
|
+
}
|
|
2218
|
+
function resolveWhatsAppDeliverablePayload(payload, info) {
|
|
2219
|
+
if (payload.isReasoning === true || payload.isCompactionNotice === true) return null;
|
|
2220
|
+
if (payload.isError === true) return null;
|
|
2221
|
+
if (info.kind === "tool") {
|
|
2222
|
+
if (!resolveSendableOutboundReplyParts(payload).hasMedia) return null;
|
|
2223
|
+
return {
|
|
2224
|
+
...payload,
|
|
2225
|
+
text: void 0
|
|
2226
|
+
};
|
|
2227
|
+
}
|
|
2228
|
+
return payload;
|
|
2229
|
+
}
|
|
2230
|
+
function resolveWhatsAppResponsePrefix(params) {
|
|
2231
|
+
const configuredResponsePrefix = params.cfg.messages?.responsePrefix;
|
|
2232
|
+
return params.pipelineResponsePrefix ?? (configuredResponsePrefix === void 0 && params.isSelfChat ? resolveIdentityNamePrefix$1(params.cfg, params.agentId) : void 0);
|
|
2233
|
+
}
|
|
2234
|
+
function buildWhatsAppInboundContext(params) {
|
|
2235
|
+
const inboundHistory = params.msg.chatType === "group" ? (params.groupHistory ?? []).map((entry) => ({
|
|
2236
|
+
sender: entry.sender,
|
|
2237
|
+
body: entry.body,
|
|
2238
|
+
timestamp: entry.timestamp
|
|
2239
|
+
})) : void 0;
|
|
2240
|
+
return finalizeInboundContext({
|
|
2241
|
+
Body: params.combinedBody,
|
|
2242
|
+
BodyForAgent: params.bodyForAgent ?? params.msg.body,
|
|
2243
|
+
InboundHistory: inboundHistory,
|
|
2244
|
+
RawBody: params.rawBody ?? params.msg.body,
|
|
2245
|
+
CommandBody: params.commandBody ?? params.msg.body,
|
|
2246
|
+
Transcript: params.transcript,
|
|
2247
|
+
From: params.msg.from,
|
|
2248
|
+
To: params.msg.to,
|
|
2249
|
+
SessionKey: params.route.sessionKey,
|
|
2250
|
+
AccountId: params.route.accountId,
|
|
2251
|
+
MessageSid: params.msg.id,
|
|
2252
|
+
ReplyToId: params.visibleReplyTo?.id,
|
|
2253
|
+
ReplyToBody: params.visibleReplyTo?.body,
|
|
2254
|
+
ReplyToSender: params.visibleReplyTo?.sender?.label,
|
|
2255
|
+
MediaPath: params.msg.mediaPath,
|
|
2256
|
+
MediaUrl: params.msg.mediaUrl,
|
|
2257
|
+
MediaType: params.msg.mediaType,
|
|
2258
|
+
MediaTranscribedIndexes: params.mediaTranscribedIndexes,
|
|
2259
|
+
ChatType: params.msg.chatType,
|
|
2260
|
+
Timestamp: params.msg.timestamp,
|
|
2261
|
+
ConversationLabel: params.msg.chatType === "group" ? params.conversationId : params.msg.from,
|
|
2262
|
+
GroupSubject: params.msg.groupSubject,
|
|
2263
|
+
GroupMembers: formatGroupMembers({
|
|
2264
|
+
participants: params.msg.groupParticipants,
|
|
2265
|
+
roster: params.groupMemberRoster,
|
|
2266
|
+
fallbackE164: params.sender.e164
|
|
2267
|
+
}),
|
|
2268
|
+
SenderName: params.sender.name,
|
|
2269
|
+
SenderId: params.sender.id ?? params.sender.e164,
|
|
2270
|
+
SenderE164: params.sender.e164,
|
|
2271
|
+
CommandAuthorized: params.commandAuthorized,
|
|
2272
|
+
ReplyThreading: params.replyThreading,
|
|
2273
|
+
WasMentioned: params.msg.wasMentioned,
|
|
2274
|
+
GroupSystemPrompt: params.groupSystemPrompt,
|
|
2275
|
+
UntrustedStructuredContext: params.msg.untrustedStructuredContext,
|
|
2276
|
+
...params.msg.location ? toLocationContext(params.msg.location) : {},
|
|
2277
|
+
Provider: "whatsapp",
|
|
2278
|
+
Surface: "whatsapp",
|
|
2279
|
+
OriginatingChannel: "whatsapp",
|
|
2280
|
+
OriginatingTo: params.msg.from
|
|
2281
|
+
});
|
|
2282
|
+
}
|
|
2283
|
+
function resolveWhatsAppDmRouteTarget(params) {
|
|
2284
|
+
if (params.msg.chatType === "group") return;
|
|
2285
|
+
if (params.senderE164) return params.normalizeE164(params.senderE164) ?? void 0;
|
|
2286
|
+
if (params.msg.from.includes("@")) return jidToE164(params.msg.from) ?? void 0;
|
|
2287
|
+
return params.normalizeE164(params.msg.from) ?? void 0;
|
|
2288
|
+
}
|
|
2289
|
+
function updateWhatsAppMainLastRoute(params) {
|
|
2290
|
+
const shouldUpdateMainLastRoute = !params.pinnedMainDmRecipient || params.pinnedMainDmRecipient === params.dmRouteTarget;
|
|
2291
|
+
const inboundLastRouteSessionKey = resolveInboundLastRouteSessionKey({
|
|
2292
|
+
route: params.route,
|
|
2293
|
+
sessionKey: params.route.sessionKey
|
|
2294
|
+
});
|
|
2295
|
+
if (params.dmRouteTarget && inboundLastRouteSessionKey === params.route.mainSessionKey && shouldUpdateMainLastRoute) {
|
|
2296
|
+
params.updateLastRoute({
|
|
2297
|
+
cfg: params.cfg,
|
|
2298
|
+
backgroundTasks: params.backgroundTasks,
|
|
2299
|
+
storeAgentId: params.route.agentId,
|
|
2300
|
+
sessionKey: params.route.mainSessionKey,
|
|
2301
|
+
channel: "whatsapp",
|
|
2302
|
+
to: params.dmRouteTarget,
|
|
2303
|
+
accountId: params.route.accountId,
|
|
2304
|
+
ctx: params.ctx,
|
|
2305
|
+
warn: params.warn
|
|
2306
|
+
});
|
|
2307
|
+
return;
|
|
2308
|
+
}
|
|
2309
|
+
if (params.dmRouteTarget && inboundLastRouteSessionKey === params.route.mainSessionKey && params.pinnedMainDmRecipient) logVerbose$1(`Skipping main-session last route update for ${params.dmRouteTarget} (pinned owner ${params.pinnedMainDmRecipient})`);
|
|
2310
|
+
}
|
|
2311
|
+
async function dispatchWhatsAppBufferedReply(params) {
|
|
2312
|
+
const textLimit = params.maxMediaTextChunkLimit ?? resolveTextChunkLimit(params.cfg, "whatsapp");
|
|
2313
|
+
const chunkMode = resolveChunkMode(params.cfg, "whatsapp", params.route.accountId);
|
|
2314
|
+
const tableMode = resolveMarkdownTableMode$1({
|
|
2315
|
+
cfg: params.cfg,
|
|
2316
|
+
channel: "whatsapp",
|
|
2317
|
+
accountId: params.route.accountId
|
|
2318
|
+
});
|
|
2319
|
+
const mediaLocalRoots = getAgentScopedMediaLocalRoots(params.cfg, params.route.agentId);
|
|
2320
|
+
const disableBlockStreaming = resolveWhatsAppDisableBlockStreaming(params.cfg);
|
|
2321
|
+
let didSendReply = false;
|
|
2322
|
+
let didLogHeartbeatStrip = false;
|
|
2323
|
+
const { queuedFinal, counts } = await dispatchReplyWithBufferedBlockDispatcher({
|
|
2324
|
+
ctx: params.context,
|
|
2325
|
+
cfg: params.cfg,
|
|
2326
|
+
replyResolver: params.replyResolver,
|
|
2327
|
+
dispatcherOptions: {
|
|
2328
|
+
...params.replyPipeline,
|
|
2329
|
+
onHeartbeatStrip: () => {
|
|
2330
|
+
if (!didLogHeartbeatStrip) {
|
|
2331
|
+
didLogHeartbeatStrip = true;
|
|
2332
|
+
logVerbose$1("Stripped stray HEARTBEAT_OK token from web reply");
|
|
2333
|
+
}
|
|
2334
|
+
},
|
|
2335
|
+
deliver: async (payload, info) => {
|
|
2336
|
+
const deliveryPayload = resolveWhatsAppDeliverablePayload(payload, info);
|
|
2337
|
+
if (!deliveryPayload) return;
|
|
2338
|
+
const normalizedOutboundPayload = normalizeWhatsAppOutboundPayload(deliveryPayload, { normalizeText: normalizeWhatsAppPayloadTextPreservingIndentation });
|
|
2339
|
+
const normalizedDeliveryPayload = deliveryPayload.text === void 0 ? {
|
|
2340
|
+
...normalizedOutboundPayload,
|
|
2341
|
+
text: void 0
|
|
2342
|
+
} : normalizedOutboundPayload;
|
|
2343
|
+
const reply = resolveSendableOutboundReplyParts(normalizedDeliveryPayload);
|
|
2344
|
+
if (!reply.hasMedia && !reply.text.trim()) return;
|
|
2345
|
+
if (!(await params.deliverReply({
|
|
2346
|
+
replyResult: normalizedDeliveryPayload,
|
|
2347
|
+
normalizedReplyResult: normalizedDeliveryPayload,
|
|
2348
|
+
msg: params.msg,
|
|
2349
|
+
mediaLocalRoots,
|
|
2350
|
+
maxMediaBytes: params.maxMediaBytes,
|
|
2351
|
+
textLimit,
|
|
2352
|
+
chunkMode,
|
|
2353
|
+
replyLogger: params.replyLogger,
|
|
2354
|
+
connectionId: params.connectionId,
|
|
2355
|
+
skipLog: false,
|
|
2356
|
+
tableMode
|
|
2357
|
+
})).providerAccepted) {
|
|
2358
|
+
params.replyLogger.warn({
|
|
2359
|
+
correlationId: params.msg.id ?? null,
|
|
2360
|
+
connectionId: params.connectionId,
|
|
2361
|
+
conversationId: params.conversationId,
|
|
2362
|
+
chatId: params.msg.chatId,
|
|
2363
|
+
to: params.msg.from,
|
|
2364
|
+
from: params.msg.to,
|
|
2365
|
+
replyKind: info.kind
|
|
2366
|
+
}, "auto-reply was not accepted by WhatsApp provider");
|
|
2367
|
+
return;
|
|
2368
|
+
}
|
|
2369
|
+
didSendReply = true;
|
|
2370
|
+
const shouldLog = normalizedDeliveryPayload.text ? true : void 0;
|
|
2371
|
+
params.rememberSentText(normalizedDeliveryPayload.text, {
|
|
2372
|
+
combinedBody: params.context.Body,
|
|
2373
|
+
combinedBodySessionKey: params.route.sessionKey,
|
|
2374
|
+
logVerboseMessage: shouldLog
|
|
2375
|
+
});
|
|
2376
|
+
const fromDisplay = params.msg.chatType === "group" ? params.conversationId : params.msg.from ?? "unknown";
|
|
2377
|
+
if (shouldLogVerbose$1()) logVerbose$1(`Reply body: ${normalizedDeliveryPayload.text != null ? reply.text : "<media>"}${reply.hasMedia ? " (media)" : ""} -> ${fromDisplay}`);
|
|
2378
|
+
},
|
|
2379
|
+
onReplyStart: params.msg.sendComposing,
|
|
2380
|
+
onError: (err, info) => {
|
|
2381
|
+
logWhatsAppReplyDeliveryError({
|
|
2382
|
+
err,
|
|
2383
|
+
info,
|
|
2384
|
+
connectionId: params.connectionId,
|
|
2385
|
+
conversationId: params.conversationId,
|
|
2386
|
+
msg: params.msg,
|
|
2387
|
+
replyLogger: params.replyLogger
|
|
2388
|
+
});
|
|
2389
|
+
}
|
|
2390
|
+
},
|
|
2391
|
+
replyOptions: {
|
|
2392
|
+
disableBlockStreaming,
|
|
2393
|
+
onModelSelected: params.onModelSelected
|
|
2394
|
+
}
|
|
2395
|
+
});
|
|
2396
|
+
if (!hasVisibleInboundReplyDispatch({
|
|
2397
|
+
queuedFinal,
|
|
2398
|
+
counts
|
|
2399
|
+
})) {
|
|
2400
|
+
if (params.shouldClearGroupHistory) params.groupHistories.set(params.groupHistoryKey, []);
|
|
2401
|
+
logVerbose$1("Skipping auto-reply: silent token or no text/media returned from resolver");
|
|
2402
|
+
return false;
|
|
2403
|
+
}
|
|
2404
|
+
if (params.shouldClearGroupHistory) params.groupHistories.set(params.groupHistoryKey, []);
|
|
2405
|
+
return didSendReply;
|
|
2406
|
+
}
|
|
2407
|
+
//#endregion
|
|
2408
|
+
//#region extensions/whatsapp/src/auto-reply/monitor/message-line.runtime.ts
|
|
2409
|
+
function normalizeAgentId$1(agentId) {
|
|
2410
|
+
return agentId.trim().toLowerCase() || "main";
|
|
2411
|
+
}
|
|
2412
|
+
function resolveIdentityNamePrefix(cfg, agentId) {
|
|
2413
|
+
const normalizedAgentId = normalizeAgentId$1(agentId);
|
|
2414
|
+
const identityName = cfg.agents?.list?.find((agent) => normalizeAgentId$1(agent.id ?? "") === normalizedAgentId)?.identity?.name?.trim();
|
|
2415
|
+
return identityName ? `[${identityName}]` : void 0;
|
|
2416
|
+
}
|
|
2417
|
+
function resolveMessagePrefix(cfg, agentId, opts) {
|
|
2418
|
+
const configured = opts?.configured ?? cfg.messages?.messagePrefix;
|
|
2419
|
+
if (configured !== void 0) return configured;
|
|
2420
|
+
if (opts?.hasAllowFrom === true) return "";
|
|
2421
|
+
return resolveIdentityNamePrefix(cfg, agentId) ?? opts?.fallback ?? "[openclaw]";
|
|
2422
|
+
}
|
|
2423
|
+
//#endregion
|
|
2424
|
+
//#region extensions/whatsapp/src/auto-reply/monitor/message-line.ts
|
|
2425
|
+
function formatReplyContext(msg) {
|
|
2426
|
+
const replyTo = getReplyContext(msg);
|
|
2427
|
+
if (!replyTo?.body) return null;
|
|
2428
|
+
return `[Replying to ${replyTo.sender?.label ?? replyTo.sender?.e164 ?? "unknown sender"}${replyTo.id ? ` id:${replyTo.id}` : ""}]\n${replyTo.body}\n[/Replying]`;
|
|
2429
|
+
}
|
|
2430
|
+
function buildInboundLine(params) {
|
|
2431
|
+
const { cfg, msg, agentId, previousTimestamp, envelope } = params;
|
|
2432
|
+
const messagePrefix = resolveMessagePrefix(cfg, agentId, {
|
|
2433
|
+
configured: cfg.channels?.whatsapp?.messagePrefix,
|
|
2434
|
+
hasAllowFrom: (cfg.channels?.whatsapp?.allowFrom?.length ?? 0) > 0
|
|
2435
|
+
});
|
|
2436
|
+
const prefixStr = messagePrefix ? `${messagePrefix} ` : "";
|
|
2437
|
+
const replyContext = formatReplyContext(msg);
|
|
2438
|
+
const baseLine = `${prefixStr}${msg.body}${replyContext ? `\n\n${replyContext}` : ""}`;
|
|
2439
|
+
const sender = getSenderIdentity(msg);
|
|
2440
|
+
return formatInboundEnvelope({
|
|
2441
|
+
channel: "WhatsApp",
|
|
2442
|
+
from: msg.chatType === "group" ? msg.from : msg.from?.replace(/^whatsapp:/, ""),
|
|
2443
|
+
timestamp: msg.timestamp,
|
|
2444
|
+
body: baseLine,
|
|
2445
|
+
chatType: msg.chatType,
|
|
2446
|
+
sender: {
|
|
2447
|
+
name: sender.name ?? void 0,
|
|
2448
|
+
e164: sender.e164 ?? void 0,
|
|
2449
|
+
id: getPrimaryIdentityId(sender) ?? void 0
|
|
2450
|
+
},
|
|
2451
|
+
previousTimestamp,
|
|
2452
|
+
envelope,
|
|
2453
|
+
fromMe: msg.fromMe
|
|
2454
|
+
});
|
|
2455
|
+
}
|
|
2456
|
+
//#endregion
|
|
2457
|
+
//#region extensions/whatsapp/src/auto-reply/monitor/process-message.ts
|
|
2458
|
+
const WHATSAPP_MESSAGE_RECEIVED_HOOK_LIMITS = {
|
|
2459
|
+
maxConcurrency: 8,
|
|
2460
|
+
maxQueue: 128,
|
|
2461
|
+
timeoutMs: 2e3
|
|
2462
|
+
};
|
|
2463
|
+
function readWhatsAppMessageReceivedHookOptIn(value) {
|
|
2464
|
+
if (!value || typeof value !== "object") return;
|
|
2465
|
+
return value.pluginHooks?.messageReceived === true ? true : void 0;
|
|
2466
|
+
}
|
|
2467
|
+
function shouldEmitWhatsAppMessageReceivedHooks(params) {
|
|
2468
|
+
const channelConfig = params.cfg.channels?.whatsapp;
|
|
2469
|
+
return readWhatsAppMessageReceivedHookOptIn(params.accountId && channelConfig?.accounts ? channelConfig.accounts[params.accountId] : void 0) ?? readWhatsAppMessageReceivedHookOptIn(channelConfig) ?? false;
|
|
2470
|
+
}
|
|
2471
|
+
function emitWhatsAppMessageReceivedHooks(params) {
|
|
2472
|
+
const canonical = deriveInboundMessageHookContext(params.ctx);
|
|
2473
|
+
const hookRunner = getGlobalHookRunner();
|
|
2474
|
+
if (hookRunner?.hasHooks("message_received")) fireAndForgetBoundedHook(() => hookRunner.runMessageReceived(toPluginMessageReceivedEvent(canonical), toPluginMessageContext(canonical)), "whatsapp: message_received plugin hook failed", void 0, WHATSAPP_MESSAGE_RECEIVED_HOOK_LIMITS);
|
|
2475
|
+
fireAndForgetBoundedHook(() => triggerInternalHook(createInternalHookEvent("message", "received", params.sessionKey, toInternalMessageReceivedContext(canonical))), "whatsapp: message_received internal hook failed", void 0, WHATSAPP_MESSAGE_RECEIVED_HOOK_LIMITS);
|
|
2476
|
+
}
|
|
2477
|
+
function emitWhatsAppMessageReceivedHooksIfEnabled(params) {
|
|
2478
|
+
if (!shouldEmitWhatsAppMessageReceivedHooks({
|
|
2479
|
+
cfg: params.cfg,
|
|
2480
|
+
accountId: params.accountId
|
|
2481
|
+
})) return;
|
|
2482
|
+
emitWhatsAppMessageReceivedHooks({
|
|
2483
|
+
ctx: params.ctx,
|
|
2484
|
+
sessionKey: params.sessionKey
|
|
2485
|
+
});
|
|
2486
|
+
}
|
|
2487
|
+
function resolvePinnedMainDmRecipient(params) {
|
|
2488
|
+
return resolvePinnedMainDmOwnerFromAllowlist({
|
|
2489
|
+
dmScope: params.cfg.session?.dmScope,
|
|
2490
|
+
allowFrom: params.allowFrom,
|
|
2491
|
+
normalizeEntry: (entry) => (0, text_runtime_exports.normalizeE164)(entry)
|
|
2492
|
+
});
|
|
2493
|
+
}
|
|
2494
|
+
async function processMessage(params) {
|
|
2495
|
+
const conversationId = params.msg.conversationId ?? params.msg.from;
|
|
2496
|
+
const self = getSelfIdentity(params.msg);
|
|
2497
|
+
const inboundPolicy = resolveWhatsAppInboundPolicy({
|
|
2498
|
+
cfg: params.cfg,
|
|
2499
|
+
accountId: params.route.accountId ?? params.msg.accountId,
|
|
2500
|
+
selfE164: self.e164 ?? null
|
|
2501
|
+
});
|
|
2502
|
+
const account = inboundPolicy.account;
|
|
2503
|
+
const contextVisibilityMode = resolveChannelContextVisibilityMode({
|
|
2504
|
+
cfg: params.cfg,
|
|
2505
|
+
channel: "whatsapp",
|
|
2506
|
+
accountId: account.accountId
|
|
2507
|
+
});
|
|
2508
|
+
const { storePath, envelopeOptions, previousTimestamp } = resolveInboundSessionEnvelopeContext({
|
|
2509
|
+
cfg: params.cfg,
|
|
2510
|
+
agentId: params.route.agentId,
|
|
2511
|
+
sessionKey: params.route.sessionKey
|
|
2512
|
+
});
|
|
2513
|
+
let audioTranscript = params.preflightAudioTranscript ?? void 0;
|
|
2514
|
+
const hasAudioBody = params.msg.mediaType?.startsWith("audio/") === true && params.msg.body === "<media:audio>";
|
|
2515
|
+
if (params.preflightAudioTranscript === void 0 && hasAudioBody && params.msg.mediaPath) try {
|
|
2516
|
+
const { transcribeFirstAudio } = await import("./audio-preflight.runtime-Bcdsl_ri.js");
|
|
2517
|
+
audioTranscript = await transcribeFirstAudio({
|
|
2518
|
+
ctx: {
|
|
2519
|
+
MediaPaths: [params.msg.mediaPath],
|
|
2520
|
+
MediaTypes: params.msg.mediaType ? [params.msg.mediaType] : void 0
|
|
2521
|
+
},
|
|
2522
|
+
cfg: params.cfg
|
|
2523
|
+
});
|
|
2524
|
+
} catch {
|
|
2525
|
+
if (shouldLogVerbose$1()) logVerbose$1("whatsapp: audio preflight transcription failed, using placeholder");
|
|
2526
|
+
}
|
|
2527
|
+
const msgForAgent = audioTranscript !== void 0 ? {
|
|
2528
|
+
...params.msg,
|
|
2529
|
+
body: audioTranscript
|
|
2530
|
+
} : params.msg;
|
|
2531
|
+
let combinedBody = buildInboundLine({
|
|
2532
|
+
cfg: params.cfg,
|
|
2533
|
+
msg: msgForAgent,
|
|
2534
|
+
agentId: params.route.agentId,
|
|
2535
|
+
previousTimestamp,
|
|
2536
|
+
envelope: envelopeOptions
|
|
2537
|
+
});
|
|
2538
|
+
let shouldClearGroupHistory = false;
|
|
2539
|
+
const visibleGroupHistory = params.msg.chatType === "group" ? resolveVisibleWhatsAppGroupHistory({
|
|
2540
|
+
history: params.groupHistory ?? params.groupHistories.get(params.groupHistoryKey) ?? [],
|
|
2541
|
+
mode: contextVisibilityMode,
|
|
2542
|
+
groupPolicy: inboundPolicy.groupPolicy,
|
|
2543
|
+
groupAllowFrom: inboundPolicy.groupAllowFrom
|
|
2544
|
+
}) : void 0;
|
|
2545
|
+
if (params.msg.chatType === "group") {
|
|
2546
|
+
const history = visibleGroupHistory ?? [];
|
|
2547
|
+
if (history.length > 0) combinedBody = buildHistoryContextFromEntries({
|
|
2548
|
+
entries: history.map((m) => ({
|
|
2549
|
+
sender: m.sender,
|
|
2550
|
+
body: m.body,
|
|
2551
|
+
timestamp: m.timestamp
|
|
2552
|
+
})),
|
|
2553
|
+
currentMessage: combinedBody,
|
|
2554
|
+
excludeLast: false,
|
|
2555
|
+
formatEntry: (entry) => {
|
|
2556
|
+
return formatInboundEnvelope$1({
|
|
2557
|
+
channel: "WhatsApp",
|
|
2558
|
+
from: conversationId,
|
|
2559
|
+
timestamp: entry.timestamp,
|
|
2560
|
+
body: entry.body,
|
|
2561
|
+
chatType: "group",
|
|
2562
|
+
senderLabel: entry.sender,
|
|
2563
|
+
envelope: envelopeOptions
|
|
2564
|
+
});
|
|
2565
|
+
}
|
|
2566
|
+
});
|
|
2567
|
+
shouldClearGroupHistory = !(params.suppressGroupHistoryClear ?? false);
|
|
2568
|
+
}
|
|
2569
|
+
const combinedEchoKey = params.buildCombinedEchoKey({
|
|
2570
|
+
sessionKey: params.route.sessionKey,
|
|
2571
|
+
combinedBody
|
|
2572
|
+
});
|
|
2573
|
+
if (params.echoHas(combinedEchoKey)) {
|
|
2574
|
+
logVerbose$1("Skipping auto-reply: detected echo for combined message");
|
|
2575
|
+
params.echoForget(combinedEchoKey);
|
|
2576
|
+
return false;
|
|
2577
|
+
}
|
|
2578
|
+
let ackReaction = params.ackReaction ?? null;
|
|
2579
|
+
if (!ackReaction && params.ackAlreadySent !== true) ackReaction = await maybeSendAckReaction({
|
|
2580
|
+
cfg: params.cfg,
|
|
2581
|
+
msg: params.msg,
|
|
2582
|
+
agentId: params.route.agentId,
|
|
2583
|
+
sessionKey: params.route.sessionKey,
|
|
2584
|
+
conversationId,
|
|
2585
|
+
verbose: params.verbose,
|
|
2586
|
+
accountId: account.accountId,
|
|
2587
|
+
info: params.replyLogger.info.bind(params.replyLogger),
|
|
2588
|
+
warn: params.replyLogger.warn.bind(params.replyLogger)
|
|
2589
|
+
});
|
|
2590
|
+
const correlationId = params.msg.id ?? newConnectionId();
|
|
2591
|
+
params.replyLogger.info({
|
|
2592
|
+
connectionId: params.connectionId,
|
|
2593
|
+
correlationId,
|
|
2594
|
+
from: params.msg.chatType === "group" ? conversationId : params.msg.from,
|
|
2595
|
+
to: params.msg.to,
|
|
2596
|
+
body: elide(combinedBody, 240),
|
|
2597
|
+
mediaType: params.msg.mediaType ?? null,
|
|
2598
|
+
mediaPath: params.msg.mediaPath ?? null
|
|
2599
|
+
}, "inbound web message");
|
|
2600
|
+
const fromDisplay = params.msg.chatType === "group" ? conversationId : params.msg.from;
|
|
2601
|
+
const kindLabel = params.msg.mediaType ? `, ${params.msg.mediaType}` : "";
|
|
2602
|
+
whatsappInboundLog.info(`Inbound message ${fromDisplay} -> ${params.msg.to} (${params.msg.chatType}${kindLabel}, ${combinedBody.length} chars)`);
|
|
2603
|
+
if (shouldLogVerbose$1()) whatsappInboundLog.debug(`Inbound body: ${elide(combinedBody, 400)}`);
|
|
2604
|
+
const sender = getSenderIdentity(params.msg);
|
|
2605
|
+
const visibleReplyTo = resolveVisibleWhatsAppReplyContext({
|
|
2606
|
+
msg: params.msg,
|
|
2607
|
+
authDir: account.authDir,
|
|
2608
|
+
mode: contextVisibilityMode,
|
|
2609
|
+
groupPolicy: inboundPolicy.groupPolicy,
|
|
2610
|
+
groupAllowFrom: inboundPolicy.groupAllowFrom
|
|
2611
|
+
});
|
|
2612
|
+
const dmRouteTarget = resolveWhatsAppDmRouteTarget({
|
|
2613
|
+
msg: params.msg,
|
|
2614
|
+
senderE164: sender.e164 ?? void 0,
|
|
2615
|
+
normalizeE164: text_runtime_exports.normalizeE164
|
|
2616
|
+
});
|
|
2617
|
+
const commandAuthorized = shouldComputeCommandAuthorized(params.msg.body, params.cfg) ? await resolveWhatsAppCommandAuthorized({
|
|
2618
|
+
cfg: params.cfg,
|
|
2619
|
+
msg: params.msg,
|
|
2620
|
+
policy: inboundPolicy
|
|
2621
|
+
}) : void 0;
|
|
2622
|
+
const { onModelSelected, ...replyPipeline } = createChannelReplyPipeline({
|
|
2623
|
+
cfg: params.cfg,
|
|
2624
|
+
agentId: params.route.agentId,
|
|
2625
|
+
channel: "whatsapp",
|
|
2626
|
+
accountId: params.route.accountId
|
|
2627
|
+
});
|
|
2628
|
+
const responsePrefix = resolveWhatsAppResponsePrefix({
|
|
2629
|
+
cfg: params.cfg,
|
|
2630
|
+
agentId: params.route.agentId,
|
|
2631
|
+
isSelfChat: params.msg.chatType !== "group" && inboundPolicy.isSelfChat,
|
|
2632
|
+
pipelineResponsePrefix: replyPipeline.responsePrefix
|
|
2633
|
+
});
|
|
2634
|
+
const replyThreading = resolveBatchedReplyThreadingPolicy(account.replyToMode ?? "off", params.msg.isBatched === true);
|
|
2635
|
+
const conversationSystemPrompt = params.msg.chatType === "group" ? resolveWhatsAppGroupSystemPrompt({
|
|
2636
|
+
accountConfig: account,
|
|
2637
|
+
groupId: conversationId
|
|
2638
|
+
}) : resolveWhatsAppDirectSystemPrompt({
|
|
2639
|
+
accountConfig: account,
|
|
2640
|
+
peerId: dmRouteTarget ?? params.msg.from
|
|
2641
|
+
});
|
|
2642
|
+
const ctxPayload = buildWhatsAppInboundContext({
|
|
2643
|
+
bodyForAgent: msgForAgent.body,
|
|
2644
|
+
combinedBody,
|
|
2645
|
+
commandBody: params.msg.body,
|
|
2646
|
+
commandAuthorized,
|
|
2647
|
+
conversationId,
|
|
2648
|
+
groupHistory: visibleGroupHistory,
|
|
2649
|
+
groupMemberRoster: params.groupMemberNames.get(params.groupHistoryKey),
|
|
2650
|
+
groupSystemPrompt: conversationSystemPrompt,
|
|
2651
|
+
msg: params.msg,
|
|
2652
|
+
rawBody: params.msg.body,
|
|
2653
|
+
route: params.route,
|
|
2654
|
+
sender: {
|
|
2655
|
+
id: getPrimaryIdentityId(sender) ?? void 0,
|
|
2656
|
+
name: sender.name ?? void 0,
|
|
2657
|
+
e164: sender.e164 ?? void 0
|
|
2658
|
+
},
|
|
2659
|
+
...audioTranscript !== void 0 ? { transcript: audioTranscript } : {},
|
|
2660
|
+
...audioTranscript !== void 0 ? { mediaTranscribedIndexes: [0] } : {},
|
|
2661
|
+
replyThreading,
|
|
2662
|
+
visibleReplyTo: visibleReplyTo ?? void 0
|
|
2663
|
+
});
|
|
2664
|
+
emitWhatsAppMessageReceivedHooksIfEnabled({
|
|
2665
|
+
cfg: params.cfg,
|
|
2666
|
+
ctx: ctxPayload,
|
|
2667
|
+
accountId: params.route.accountId,
|
|
2668
|
+
sessionKey: params.route.sessionKey
|
|
2669
|
+
});
|
|
2670
|
+
const pinnedMainDmRecipient = resolvePinnedMainDmRecipient({
|
|
2671
|
+
cfg: params.cfg,
|
|
2672
|
+
allowFrom: inboundPolicy.configuredAllowFrom
|
|
2673
|
+
});
|
|
2674
|
+
updateWhatsAppMainLastRoute({
|
|
2675
|
+
backgroundTasks: params.backgroundTasks,
|
|
2676
|
+
cfg: params.cfg,
|
|
2677
|
+
ctx: ctxPayload,
|
|
2678
|
+
dmRouteTarget,
|
|
2679
|
+
pinnedMainDmRecipient,
|
|
2680
|
+
route: params.route,
|
|
2681
|
+
updateLastRoute: updateLastRouteInBackground,
|
|
2682
|
+
warn: params.replyLogger.warn.bind(params.replyLogger)
|
|
2683
|
+
});
|
|
2684
|
+
const turnResult = await runInboundReplyTurn({
|
|
2685
|
+
channel: "whatsapp",
|
|
2686
|
+
accountId: params.route.accountId,
|
|
2687
|
+
raw: params.msg,
|
|
2688
|
+
adapter: {
|
|
2689
|
+
ingest: () => ({
|
|
2690
|
+
id: params.msg.id ?? `${conversationId}:${Date.now()}`,
|
|
2691
|
+
timestamp: params.msg.timestamp,
|
|
2692
|
+
rawText: ctxPayload.RawBody ?? "",
|
|
2693
|
+
textForAgent: ctxPayload.BodyForAgent,
|
|
2694
|
+
textForCommands: ctxPayload.CommandBody,
|
|
2695
|
+
raw: params.msg
|
|
2696
|
+
}),
|
|
2697
|
+
resolveTurn: () => ({
|
|
2698
|
+
channel: "whatsapp",
|
|
2699
|
+
accountId: params.route.accountId,
|
|
2700
|
+
routeSessionKey: params.route.sessionKey,
|
|
2701
|
+
storePath,
|
|
2702
|
+
ctxPayload,
|
|
2703
|
+
recordInboundSession,
|
|
2704
|
+
record: {
|
|
2705
|
+
onRecordError: (err) => {
|
|
2706
|
+
params.replyLogger.warn({
|
|
2707
|
+
error: formatError(err),
|
|
2708
|
+
storePath,
|
|
2709
|
+
sessionKey: params.route.sessionKey
|
|
2710
|
+
}, "failed updating session meta");
|
|
2711
|
+
},
|
|
2712
|
+
trackSessionMetaTask: (task) => {
|
|
2713
|
+
trackBackgroundTask(params.backgroundTasks, task);
|
|
2714
|
+
}
|
|
2715
|
+
},
|
|
2716
|
+
runDispatch: () => dispatchWhatsAppBufferedReply({
|
|
2717
|
+
cfg: params.cfg,
|
|
2718
|
+
connectionId: params.connectionId,
|
|
2719
|
+
context: ctxPayload,
|
|
2720
|
+
conversationId,
|
|
2721
|
+
deliverReply: deliverWebReply,
|
|
2722
|
+
groupHistories: params.groupHistories,
|
|
2723
|
+
groupHistoryKey: params.groupHistoryKey,
|
|
2724
|
+
maxMediaBytes: params.maxMediaBytes,
|
|
2725
|
+
maxMediaTextChunkLimit: params.maxMediaTextChunkLimit,
|
|
2726
|
+
msg: params.msg,
|
|
2727
|
+
onModelSelected,
|
|
2728
|
+
rememberSentText: params.rememberSentText,
|
|
2729
|
+
replyLogger: params.replyLogger,
|
|
2730
|
+
replyPipeline: {
|
|
2731
|
+
...replyPipeline,
|
|
2732
|
+
responsePrefix
|
|
2733
|
+
},
|
|
2734
|
+
replyResolver: params.replyResolver,
|
|
2735
|
+
route: params.route,
|
|
2736
|
+
shouldClearGroupHistory
|
|
2737
|
+
})
|
|
2738
|
+
})
|
|
2739
|
+
}
|
|
2740
|
+
});
|
|
2741
|
+
const didSendReply = turnResult.dispatched ? turnResult.dispatchResult : false;
|
|
2742
|
+
removeAckReactionHandleAfterReply({
|
|
2743
|
+
removeAfterReply: Boolean(params.cfg.messages?.removeAckAfterReply && didSendReply),
|
|
2744
|
+
ackReaction,
|
|
2745
|
+
onError: (err) => {
|
|
2746
|
+
logAckFailure({
|
|
2747
|
+
log: logVerbose$1,
|
|
2748
|
+
channel: "whatsapp",
|
|
2749
|
+
target: `${params.msg.chatId ?? conversationId}/${params.msg.id ?? "unknown"}`,
|
|
2750
|
+
error: err
|
|
2751
|
+
});
|
|
2752
|
+
}
|
|
2753
|
+
});
|
|
2754
|
+
return didSendReply;
|
|
2755
|
+
}
|
|
2756
|
+
//#endregion
|
|
2757
|
+
//#region extensions/whatsapp/src/auto-reply/monitor/on-message.ts
|
|
2758
|
+
function createWebOnMessageHandler(params) {
|
|
2759
|
+
const processForRoute = async (msg, route, groupHistoryKey, opts) => {
|
|
2760
|
+
const processParams = {
|
|
2761
|
+
cfg: params.cfg,
|
|
2762
|
+
msg,
|
|
2763
|
+
route,
|
|
2764
|
+
groupHistoryKey,
|
|
2765
|
+
groupHistories: params.groupHistories,
|
|
2766
|
+
groupMemberNames: params.groupMemberNames,
|
|
2767
|
+
connectionId: params.connectionId,
|
|
2768
|
+
verbose: params.verbose,
|
|
2769
|
+
maxMediaBytes: params.maxMediaBytes,
|
|
2770
|
+
replyResolver: params.replyResolver,
|
|
2771
|
+
replyLogger: params.replyLogger,
|
|
2772
|
+
backgroundTasks: params.backgroundTasks,
|
|
2773
|
+
rememberSentText: params.echoTracker.rememberText,
|
|
2774
|
+
echoHas: params.echoTracker.has,
|
|
2775
|
+
echoForget: params.echoTracker.forget,
|
|
2776
|
+
buildCombinedEchoKey: params.echoTracker.buildCombinedKey
|
|
2777
|
+
};
|
|
2778
|
+
if (opts?.groupHistory !== void 0) processParams.groupHistory = opts.groupHistory;
|
|
2779
|
+
if (opts?.suppressGroupHistoryClear !== void 0) processParams.suppressGroupHistoryClear = opts.suppressGroupHistoryClear;
|
|
2780
|
+
if (opts?.preflightAudioTranscript !== void 0) processParams.preflightAudioTranscript = opts.preflightAudioTranscript;
|
|
2781
|
+
if (opts?.ackAlreadySent === true) processParams.ackAlreadySent = true;
|
|
2782
|
+
if (opts?.ackReaction !== void 0) processParams.ackReaction = opts.ackReaction;
|
|
2783
|
+
return processMessage(processParams);
|
|
2784
|
+
};
|
|
2785
|
+
return async (msg) => {
|
|
2786
|
+
const conversationId = msg.conversationId ?? msg.from;
|
|
2787
|
+
const peerId = resolvePeerId(msg);
|
|
2788
|
+
const baseRoute = resolveAgentRoute({
|
|
2789
|
+
cfg: getRuntimeConfig$1(),
|
|
2790
|
+
channel: "whatsapp",
|
|
2791
|
+
accountId: msg.accountId,
|
|
2792
|
+
peer: {
|
|
2793
|
+
kind: msg.chatType === "group" ? "group" : "direct",
|
|
2794
|
+
id: peerId
|
|
2795
|
+
}
|
|
2796
|
+
});
|
|
2797
|
+
const route = msg.chatType === "group" ? resolveWhatsAppGroupSessionRoute(baseRoute) : baseRoute;
|
|
2798
|
+
const groupHistoryKey = msg.chatType === "group" ? buildGroupHistoryKey({
|
|
2799
|
+
channel: "whatsapp",
|
|
2800
|
+
accountId: route.accountId,
|
|
2801
|
+
peerKind: "group",
|
|
2802
|
+
peerId
|
|
2803
|
+
}) : route.sessionKey;
|
|
2804
|
+
if (msg.from === msg.to) logVerbose(`📱 Same-phone mode detected (from === to: ${msg.from})`);
|
|
2805
|
+
if (params.echoTracker.has(msg.body)) {
|
|
2806
|
+
logVerbose("Skipping auto-reply: detected echo (message matches recently sent text)");
|
|
2807
|
+
params.echoTracker.forget(msg.body);
|
|
2808
|
+
return;
|
|
2809
|
+
}
|
|
2810
|
+
let preflightAudioTranscript;
|
|
2811
|
+
const hasAudioBody = msg.mediaType?.startsWith("audio/") === true && msg.body === "<media:audio>";
|
|
2812
|
+
const canRunEarlyAudioPreflight = msg.chatType === "group" || msg.accessControlPassed === true;
|
|
2813
|
+
let ackAlreadySent = false;
|
|
2814
|
+
let ackReaction = null;
|
|
2815
|
+
const runAudioPreflightOnce = async () => {
|
|
2816
|
+
if (preflightAudioTranscript !== void 0 || !canRunEarlyAudioPreflight || !hasAudioBody || !msg.mediaPath) return;
|
|
2817
|
+
ackReaction = await maybeSendAckReaction({
|
|
2818
|
+
cfg: params.cfg,
|
|
2819
|
+
msg,
|
|
2820
|
+
agentId: route.agentId,
|
|
2821
|
+
sessionKey: route.sessionKey,
|
|
2822
|
+
conversationId,
|
|
2823
|
+
verbose: params.verbose,
|
|
2824
|
+
accountId: route.accountId,
|
|
2825
|
+
info: params.replyLogger.info.bind(params.replyLogger),
|
|
2826
|
+
warn: params.replyLogger.warn.bind(params.replyLogger)
|
|
2827
|
+
});
|
|
2828
|
+
ackAlreadySent = ackReaction !== null;
|
|
2829
|
+
try {
|
|
2830
|
+
const { transcribeFirstAudio } = await import("./audio-preflight.runtime-Bcdsl_ri.js");
|
|
2831
|
+
preflightAudioTranscript = await transcribeFirstAudio({
|
|
2832
|
+
ctx: {
|
|
2833
|
+
MediaPaths: [msg.mediaPath],
|
|
2834
|
+
MediaTypes: msg.mediaType ? [msg.mediaType] : void 0
|
|
2835
|
+
},
|
|
2836
|
+
cfg: params.cfg
|
|
2837
|
+
}) ?? null;
|
|
2838
|
+
} catch {
|
|
2839
|
+
preflightAudioTranscript = null;
|
|
2840
|
+
}
|
|
2841
|
+
};
|
|
2842
|
+
if (msg.chatType === "group") {
|
|
2843
|
+
const sender = getSenderIdentity(msg);
|
|
2844
|
+
const metaCtx = {
|
|
2845
|
+
From: msg.from,
|
|
2846
|
+
To: msg.to,
|
|
2847
|
+
SessionKey: route.sessionKey,
|
|
2848
|
+
AccountId: route.accountId,
|
|
2849
|
+
ChatType: msg.chatType,
|
|
2850
|
+
ConversationLabel: conversationId,
|
|
2851
|
+
GroupSubject: msg.groupSubject,
|
|
2852
|
+
SenderName: sender.name ?? void 0,
|
|
2853
|
+
SenderId: getPrimaryIdentityId(sender) ?? void 0,
|
|
2854
|
+
SenderE164: sender.e164 ?? void 0,
|
|
2855
|
+
Provider: "whatsapp",
|
|
2856
|
+
Surface: "whatsapp",
|
|
2857
|
+
OriginatingChannel: "whatsapp",
|
|
2858
|
+
OriginatingTo: conversationId
|
|
2859
|
+
};
|
|
2860
|
+
updateLastRouteInBackground({
|
|
2861
|
+
cfg: params.cfg,
|
|
2862
|
+
backgroundTasks: params.backgroundTasks,
|
|
2863
|
+
storeAgentId: route.agentId,
|
|
2864
|
+
sessionKey: route.sessionKey,
|
|
2865
|
+
channel: "whatsapp",
|
|
2866
|
+
to: conversationId,
|
|
2867
|
+
accountId: route.accountId,
|
|
2868
|
+
ctx: metaCtx,
|
|
2869
|
+
warn: params.replyLogger.warn.bind(params.replyLogger)
|
|
2870
|
+
});
|
|
2871
|
+
let gating = await applyGroupGating({
|
|
2872
|
+
cfg: params.cfg,
|
|
2873
|
+
msg,
|
|
2874
|
+
deferMissingMention: hasAudioBody && Boolean(msg.mediaPath),
|
|
2875
|
+
conversationId,
|
|
2876
|
+
groupHistoryKey,
|
|
2877
|
+
agentId: route.agentId,
|
|
2878
|
+
sessionKey: route.sessionKey,
|
|
2879
|
+
baseMentionConfig: params.baseMentionConfig,
|
|
2880
|
+
authDir: params.account.authDir,
|
|
2881
|
+
selfChatMode: params.account.selfChatMode,
|
|
2882
|
+
groupHistories: params.groupHistories,
|
|
2883
|
+
groupHistoryLimit: params.groupHistoryLimit,
|
|
2884
|
+
groupMemberNames: params.groupMemberNames,
|
|
2885
|
+
logVerbose,
|
|
2886
|
+
replyLogger: params.replyLogger
|
|
2887
|
+
});
|
|
2888
|
+
if (!gating.shouldProcess && "needsMentionText" in gating && gating.needsMentionText === true) {
|
|
2889
|
+
await runAudioPreflightOnce();
|
|
2890
|
+
gating = await applyGroupGating({
|
|
2891
|
+
cfg: params.cfg,
|
|
2892
|
+
msg,
|
|
2893
|
+
...typeof preflightAudioTranscript === "string" ? { mentionText: preflightAudioTranscript } : {},
|
|
2894
|
+
conversationId,
|
|
2895
|
+
groupHistoryKey,
|
|
2896
|
+
agentId: route.agentId,
|
|
2897
|
+
sessionKey: route.sessionKey,
|
|
2898
|
+
baseMentionConfig: params.baseMentionConfig,
|
|
2899
|
+
authDir: params.account.authDir,
|
|
2900
|
+
selfChatMode: params.account.selfChatMode,
|
|
2901
|
+
groupHistories: params.groupHistories,
|
|
2902
|
+
groupHistoryLimit: params.groupHistoryLimit,
|
|
2903
|
+
groupMemberNames: params.groupMemberNames,
|
|
2904
|
+
logVerbose,
|
|
2905
|
+
replyLogger: params.replyLogger
|
|
2906
|
+
});
|
|
2907
|
+
}
|
|
2908
|
+
if (!gating.shouldProcess) return;
|
|
2909
|
+
} else if (!msg.sender?.e164 && !msg.senderE164 && peerId && peerId.startsWith("+")) {
|
|
2910
|
+
const normalized = (0, text_runtime_exports.normalizeE164)(peerId);
|
|
2911
|
+
if (normalized) {
|
|
2912
|
+
msg.sender = {
|
|
2913
|
+
...msg.sender,
|
|
2914
|
+
e164: normalized
|
|
2915
|
+
};
|
|
2916
|
+
msg.senderE164 = normalized;
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2919
|
+
await runAudioPreflightOnce();
|
|
2920
|
+
if (await maybeBroadcastMessage({
|
|
2921
|
+
cfg: params.cfg,
|
|
2922
|
+
msg,
|
|
2923
|
+
peerId,
|
|
2924
|
+
route,
|
|
2925
|
+
groupHistoryKey,
|
|
2926
|
+
groupHistories: params.groupHistories,
|
|
2927
|
+
...preflightAudioTranscript !== void 0 ? { preflightAudioTranscript } : {},
|
|
2928
|
+
...ackAlreadySent && msg.chatType !== "group" ? { ackAlreadySent: true } : {},
|
|
2929
|
+
...ackReaction && msg.chatType !== "group" ? { ackReaction } : {},
|
|
2930
|
+
processMessage: (m, r, k, opts) => processForRoute(m, r, k, opts)
|
|
2931
|
+
})) return;
|
|
2932
|
+
await processForRoute(msg, route, groupHistoryKey, {
|
|
2933
|
+
...preflightAudioTranscript !== void 0 ? { preflightAudioTranscript } : {},
|
|
2934
|
+
...ackAlreadySent ? { ackAlreadySent: true } : {},
|
|
2935
|
+
...ackReaction ? { ackReaction } : {}
|
|
2936
|
+
});
|
|
2937
|
+
};
|
|
2938
|
+
}
|
|
2939
|
+
//#endregion
|
|
2940
|
+
//#region extensions/whatsapp/src/auto-reply/monitor.ts
|
|
2941
|
+
function isNonRetryableWebCloseStatus(statusCode) {
|
|
2942
|
+
return statusCode === 440;
|
|
2943
|
+
}
|
|
2944
|
+
let replyResolverRuntimePromise = null;
|
|
2945
|
+
function loadReplyResolverRuntime() {
|
|
2946
|
+
replyResolverRuntimePromise ??= import("./reply-resolver.runtime-BRCgoO4C.js");
|
|
2947
|
+
return replyResolverRuntimePromise;
|
|
2948
|
+
}
|
|
2949
|
+
function normalizeReconnectAccountId(accountId) {
|
|
2950
|
+
return (accountId ?? "").trim() || "default";
|
|
2951
|
+
}
|
|
2952
|
+
function isNoListenerReconnectError(lastError) {
|
|
2953
|
+
return typeof lastError === "string" && /No active WhatsApp Web listener/i.test(lastError);
|
|
2954
|
+
}
|
|
2955
|
+
function resolveExplicitWhatsAppDebounceOverride(params) {
|
|
2956
|
+
const channel = params.sourceCfg?.channels?.whatsapp;
|
|
2957
|
+
if (!channel) return;
|
|
2958
|
+
const accountId = normalizeReconnectAccountId(params.accountId);
|
|
2959
|
+
const accountDebounce = resolveAccountEntry(channel.accounts, accountId)?.debounceMs;
|
|
2960
|
+
if (accountDebounce !== void 0) return accountDebounce;
|
|
2961
|
+
if (accountId !== "default") {
|
|
2962
|
+
const defaultAccountDebounce = resolveAccountEntry(channel.accounts, "default")?.debounceMs;
|
|
2963
|
+
if (defaultAccountDebounce !== void 0) return defaultAccountDebounce;
|
|
2964
|
+
}
|
|
2965
|
+
return channel.debounceMs;
|
|
2966
|
+
}
|
|
2967
|
+
function isRetryableAuthUnstableError(error) {
|
|
2968
|
+
return error instanceof WhatsAppAuthUnstableError || typeof error === "object" && error !== null && "code" in error && error.code === "whatsapp-auth-unstable";
|
|
2969
|
+
}
|
|
2970
|
+
async function clearTerminalWebAuthState(params) {
|
|
2971
|
+
try {
|
|
2972
|
+
const cleared = await logoutWeb({
|
|
2973
|
+
authDir: params.account.authDir,
|
|
2974
|
+
isLegacyAuthDir: params.account.isLegacyAuthDir,
|
|
2975
|
+
runtime: params.runtime
|
|
2976
|
+
});
|
|
2977
|
+
params.log.warn({
|
|
2978
|
+
accountId: params.account.accountId,
|
|
2979
|
+
cleared,
|
|
2980
|
+
healthState: params.healthState,
|
|
2981
|
+
status: params.statusLabel
|
|
2982
|
+
}, "web reconnect: cleared cached auth after terminal close");
|
|
2983
|
+
} catch (error) {
|
|
2984
|
+
params.log.warn({
|
|
2985
|
+
accountId: params.account.accountId,
|
|
2986
|
+
error: formatError(error),
|
|
2987
|
+
healthState: params.healthState,
|
|
2988
|
+
status: params.statusLabel
|
|
2989
|
+
}, "web reconnect: failed clearing cached auth after terminal close");
|
|
2990
|
+
params.runtime.error(`WhatsApp Web cleanup failed after terminal close (status ${params.statusLabel}). Run \`${formatCliCommand("openclaw channels logout --channel whatsapp")}\`, then relink with \`${formatCliCommand("openclaw channels login --channel whatsapp")}\`.`);
|
|
2991
|
+
}
|
|
2992
|
+
}
|
|
2993
|
+
const DEFAULT_TRANSPORT_TIMEOUT_MS = 300 * 1e3;
|
|
2994
|
+
async function monitorWebChannel(verbose, listenerFactory = attachWebInboxToSocket, keepAlive = true, replyResolver, runtime = defaultRuntime, abortSignal, tuning = {}) {
|
|
2995
|
+
const activeReplyResolver = replyResolver ?? (await loadReplyResolverRuntime()).getReplyFromConfig;
|
|
2996
|
+
const runId = newConnectionId();
|
|
2997
|
+
const replyLogger = getChildLogger$1({
|
|
2998
|
+
module: "web-auto-reply",
|
|
2999
|
+
runId
|
|
3000
|
+
});
|
|
3001
|
+
const heartbeatLogger = getChildLogger$1({
|
|
3002
|
+
module: "web-heartbeat",
|
|
3003
|
+
runId
|
|
3004
|
+
});
|
|
3005
|
+
const reconnectLogger = getChildLogger$1({
|
|
3006
|
+
module: "web-reconnect",
|
|
3007
|
+
runId
|
|
3008
|
+
});
|
|
3009
|
+
const statusController = createWebChannelStatusController(tuning.statusSink);
|
|
3010
|
+
statusController.emit();
|
|
3011
|
+
const baseCfg = getRuntimeConfig$1();
|
|
3012
|
+
const sourceCfg = getRuntimeConfigSourceSnapshot();
|
|
3013
|
+
const account = resolveWhatsAppAccount({
|
|
3014
|
+
cfg: baseCfg,
|
|
3015
|
+
accountId: tuning.accountId
|
|
3016
|
+
});
|
|
3017
|
+
const cfg = {
|
|
3018
|
+
...baseCfg,
|
|
3019
|
+
channels: {
|
|
3020
|
+
...baseCfg.channels,
|
|
3021
|
+
whatsapp: {
|
|
3022
|
+
...baseCfg.channels?.whatsapp,
|
|
3023
|
+
ackReaction: account.ackReaction,
|
|
3024
|
+
messagePrefix: account.messagePrefix,
|
|
3025
|
+
allowFrom: account.allowFrom,
|
|
3026
|
+
groupAllowFrom: account.groupAllowFrom,
|
|
3027
|
+
groupPolicy: account.groupPolicy,
|
|
3028
|
+
textChunkLimit: account.textChunkLimit,
|
|
3029
|
+
chunkMode: account.chunkMode,
|
|
3030
|
+
mediaMaxMb: account.mediaMaxMb,
|
|
3031
|
+
blockStreaming: account.blockStreaming,
|
|
3032
|
+
groups: account.groups
|
|
3033
|
+
}
|
|
3034
|
+
}
|
|
3035
|
+
};
|
|
3036
|
+
const maxMediaBytes = resolveWhatsAppMediaMaxBytes(account);
|
|
3037
|
+
const heartbeatSeconds = resolveHeartbeatSeconds(cfg, tuning.heartbeatSeconds);
|
|
3038
|
+
const reconnectPolicy = resolveReconnectPolicy(cfg, tuning.reconnect);
|
|
3039
|
+
const socketTiming = resolveWhatsAppSocketTiming(cfg, tuning.socketTiming);
|
|
3040
|
+
const baseMentionConfig = buildMentionConfig(cfg);
|
|
3041
|
+
const groupHistoryLimit = account.historyLimit ?? cfg.channels?.whatsapp?.historyLimit ?? cfg.messages?.groupChat?.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT;
|
|
3042
|
+
const groupHistories = /* @__PURE__ */ new Map();
|
|
3043
|
+
const groupMemberNames = /* @__PURE__ */ new Map();
|
|
3044
|
+
const groupMetadataCache = /* @__PURE__ */ new Map();
|
|
3045
|
+
const echoTracker = createEchoTracker({
|
|
3046
|
+
maxItems: 100,
|
|
3047
|
+
logVerbose
|
|
3048
|
+
});
|
|
3049
|
+
const sleep = tuning.sleep ?? ((ms, signal) => sleepWithAbort(ms, signal ?? abortSignal));
|
|
3050
|
+
const stopRequested = () => abortSignal?.aborted === true;
|
|
3051
|
+
const currentMaxListeners = process.getMaxListeners?.() ?? 10;
|
|
3052
|
+
if (process.setMaxListeners && currentMaxListeners < 50) process.setMaxListeners(50);
|
|
3053
|
+
let sigintStop = false;
|
|
3054
|
+
const handleSigint = () => {
|
|
3055
|
+
sigintStop = true;
|
|
3056
|
+
};
|
|
3057
|
+
process.once("SIGINT", handleSigint);
|
|
3058
|
+
const transportTimeoutMs = tuning.transportTimeoutMs ?? DEFAULT_TRANSPORT_TIMEOUT_MS;
|
|
3059
|
+
const messageTimeoutMs = tuning.messageTimeoutMs ?? 1800 * 1e3;
|
|
3060
|
+
const watchdogCheckMs = tuning.watchdogCheckMs ?? 60 * 1e3;
|
|
3061
|
+
const controller = new WhatsAppConnectionController({
|
|
3062
|
+
accountId: account.accountId,
|
|
3063
|
+
authDir: account.authDir,
|
|
3064
|
+
verbose,
|
|
3065
|
+
keepAlive,
|
|
3066
|
+
heartbeatSeconds,
|
|
3067
|
+
transportTimeoutMs,
|
|
3068
|
+
messageTimeoutMs,
|
|
3069
|
+
watchdogCheckMs,
|
|
3070
|
+
reconnectPolicy,
|
|
3071
|
+
socketTiming,
|
|
3072
|
+
abortSignal,
|
|
3073
|
+
sleep,
|
|
3074
|
+
isNonRetryableStatus: isNonRetryableWebCloseStatus
|
|
3075
|
+
});
|
|
3076
|
+
try {
|
|
3077
|
+
while (true) {
|
|
3078
|
+
if (stopRequested()) break;
|
|
3079
|
+
const connectionId = newConnectionId();
|
|
3080
|
+
const inboundDebounceMs = resolveInboundDebounceMs({
|
|
3081
|
+
cfg,
|
|
3082
|
+
channel: "whatsapp",
|
|
3083
|
+
overrideMs: resolveExplicitWhatsAppDebounceOverride({
|
|
3084
|
+
cfg,
|
|
3085
|
+
sourceCfg,
|
|
3086
|
+
accountId: account.accountId
|
|
3087
|
+
})
|
|
3088
|
+
});
|
|
3089
|
+
const shouldDebounce = (msg) => {
|
|
3090
|
+
if (msg.mediaPath || msg.mediaType) return false;
|
|
3091
|
+
if (msg.location) return false;
|
|
3092
|
+
if (msg.replyToId || msg.replyToBody) return false;
|
|
3093
|
+
return !hasControlCommand(msg.body, cfg);
|
|
3094
|
+
};
|
|
3095
|
+
let connection;
|
|
3096
|
+
try {
|
|
3097
|
+
connection = await controller.openConnection({
|
|
3098
|
+
connectionId,
|
|
3099
|
+
createListener: async ({ sock, connection }) => {
|
|
3100
|
+
const onMessage = createWebOnMessageHandler({
|
|
3101
|
+
cfg,
|
|
3102
|
+
verbose,
|
|
3103
|
+
connectionId,
|
|
3104
|
+
maxMediaBytes,
|
|
3105
|
+
groupHistoryLimit,
|
|
3106
|
+
groupHistories,
|
|
3107
|
+
groupMemberNames,
|
|
3108
|
+
echoTracker,
|
|
3109
|
+
backgroundTasks: connection.backgroundTasks,
|
|
3110
|
+
replyResolver: activeReplyResolver,
|
|
3111
|
+
replyLogger,
|
|
3112
|
+
baseMentionConfig,
|
|
3113
|
+
account
|
|
3114
|
+
});
|
|
3115
|
+
return await (listenerFactory ?? attachWebInboxToSocket)({
|
|
3116
|
+
cfg,
|
|
3117
|
+
verbose,
|
|
3118
|
+
accountId: account.accountId,
|
|
3119
|
+
authDir: account.authDir,
|
|
3120
|
+
mediaMaxMb: account.mediaMaxMb,
|
|
3121
|
+
selfChatMode: account.selfChatMode,
|
|
3122
|
+
sendReadReceipts: account.sendReadReceipts,
|
|
3123
|
+
debounceMs: inboundDebounceMs,
|
|
3124
|
+
shouldDebounce,
|
|
3125
|
+
socketRef: controller.socketRef,
|
|
3126
|
+
shouldRetryDisconnect: () => !sigintStop && controller.shouldRetryDisconnect(),
|
|
3127
|
+
disconnectRetryPolicy: reconnectPolicy,
|
|
3128
|
+
disconnectRetryAbortSignal: controller.getDisconnectRetryAbortSignal(),
|
|
3129
|
+
groupMetadataCache,
|
|
3130
|
+
onMessage: async (msg) => {
|
|
3131
|
+
const inboundAt = Date.now();
|
|
3132
|
+
controller.noteInbound(inboundAt);
|
|
3133
|
+
statusController.noteInbound(inboundAt);
|
|
3134
|
+
await onMessage(msg);
|
|
3135
|
+
},
|
|
3136
|
+
sock
|
|
3137
|
+
});
|
|
3138
|
+
},
|
|
3139
|
+
onHeartbeat: (snapshot) => {
|
|
3140
|
+
const authAgeMs = getWebAuthAgeMs(account.authDir);
|
|
3141
|
+
const minutesSinceLastMessage = snapshot.lastInboundAt ? Math.floor((Date.now() - snapshot.lastInboundAt) / 6e4) : null;
|
|
3142
|
+
const logData = {
|
|
3143
|
+
connectionId: snapshot.connectionId,
|
|
3144
|
+
reconnectAttempts: snapshot.reconnectAttempts,
|
|
3145
|
+
messagesHandled: snapshot.handledMessages,
|
|
3146
|
+
lastInboundAt: snapshot.lastInboundAt,
|
|
3147
|
+
lastTransportActivityAt: snapshot.lastTransportActivityAt,
|
|
3148
|
+
authAgeMs,
|
|
3149
|
+
uptimeMs: snapshot.uptimeMs,
|
|
3150
|
+
...minutesSinceLastMessage !== null && minutesSinceLastMessage > 30 ? { minutesSinceLastMessage } : {}
|
|
3151
|
+
};
|
|
3152
|
+
statusController.noteTransportActivity(snapshot.lastTransportActivityAt);
|
|
3153
|
+
if (minutesSinceLastMessage && minutesSinceLastMessage > 30) heartbeatLogger.warn(logData, "⚠️ web gateway heartbeat - no messages in 30+ minutes");
|
|
3154
|
+
else heartbeatLogger.info(logData, "web gateway heartbeat");
|
|
3155
|
+
},
|
|
3156
|
+
onWatchdogTimeout: (snapshot) => {
|
|
3157
|
+
const now = Date.now();
|
|
3158
|
+
const transportSilentMs = now - snapshot.lastTransportActivityAt;
|
|
3159
|
+
const appBaselineAt = snapshot.lastInboundAt ?? snapshot.startedAt;
|
|
3160
|
+
const minutesSinceTransportActivity = Math.floor(transportSilentMs / 6e4);
|
|
3161
|
+
const minutesSinceAppActivity = Math.floor((now - appBaselineAt) / 6e4);
|
|
3162
|
+
const watchdogReason = transportSilentMs > transportTimeoutMs ? "transport-inactive" : "app-silent";
|
|
3163
|
+
statusController.noteWatchdogStale();
|
|
3164
|
+
heartbeatLogger.warn({
|
|
3165
|
+
connectionId: snapshot.connectionId,
|
|
3166
|
+
watchdogReason,
|
|
3167
|
+
minutesSinceTransportActivity,
|
|
3168
|
+
minutesSinceAppActivity,
|
|
3169
|
+
lastInboundAt: snapshot.lastInboundAt ? new Date(snapshot.lastInboundAt) : null,
|
|
3170
|
+
lastTransportActivityAt: new Date(snapshot.lastTransportActivityAt),
|
|
3171
|
+
messagesHandled: snapshot.handledMessages
|
|
3172
|
+
}, "WhatsApp watchdog timeout detected - forcing reconnect");
|
|
3173
|
+
whatsappHeartbeatLog.warn(`WhatsApp watchdog timeout (${watchdogReason}) - restarting connection`);
|
|
3174
|
+
}
|
|
3175
|
+
});
|
|
3176
|
+
} catch (error) {
|
|
3177
|
+
if (!isRetryableAuthUnstableError(error)) throw error;
|
|
3178
|
+
const retryDecision = controller.consumeReconnectAttempt();
|
|
3179
|
+
statusController.noteReconnectAttempts(retryDecision.reconnectAttempts);
|
|
3180
|
+
statusController.noteClose({
|
|
3181
|
+
error: error.message,
|
|
3182
|
+
reconnectAttempts: retryDecision.reconnectAttempts,
|
|
3183
|
+
healthState: retryDecision.healthState
|
|
3184
|
+
});
|
|
3185
|
+
if (retryDecision.action === "stop") {
|
|
3186
|
+
reconnectLogger.warn({
|
|
3187
|
+
connectionId,
|
|
3188
|
+
reconnectAttempts: retryDecision.reconnectAttempts,
|
|
3189
|
+
maxAttempts: reconnectPolicy.maxAttempts
|
|
3190
|
+
}, "web reconnect: auth state stayed unstable; max attempts reached");
|
|
3191
|
+
runtime.error(`WhatsApp auth state is still stabilizing after ${retryDecision.reconnectAttempts}/${reconnectPolicy.maxAttempts} attempts. Stopping web monitoring.`);
|
|
3192
|
+
await controller.shutdown();
|
|
3193
|
+
break;
|
|
3194
|
+
}
|
|
3195
|
+
reconnectLogger.info({
|
|
3196
|
+
connectionId,
|
|
3197
|
+
reconnectAttempts: retryDecision.reconnectAttempts,
|
|
3198
|
+
delayMs: retryDecision.delayMs
|
|
3199
|
+
}, "web reconnect: auth state still stabilizing during inbox attach; retrying");
|
|
3200
|
+
runtime.error(`WhatsApp auth state is still stabilizing. Retry ${retryDecision.reconnectAttempts}/${reconnectPolicy.maxAttempts || "∞"} for inbox attach in ${formatDurationPrecise(retryDecision.delayMs ?? 0)}.`);
|
|
3201
|
+
try {
|
|
3202
|
+
await controller.waitBeforeRetry(retryDecision.delayMs ?? 0);
|
|
3203
|
+
} catch {
|
|
3204
|
+
break;
|
|
3205
|
+
}
|
|
3206
|
+
continue;
|
|
3207
|
+
}
|
|
3208
|
+
statusController.noteConnected();
|
|
3209
|
+
controller.setUnhandledRejectionCleanup(registerUnhandledRejectionHandler((reason) => {
|
|
3210
|
+
if (!isLikelyWhatsAppCryptoError(reason)) return false;
|
|
3211
|
+
const errorStr = formatError(reason);
|
|
3212
|
+
reconnectLogger.warn({
|
|
3213
|
+
connectionId: connection.connectionId,
|
|
3214
|
+
error: errorStr
|
|
3215
|
+
}, "web reconnect: unhandled rejection from WhatsApp socket; forcing reconnect");
|
|
3216
|
+
controller.forceClose({
|
|
3217
|
+
status: 499,
|
|
3218
|
+
isLoggedOut: false,
|
|
3219
|
+
error: reason
|
|
3220
|
+
});
|
|
3221
|
+
return true;
|
|
3222
|
+
}));
|
|
3223
|
+
const { e164: selfE164 } = readWebSelfId(account.authDir);
|
|
3224
|
+
const connectRoute = resolveAgentRoute({
|
|
3225
|
+
cfg,
|
|
3226
|
+
channel: "whatsapp",
|
|
3227
|
+
accountId: account.accountId
|
|
3228
|
+
});
|
|
3229
|
+
enqueueSystemEvent(`WhatsApp gateway connected${selfE164 ? ` as ${selfE164}` : ""}.`, {
|
|
3230
|
+
sessionKey: connectRoute.sessionKey,
|
|
3231
|
+
trusted: true
|
|
3232
|
+
});
|
|
3233
|
+
const normalizedAccountId = normalizeReconnectAccountId(account.accountId);
|
|
3234
|
+
drainPendingDeliveries({
|
|
3235
|
+
drainKey: `whatsapp:${normalizedAccountId}`,
|
|
3236
|
+
logLabel: "WhatsApp reconnect drain",
|
|
3237
|
+
cfg,
|
|
3238
|
+
log: reconnectLogger,
|
|
3239
|
+
selectEntry: (entry) => ({
|
|
3240
|
+
match: entry.channel === "whatsapp" && normalizeReconnectAccountId(entry.accountId) === normalizedAccountId,
|
|
3241
|
+
bypassBackoff: isNoListenerReconnectError(entry.lastError)
|
|
3242
|
+
})
|
|
3243
|
+
}).catch((err) => {
|
|
3244
|
+
reconnectLogger.warn({
|
|
3245
|
+
connectionId: connection.connectionId,
|
|
3246
|
+
error: String(err)
|
|
3247
|
+
}, "reconnect drain failed");
|
|
3248
|
+
});
|
|
3249
|
+
whatsappLog.info("Listening for personal WhatsApp inbound messages.");
|
|
3250
|
+
if (process.stdout.isTTY || process.stderr.isTTY) whatsappLog.raw("Ctrl+C to stop.");
|
|
3251
|
+
if (!keepAlive) {
|
|
3252
|
+
await controller.shutdown();
|
|
3253
|
+
return;
|
|
3254
|
+
}
|
|
3255
|
+
const reason = await controller.waitForClose();
|
|
3256
|
+
if (stopRequested() || sigintStop || reason === "aborted") {
|
|
3257
|
+
await controller.shutdown();
|
|
3258
|
+
break;
|
|
3259
|
+
}
|
|
3260
|
+
const decision = controller.resolveCloseDecision(reason);
|
|
3261
|
+
if (decision === "aborted") {
|
|
3262
|
+
await controller.shutdown();
|
|
3263
|
+
break;
|
|
3264
|
+
}
|
|
3265
|
+
statusController.noteReconnectAttempts(controller.getReconnectAttempts());
|
|
3266
|
+
reconnectLogger.info({
|
|
3267
|
+
connectionId: connection.connectionId,
|
|
3268
|
+
status: decision.normalized.statusLabel,
|
|
3269
|
+
loggedOut: decision.normalized.isLoggedOut,
|
|
3270
|
+
reconnectAttempts: decision.reconnectAttempts,
|
|
3271
|
+
error: decision.normalized.errorText
|
|
3272
|
+
}, "web reconnect: connection closed");
|
|
3273
|
+
enqueueSystemEvent(`WhatsApp gateway disconnected (status ${decision.normalized.statusLabel})`, {
|
|
3274
|
+
sessionKey: connectRoute.sessionKey,
|
|
3275
|
+
trusted: true
|
|
3276
|
+
});
|
|
3277
|
+
if (decision.action === "stop") {
|
|
3278
|
+
await controller.closeCurrentConnection();
|
|
3279
|
+
statusController.noteClose({
|
|
3280
|
+
statusCode: decision.normalized.statusCode,
|
|
3281
|
+
loggedOut: decision.normalized.isLoggedOut,
|
|
3282
|
+
error: decision.normalized.errorText,
|
|
3283
|
+
reconnectAttempts: decision.reconnectAttempts,
|
|
3284
|
+
healthState: decision.healthState
|
|
3285
|
+
});
|
|
3286
|
+
if (decision.healthState === "logged-out") {
|
|
3287
|
+
await clearTerminalWebAuthState({
|
|
3288
|
+
account,
|
|
3289
|
+
runtime,
|
|
3290
|
+
statusLabel: decision.normalized.statusLabel,
|
|
3291
|
+
healthState: decision.healthState,
|
|
3292
|
+
log: reconnectLogger
|
|
3293
|
+
});
|
|
3294
|
+
runtime.error(`WhatsApp session logged out. Run \`${formatCliCommand("openclaw channels login --channel whatsapp")}\` to relink.`);
|
|
3295
|
+
} else if (decision.healthState === "conflict") {
|
|
3296
|
+
await clearTerminalWebAuthState({
|
|
3297
|
+
account,
|
|
3298
|
+
runtime,
|
|
3299
|
+
statusLabel: decision.normalized.statusLabel,
|
|
3300
|
+
healthState: decision.healthState,
|
|
3301
|
+
log: reconnectLogger
|
|
3302
|
+
});
|
|
3303
|
+
reconnectLogger.warn({
|
|
3304
|
+
connectionId: connection.connectionId,
|
|
3305
|
+
status: decision.normalized.statusLabel,
|
|
3306
|
+
error: decision.normalized.errorText
|
|
3307
|
+
}, "web reconnect: non-retryable close status; stopping monitor");
|
|
3308
|
+
runtime.error(`WhatsApp Web connection closed (status ${decision.normalized.statusLabel}: session conflict). Resolve conflicting WhatsApp Web sessions, then relink with \`${formatCliCommand("openclaw channels login --channel whatsapp")}\`. Stopping web monitoring.`);
|
|
3309
|
+
} else {
|
|
3310
|
+
reconnectLogger.warn({
|
|
3311
|
+
connectionId: connection.connectionId,
|
|
3312
|
+
status: decision.normalized.statusLabel,
|
|
3313
|
+
reconnectAttempts: decision.reconnectAttempts,
|
|
3314
|
+
maxAttempts: reconnectPolicy.maxAttempts
|
|
3315
|
+
}, "web reconnect: max attempts reached; continuing in degraded mode");
|
|
3316
|
+
runtime.error(`WhatsApp Web reconnect: max attempts reached (${decision.reconnectAttempts}/${reconnectPolicy.maxAttempts}). Stopping web monitoring.`);
|
|
3317
|
+
}
|
|
3318
|
+
await controller.shutdown();
|
|
3319
|
+
break;
|
|
3320
|
+
}
|
|
3321
|
+
statusController.noteClose({
|
|
3322
|
+
statusCode: decision.normalized.statusCode,
|
|
3323
|
+
error: decision.normalized.errorText,
|
|
3324
|
+
reconnectAttempts: decision.reconnectAttempts,
|
|
3325
|
+
healthState: decision.healthState
|
|
3326
|
+
});
|
|
3327
|
+
reconnectLogger.info({
|
|
3328
|
+
connectionId: connection.connectionId,
|
|
3329
|
+
status: decision.normalized.statusLabel,
|
|
3330
|
+
reconnectAttempts: decision.reconnectAttempts,
|
|
3331
|
+
maxAttempts: reconnectPolicy.maxAttempts || "unlimited",
|
|
3332
|
+
delayMs: decision.delayMs
|
|
3333
|
+
}, "web reconnect: scheduling retry");
|
|
3334
|
+
runtime.error(`WhatsApp Web connection closed (status ${decision.normalized.statusLabel}). Retry ${decision.reconnectAttempts}/${reconnectPolicy.maxAttempts || "∞"} in ${formatDurationPrecise(decision.delayMs ?? 0)}… (${decision.normalized.errorText})`);
|
|
3335
|
+
await controller.closeCurrentConnection();
|
|
3336
|
+
try {
|
|
3337
|
+
await controller.waitBeforeRetry(decision.delayMs ?? 0);
|
|
3338
|
+
} catch {
|
|
3339
|
+
break;
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
3342
|
+
} finally {
|
|
3343
|
+
statusController.markStopped();
|
|
3344
|
+
process.removeListener("SIGINT", handleSigint);
|
|
3345
|
+
await controller.shutdown();
|
|
3346
|
+
}
|
|
3347
|
+
}
|
|
3348
|
+
//#endregion
|
|
3349
|
+
export { loadWebMediaRaw as a, monitorWebInbox as c, extractMediaPlaceholder as d, extractText as f, loadWebMedia$1 as i, extractContactContext as l, LocalMediaAccessError as n, optimizeImageToJpeg as o, resetWebInboundDedupe as p, getDefaultLocalRoots as r, optimizeImageToPng as s, monitorWebChannel as t, extractLocationData as u };
|