feat(whatsapp): subscribe to inbound reaction events
Subscribe to Baileys `messages.reaction` events and surface them as system events for the agent session. Follows the same pattern used by Signal and Slack: listen → parse → route → enqueueSystemEvent. - Add `WebInboundReaction` type and `onReaction` callback to monitor - Parse emoji, sender JID, target message ID from Baileys event - Route via `resolveAgentRoute` and enqueue as system event - Handle self-reaction attribution (reactionKey.fromMe → selfJid) - Skip reaction removals (empty emoji) and status/broadcast JIDs - Outer + inner try/catch for resilience against malformed events - 13 unit tests (monitor-level + system event wiring) - Changelog entry and docs updates Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
5f4715acfc
commit
9369e36eb2
77
CHANGELOG.md
77
CHANGELOG.md
@ -3,9 +3,12 @@
|
||||
Docs: https://docs.molt.bot
|
||||
|
||||
## 2026.1.27-beta.1
|
||||
|
||||
Status: beta.
|
||||
|
||||
### Changes
|
||||
|
||||
- WhatsApp: subscribe to inbound reaction events and surface them as system events for the agent (e.g., "user reacted 👍 to message X").
|
||||
- Rebrand: rename the npm package/CLI to `moltbot`, add a `moltbot` compatibility shim, and move extensions to the `@moltbot/*` scope.
|
||||
- Commands: group /help and /commands output with Telegram paging. (#2504) Thanks @hougangdev.
|
||||
- macOS: limit project-local `node_modules/.bin` PATH preference to debug builds (reduce PATH hijacking risk).
|
||||
@ -70,9 +73,11 @@ Status: beta.
|
||||
- Routing: add per-account DM session scope and document multi-account isolation. (#3095) Thanks @jarvis-sam.
|
||||
|
||||
### Breaking
|
||||
|
||||
- **BREAKING:** Gateway auth mode "none" is removed; gateway now requires token/password (Tailscale Serve identity still allowed).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Telegram: avoid silent empty replies by tracking normalization skips before fallback. (#3796)
|
||||
- Mentions: honor mentionPatterns even when explicit mentions are present. (#3303) Thanks @HirokiKobayashi-R.
|
||||
- Discord: restore username directory lookup in target resolution. (#3131) Thanks @bonald.
|
||||
@ -125,6 +130,7 @@ Status: beta.
|
||||
## 2026.1.24-3
|
||||
|
||||
### Fixes
|
||||
|
||||
- Slack: fix image downloads failing due to missing Authorization header on cross-origin redirects. (#1936) Thanks @sanderhelgesen.
|
||||
- Gateway: harden reverse proxy handling for local-client detection and unauthenticated proxied connects. (#1795) Thanks @orlyjamie.
|
||||
- Security audit: flag loopback Control UI with auth disabled as critical. (#1795) Thanks @orlyjamie.
|
||||
@ -133,16 +139,19 @@ Status: beta.
|
||||
## 2026.1.24-2
|
||||
|
||||
### Fixes
|
||||
|
||||
- Packaging: include dist/link-understanding output in npm tarball (fixes missing apply.js import on install).
|
||||
|
||||
## 2026.1.24-1
|
||||
|
||||
### Fixes
|
||||
|
||||
- Packaging: include dist/shared output in npm tarball (fixes missing reasoning-tags import on install).
|
||||
|
||||
## 2026.1.24
|
||||
|
||||
### Highlights
|
||||
|
||||
- Providers: Ollama discovery + docs; Venice guide upgrades + cross-links. (#1606) Thanks @abhaymundhara. https://docs.molt.bot/providers/ollama https://docs.molt.bot/providers/venice
|
||||
- Channels: LINE plugin (Messaging API) with rich replies + quick replies. (#1630) Thanks @plum-dawg.
|
||||
- TTS: Edge fallback (keyless) + `/tts` auto modes. (#1668, #1667) Thanks @steipete, @sebslight. https://docs.molt.bot/tts
|
||||
@ -150,6 +159,7 @@ Status: beta.
|
||||
- Telegram: DM topics as separate sessions + outbound link preview toggle. (#1597, #1700) Thanks @rohannagpal, @zerone0x. https://docs.molt.bot/channels/telegram
|
||||
|
||||
### Changes
|
||||
|
||||
- Channels: add LINE plugin (Messaging API) with rich replies, quick replies, and plugin HTTP registry. (#1630) Thanks @plum-dawg.
|
||||
- TTS: add Edge TTS provider fallback, defaulting to keyless Edge with MP3 retry on format failures. (#1668) Thanks @steipete. https://docs.molt.bot/tts
|
||||
- TTS: add auto mode enum (off/always/inbound/tagged) with per-session `/tts` override. (#1667) Thanks @sebslight. https://docs.molt.bot/tts
|
||||
@ -168,6 +178,7 @@ Status: beta.
|
||||
- Dev: add prek pre-commit hooks + dependabot config for weekly updates. (#1720) Thanks @dguido.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Web UI: fix config/debug layout overflow, scrolling, and code block sizing. (#1715) Thanks @saipreetham589.
|
||||
- Web UI: show Stop button during active runs, swap back to New session when idle. (#1664) Thanks @ndbroadbent.
|
||||
- Web UI: clear stale disconnect banners on reconnect; allow form saves with unsupported schema paths but block missing schema. (#1707) Thanks @Glucksberg.
|
||||
@ -211,11 +222,13 @@ Status: beta.
|
||||
## 2026.1.23-1
|
||||
|
||||
### Fixes
|
||||
|
||||
- Packaging: include dist/tts output in npm tarball (fixes missing dist/tts/tts.js).
|
||||
|
||||
## 2026.1.23
|
||||
|
||||
### Highlights
|
||||
|
||||
- TTS: move Telegram TTS into core + enable model-driven TTS tags by default for expressive audio replies. (#1559) Thanks @Glucksberg. https://docs.molt.bot/tts
|
||||
- Gateway: add `/tools/invoke` HTTP endpoint for direct tool calls (auth + tool policy enforced). (#1575) Thanks @vignesh07. https://docs.molt.bot/gateway/tools-invoke-http-api
|
||||
- Heartbeat: per-channel visibility controls (OK/alerts/indicator). (#1452) Thanks @dlauer. https://docs.molt.bot/gateway/heartbeat
|
||||
@ -223,6 +236,7 @@ Status: beta.
|
||||
- Channels: add Tlon/Urbit channel plugin (DMs, group mentions, thread replies). (#1544) Thanks @wca4a. https://docs.molt.bot/channels/tlon
|
||||
|
||||
### Changes
|
||||
|
||||
- Channels: allow per-group tool allow/deny policies across built-in + plugin channels. (#1546) Thanks @adam91holt. https://docs.molt.bot/multi-agent-sandbox-tools
|
||||
- Agents: add Bedrock auto-discovery defaults + config overrides. (#1553) Thanks @fal3. https://docs.molt.bot/bedrock
|
||||
- CLI: add `moltbot system` for system events + heartbeat controls; remove standalone `wake`. (commit 71203829d) https://docs.molt.bot/cli/system
|
||||
@ -237,6 +251,7 @@ Status: beta.
|
||||
- Docs: clarify HEARTBEAT.md empty file skips heartbeats, missing file still runs. (#1535) Thanks @JustYannicc. https://docs.molt.bot/gateway/heartbeat
|
||||
|
||||
### Fixes
|
||||
|
||||
- Sessions: accept non-UUID sessionIds for history/send/status while preserving agent scoping. (#1518)
|
||||
- Heartbeat: accept plugin channel ids for heartbeat target validation + UI hints.
|
||||
- Messaging/Sessions: mirror outbound sends into target session keys (threads + dmScope), create session entries on send, and normalize session key casing. (#1520, commit 4b6cdd1d3)
|
||||
@ -275,6 +290,7 @@ Status: beta.
|
||||
## 2026.1.22
|
||||
|
||||
### Changes
|
||||
|
||||
- Highlight: Compaction safeguard now uses adaptive chunking, progressive fallback, and UI status + retries. (#1466) Thanks @dlauer.
|
||||
- Providers: add Antigravity usage tracking to status output. (#1490) Thanks @patelhiren.
|
||||
- Slack: add chat-type reply threading overrides via `replyToModeByChatType`. (#1442) Thanks @stefangalescu.
|
||||
@ -282,6 +298,7 @@ Status: beta.
|
||||
- Onboarding: add hatch choice (TUI/Web/Later), token explainer, background dashboard seed on macOS, and showcase link.
|
||||
|
||||
### Fixes
|
||||
|
||||
- BlueBubbles: stop typing indicator on idle/no-reply. (#1439) Thanks @Nicell.
|
||||
- Message tool: keep path/filePath as-is for send; hydrate buffers only for sendAttachment. (#1444) Thanks @hopyky.
|
||||
- Auto-reply: only report a model switch when session state is available. (#1465) Thanks @robbyczgw-cla.
|
||||
@ -312,12 +329,14 @@ Status: beta.
|
||||
## 2026.1.21-2
|
||||
|
||||
### Fixes
|
||||
|
||||
- Control UI: ignore bootstrap identity placeholder text for avatar values and fall back to the default avatar. https://docs.molt.bot/cli/agents https://docs.molt.bot/web/control-ui
|
||||
- Slack: remove deprecated `filetype` field from `files.uploadV2` to eliminate API warnings. (#1447)
|
||||
|
||||
## 2026.1.21
|
||||
|
||||
### Changes
|
||||
|
||||
- Highlight: Lobster optional plugin tool for typed workflows + approval gates. https://docs.molt.bot/tools/lobster
|
||||
- Lobster: allow workflow file args via `argsJson` in the plugin tool. https://docs.molt.bot/tools/lobster
|
||||
- Heartbeat: allow running heartbeats in an explicit session key. (#1256) Thanks @zknicker.
|
||||
@ -340,10 +359,12 @@ Status: beta.
|
||||
- Docs: add per-message Gmail search example for gog. (#1220) Thanks @mbelinky.
|
||||
|
||||
### Breaking
|
||||
|
||||
- **BREAKING:** Control UI now rejects insecure HTTP without device identity by default. Use HTTPS (Tailscale Serve) or set `gateway.controlUi.allowInsecureAuth: true` to allow token-only auth. https://docs.molt.bot/web/control-ui#insecure-http
|
||||
- **BREAKING:** Envelope and system event timestamps now default to host-local time (was UTC) so agents don’t have to constantly convert.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Nodes/macOS: prompt on allowlist miss for node exec approvals, persist allowlist decisions, and flatten node invoke errors. (#1394) Thanks @ngutman.
|
||||
- Gateway: keep auto bind loopback-first and add explicit tailnet binding to avoid Tailscale taking over local UI. (#1380)
|
||||
- Memory: prevent CLI hangs by deferring vector probes, adding sqlite-vec/embedding timeouts, and showing sync progress early.
|
||||
@ -366,6 +387,7 @@ Status: beta.
|
||||
## 2026.1.20
|
||||
|
||||
### Changes
|
||||
|
||||
- Control UI: add copy-as-markdown with error feedback. (#1345) https://docs.molt.bot/web/control-ui
|
||||
- Control UI: drop the legacy list view. (#1345) https://docs.molt.bot/web/control-ui
|
||||
- TUI: add syntax highlighting for code blocks. (#1200) https://docs.molt.bot/tui
|
||||
@ -444,9 +466,11 @@ Status: beta.
|
||||
- Swabble: use the tagged Commander Swift package release.
|
||||
|
||||
### Breaking
|
||||
|
||||
- **BREAKING:** Reject invalid/unknown config entries and refuse to start the gateway for safety. Run `moltbot doctor --fix` to repair, then update plugins (`moltbot plugins update`) if you use any.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Discovery: shorten Bonjour DNS-SD service type to `_moltbot-gw._tcp` and update discovery clients/docs.
|
||||
- Diagnostics: export OTLP logs, correct queue depth tracking, and document message-flow telemetry.
|
||||
- Diagnostics: emit message-flow diagnostics across channels via shared dispatch. (#1244)
|
||||
@ -546,20 +570,23 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
## 2026.1.16-2
|
||||
|
||||
### Changes
|
||||
|
||||
- CLI: stamp build commit into dist metadata so banners show the commit in npm installs.
|
||||
- CLI: close memory manager after memory commands to avoid hanging processes. (#1127) — thanks @NicholasSpisak.
|
||||
|
||||
## 2026.1.16-1
|
||||
|
||||
### Highlights
|
||||
|
||||
- Hooks: add hooks system with bundled hooks, CLI tooling, and docs. (#1028) — thanks @ThomsenDrake. https://docs.molt.bot/hooks
|
||||
- Media: add inbound media understanding (image/audio/video) with provider + CLI fallbacks. https://docs.molt.bot/nodes/media-understanding
|
||||
- Plugins: add Zalo Personal plugin (`@moltbot/zalouser`) and unify channel directory for plugins. (#1032) — thanks @suminhthanh. https://docs.molt.bot/plugins/zalouser
|
||||
- Models: add Vercel AI Gateway auth choice + onboarding updates. (#1016) — thanks @timolins. https://docs.molt.bot/providers/vercel-ai-gateway
|
||||
- Sessions: add `session.identityLinks` for cross-platform DM session li nking. (#1033) — thanks @thewilloftheshadow. https://docs.molt.bot/concepts/session
|
||||
- Sessions: add `session.identityLinks` for cross-platform DM session li nking. (#1033) — thanks @thewilloftheshadow. https://docs.molt.bot/concepts/session
|
||||
- Web search: add `country`/`language` parameters (schema + Brave API) and docs. (#1046) — thanks @YuriNachos. https://docs.molt.bot/tools/web
|
||||
|
||||
### Breaking
|
||||
|
||||
- **BREAKING:** `moltbot message` and message tool now require `target` (dropping `to`/`channelId` for destinations). (#1034) — thanks @tobalsan.
|
||||
- **BREAKING:** Channel auth now prefers config over env for Discord/Telegram/Matrix (env is fallback only). (#1040) — thanks @thewilloftheshadow.
|
||||
- **BREAKING:** Drop legacy `chatType: "room"` support; use `chatType: "channel"`.
|
||||
@ -568,6 +595,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
- **BREAKING:** `moltbot plugins install <path>` now copies into `~/.clawdbot/extensions` (use `--link` to keep path-based loading).
|
||||
|
||||
### Changes
|
||||
|
||||
- Plugins: ship bundled plugins disabled by default and allow overrides by installed versions. (#1066) — thanks @ItzR3NO.
|
||||
- Plugins: add bundled Antigravity + Gemini CLI OAuth + Copilot Proxy provider plugins. (#1066) — thanks @ItzR3NO.
|
||||
- Tools: improve `web_fetch` extraction using Readability (with fallback).
|
||||
@ -603,6 +631,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
- Plugins: add zip installs and `--link` to avoid copying local paths.
|
||||
|
||||
### Fixes
|
||||
|
||||
- macOS: drain subprocess pipes before waiting to avoid deadlocks. (#1081) — thanks @thesash.
|
||||
- Verbose: wrap tool summaries/output in markdown only for markdown-capable channels.
|
||||
- Tools: include provider/session context in elevated exec denial errors.
|
||||
@ -659,17 +688,20 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
## 2026.1.15
|
||||
|
||||
### Highlights
|
||||
|
||||
- Plugins: add provider auth registry + `moltbot models auth login` for plugin-driven OAuth/API key flows.
|
||||
- Browser: improve remote CDP/Browserless support (auth passthrough, `wss` upgrade, timeouts, clearer errors).
|
||||
- Heartbeat: per-agent configuration + 24h duplicate suppression. (#980) — thanks @voidserf.
|
||||
- Security: audit warns on weak model tiers; app nodes store auth tokens encrypted (Keychain/SecurePrefs).
|
||||
|
||||
### Breaking
|
||||
|
||||
- **BREAKING:** iOS minimum version is now 18.0 to support Textual markdown rendering in native chat. (#702)
|
||||
- **BREAKING:** Microsoft Teams is now a plugin; install `@moltbot/msteams` via `moltbot plugins install @moltbot/msteams`.
|
||||
- **BREAKING:** Channel auth now prefers config over env for Discord/Telegram/Matrix (env is fallback only). (#1040) — thanks @thewilloftheshadow.
|
||||
|
||||
### Changes
|
||||
|
||||
- UI/Apps: move channel/config settings to schema-driven forms and rename Connections → Channels. (#1040) — thanks @thewilloftheshadow.
|
||||
- CLI: set process titles to `moltbot-<command>` for clearer process listings.
|
||||
- CLI/macOS: sync remote SSH target/identity to config and let `gateway status` auto-infer SSH targets (ssh-config aware).
|
||||
@ -709,6 +741,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
- Discord: allow emoji/sticker uploads + channel actions in config defaults. (#870) — thanks @JDIVE.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Messages: make `/stop` clear queued followups and pending session lane work for a hard abort.
|
||||
- Messages: make `/stop` abort active sub-agent runs spawned from the requester session and report how many were stopped.
|
||||
- WhatsApp: report linked status consistently in channel status. (#1050) — thanks @YuriNachos.
|
||||
@ -745,12 +778,14 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
## 2026.1.14-1
|
||||
|
||||
### Highlights
|
||||
|
||||
- Web search: `web_search`/`web_fetch` tools (Brave API) + first-time setup in onboarding/configure.
|
||||
- Browser control: Chrome extension relay takeover mode + remote browser control support.
|
||||
- Plugins: channel plugins (gateway HTTP hooks) + Zalo plugin + onboarding install flow. (#854) — thanks @longmaba.
|
||||
- Security: expanded `moltbot security audit` (+ `--fix`), detect-secrets CI scan, and a `SECURITY.md` reporting policy.
|
||||
|
||||
### Changes
|
||||
|
||||
- Docs: clarify per-agent auth stores, sandboxed skill binaries, and elevated semantics.
|
||||
- Docs: add FAQ entries for missing provider auth after adding agents and Gemini thinking signature errors.
|
||||
- Agents: add optional auth-profile copy prompt on `agents add` and improve auth error messaging.
|
||||
@ -767,6 +802,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
- Browser: add Chrome extension relay takeover mode (toolbar button), plus `moltbot browser extension install/path` and remote browser control (standalone server + token auth).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Sessions: refactor session store updates to lock + mutate per-entry, add chat.inject, and harden subagent cleanup flow. (#944) — thanks @tyler6204.
|
||||
- Browser: add tests for snapshot labels/efficient query params and labeled image responses.
|
||||
- Google: downgrade unsigned thinking blocks before send to avoid missing signature errors.
|
||||
@ -788,6 +824,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
## 2026.1.14
|
||||
|
||||
### Changes
|
||||
|
||||
- Usage: add MiniMax coding plan usage tracking.
|
||||
- Auth: label Claude Code CLI auth options. (#915) — thanks @SeanZoR.
|
||||
- Docs: standardize Claude Code CLI naming across docs and prompts. (follow-up to #915)
|
||||
@ -795,14 +832,16 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
- Config: add `channels.<provider>.configWrites` gating for channel-initiated config writes; migrate Slack channel IDs.
|
||||
|
||||
### Fixes
|
||||
- Mac: pass auth token/password to dashboard URL for authenticated access. (#918) — thanks @rahthakor.
|
||||
- UI: use application-defined WebSocket close code (browser compatibility). (#918) — thanks @rahthakor.
|
||||
|
||||
- Mac: pass auth token/password to dashboard URL for authenticated access. (#918) — thanks @rahthakor.
|
||||
- UI: use application-defined WebSocket close code (browser compatibility). (#918) — thanks @rahthakor.
|
||||
- TUI: render picker overlays via the overlay stack so /models and /settings display. (#921) — thanks @grizzdank.
|
||||
- TUI: add a bright spinner + elapsed time in the status line for send/stream/run states.
|
||||
- TUI: show LLM error messages (rate limits, auth, etc.) instead of `(no output)`.
|
||||
- Gateway/Dev: ensure `pnpm gateway:dev` always uses the dev profile config + state (`~/.clawdbot-dev`).
|
||||
|
||||
#### Agents / Auth / Tools / Sandbox
|
||||
|
||||
- Agents: make user time zone and 24-hour time explicit in the system prompt. (#859) — thanks @CashWilliams.
|
||||
- Agents: strip downgraded tool call text without eating adjacent replies and filter thinking-tag leaks. (#905) — thanks @erikpr1994.
|
||||
- Agents: cap tool call IDs for OpenAI/OpenRouter to avoid request rejections. (#875) — thanks @j1philli.
|
||||
@ -815,6 +854,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
- Google: downgrade unsigned thinking blocks before send to avoid missing signature errors.
|
||||
|
||||
#### macOS / Apps
|
||||
|
||||
- macOS: ensure launchd log directory exists with a test-only override. (#909) — thanks @roshanasingh4.
|
||||
- macOS: format ConnectionsStore config to satisfy SwiftFormat lint. (#852) — thanks @mneves75.
|
||||
- macOS: pass auth token/password to dashboard URL for authenticated access. (#918) — thanks @rahthakor.
|
||||
@ -834,12 +874,14 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
## 2026.1.13
|
||||
|
||||
### Fixes
|
||||
|
||||
- Postinstall: treat already-applied pnpm patches as no-ops to avoid npm/bun install failures.
|
||||
- Packaging: pin `@mariozechner/pi-ai` to 0.45.7 and refresh patched dependency to match npm resolution.
|
||||
|
||||
## 2026.1.12-2
|
||||
|
||||
### Fixes
|
||||
|
||||
- Packaging: include `dist/memory/**` in the npm tarball (fixes `ERR_MODULE_NOT_FOUND` for `dist/memory/index.js`).
|
||||
- Agents: persist sub-agent registry across gateway restarts and resume announce flow safely. (#831) — thanks @roshanasingh4.
|
||||
- Agents: strip invalid Gemini thought signatures from OpenRouter history to avoid 400s. (#841, #845) — thanks @MatthieuBizien.
|
||||
@ -847,11 +889,13 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
## 2026.1.12-1
|
||||
|
||||
### Fixes
|
||||
|
||||
- Packaging: include `dist/channels/**` in the npm tarball (fixes `ERR_MODULE_NOT_FOUND` for `dist/channels/registry.js`).
|
||||
|
||||
## 2026.1.12
|
||||
|
||||
### Highlights
|
||||
|
||||
- **BREAKING:** rename chat “providers” (Slack/Telegram/WhatsApp/…) to **channels** across CLI/RPC/config; legacy config keys auto-migrate on load (and are written back as `channels.*`).
|
||||
- Memory: add vector search for agent memories (Markdown-only) with SQLite index, chunking, lazy sync + file watch, and per-agent enablement/fallback.
|
||||
- Plugins: restore full voice-call plugin parity (Telnyx/Twilio, streaming, inbound policies, tools/CLI).
|
||||
@ -860,6 +904,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
- Agents: add compaction mode config with optional safeguard summarization and per-agent model fallbacks. (#700) — thanks @thewilloftheshadow; (#583) — thanks @mitschabaude-bot.
|
||||
|
||||
### New & Improved
|
||||
|
||||
- Memory: add custom OpenAI-compatible embedding endpoints; support OpenAI/local `node-llama-cpp` embeddings with per-agent overrides and provider metadata in tools/CLI. (#819) — thanks @mukhtharcm.
|
||||
- Memory: new `moltbot memory` CLI plus `memory_search`/`memory_get` tools with snippets + line ranges; index stored under `~/.clawdbot/memory/{agentId}.sqlite` with watch-on-by-default.
|
||||
- Agents: strengthen memory recall guidance; make workspace bootstrap truncation configurable (default 20k) with warnings; add default sub-agent model config.
|
||||
@ -873,9 +918,11 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
- Heartbeat: default `ackMaxChars` to 300 so short `HEARTBEAT_OK` replies stay internal.
|
||||
|
||||
### Installer
|
||||
|
||||
- Install: run `moltbot doctor --non-interactive` after git installs/updates and nudge daemon restarts when detected.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Doctor: warn on pnpm workspace mismatches, missing Control UI assets, and missing tsx binaries; offer UI rebuilds.
|
||||
- Tools: apply global tool allow/deny even when agent-specific tool policy is set.
|
||||
- Models/Providers: treat credential validation failures as auth errors to trigger fallback; normalize `${ENV_VAR}` apiKey values and auto-fill missing provider keys; preserve explicit GitHub Copilot provider config + agent-dir auth profiles. (#822) — thanks @sebslight; (#705) — thanks @TAGOOZ.
|
||||
@ -900,6 +947,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
- Connections UI: polish multi-account account cards. (#816) — thanks @steipete.
|
||||
|
||||
### Maintenance
|
||||
|
||||
- Dependencies: bump Pi packages to 0.45.3 and refresh patched pi-ai.
|
||||
- Testing: update Vitest + browser-playwright to 4.0.17.
|
||||
- Docs: add Amazon Bedrock provider notes and link from models/FAQ.
|
||||
@ -907,12 +955,14 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
## 2026.1.11
|
||||
|
||||
### Highlights
|
||||
|
||||
- Plugins are now first-class: loader + CLI management, plus the new Voice Call plugin.
|
||||
- Config: modular `$include` support for split config files. (#731) — thanks @pasogott.
|
||||
- Agents/Pi: reserve compaction headroom so pre-compaction memory writes can run before auto-compaction.
|
||||
- Agents: automatic pre-compaction memory flush turn to store durable memories before compaction.
|
||||
|
||||
### Changes
|
||||
|
||||
- CLI/Onboarding: simplify MiniMax auth choice to a single M2.1 option.
|
||||
- CLI: configure section selection now loops until Continue.
|
||||
- Docs: explain MiniMax vs MiniMax Lightning (speed vs cost) and restore LM Studio example.
|
||||
@ -948,6 +998,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
- macOS: remove the attach-only gateway setting; local mode now always manages launchd while still attaching to an existing gateway if present.
|
||||
|
||||
### Installer
|
||||
|
||||
- Postinstall: replace `git apply` with builtin JS patcher (works npm/pnpm/bun; no git dependency) plus regression tests.
|
||||
- Postinstall: skip pnpm patch fallback when the new patcher is active.
|
||||
- Installer tests: add root+non-root docker smokes, CI workflow to fetch molt.bot scripts and run install sh/cli with onboarding skipped.
|
||||
@ -956,6 +1007,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
- Installer UX: add `--install-method git|npm` and auto-detect source checkouts (prompt to update git checkout vs migrate to npm).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Models/Onboarding: configure MiniMax (minimax.io) via Anthropic-compatible `/anthropic` endpoint by default (keep `minimax-api` as a legacy alias).
|
||||
- Models: normalize Gemini 3 Pro/Flash IDs to preview names for live model lookups. (#769) — thanks @steipete.
|
||||
- CLI: fix guardCancel typing for configure prompts. (#769) — thanks @steipete.
|
||||
@ -995,12 +1047,14 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
## 2026.1.10
|
||||
|
||||
### Highlights
|
||||
|
||||
- CLI: `moltbot status` now table-based + shows OS/update/gateway/daemon/agents/sessions; `status --all` adds a full read-only debug report (tables, log tails, Tailscale summary, and scan progress via OSC-9 + spinner).
|
||||
- CLI Backends: add Codex CLI fallback with resume support (text output) and JSONL parsing for new runs, plus a live CLI resume probe.
|
||||
- CLI: add `moltbot update` (safe-ish git checkout update) + `--update` shorthand. (#673) — thanks @fm1randa.
|
||||
- Gateway: add OpenAI-compatible `/v1/chat/completions` HTTP endpoint (auth, SSE streaming, per-agent routing). (#680).
|
||||
|
||||
### Changes
|
||||
|
||||
- Onboarding/Models: add first-class Z.AI (GLM) auth choice (`zai-api-key`) + `--zai-api-key` flag.
|
||||
- CLI/Onboarding: add OpenRouter API key auth option in configure/onboard. (#703) — thanks @mteam88.
|
||||
- Agents: add human-delay pacing between block replies (modes: off/natural/custom, per-agent configurable). (#446) — thanks @tony-freedomology.
|
||||
@ -1014,6 +1068,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
- Docker: allow optional home volume + extra bind mounts in `docker-setup.sh`. (#679) — thanks @gabriel-trigo.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Auto-reply: suppress draft/typing streaming for `NO_REPLY` (silent system ops) so it doesn’t leak partial output.
|
||||
- CLI/Status: expand tables to full terminal width; clarify provider setup vs runtime warnings; richer per-provider detail; token previews in `status` while keeping `status --all` redacted; add troubleshooting link footer; keep log tails pasteable; show gateway auth used when reachable; surface provider runtime errors (Signal/iMessage/Slack); harden `tailscale status --json` parsing; make `status --all` scan progress determinate; and replace the footer with a 3-line “Next steps” recommendation (share/debug/probe).
|
||||
- CLI/Gateway: clarify that `moltbot gateway status` reports RPC health (connect + RPC) and shows RPC failures separately from connect failures.
|
||||
@ -1085,10 +1140,10 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
- Agents: repair session transcripts by dropping duplicate tool results across the whole history (unblocks Anthropic-compatible APIs after retries).
|
||||
- Tests/Live: reset the gateway session between model runs to avoid cross-provider transcript incompatibilities (notably OpenAI Responses reasoning replay rules).
|
||||
|
||||
|
||||
## 2026.1.9
|
||||
|
||||
### Highlights
|
||||
|
||||
- Microsoft Teams provider: polling, attachments, outbound CLI send, per-channel policy.
|
||||
- Models/Auth expansion: OpenCode Zen + MiniMax API onboarding; token auth profiles + auth order; OAuth health in doctor/status.
|
||||
- CLI/Gateway UX: message subcommands, gateway discover/status/SSH, /config + /debug, sandbox CLI.
|
||||
@ -1097,10 +1152,12 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
- Control UI/TUI: queued messages, session links, reasoning view, mobile polish, logs UX.
|
||||
|
||||
### Breaking
|
||||
|
||||
- CLI: `moltbot message` now subcommands (`message send|poll|...`) and requires `--provider` unless only one provider configured.
|
||||
- Commands/Tools: `/restart` and gateway restart tool disabled by default; enable with `commands.restart=true`.
|
||||
|
||||
### New Features and Changes
|
||||
|
||||
- Models/Auth: OpenCode Zen onboarding (#623) — thanks @magimetal; MiniMax Anthropic-compatible API + hosted onboarding (#590, #495) — thanks @mneves75, @tobiasbischoff.
|
||||
- Models/Auth: setup-token + token auth profiles; `moltbot models auth order {get,set,clear}`; per-agent auth candidates in `/model status`; OAuth expiry checks in doctor/status.
|
||||
- Agent/System: claude-cli runner; `session_status` tool (and sandbox allow); adaptive context pruning default; system prompt messaging guidance + no auto self-update; eligible skills list injection; sub-agent context trimmed.
|
||||
@ -1122,6 +1179,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
- Apps/Branding: refreshed iOS/Android/macOS icons (#521) — thanks @fishfisher.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Packaging: include MS Teams send module in npm tarball.
|
||||
- Sandbox/Browser: auto-start CDP endpoint; proxy CDP out of container for attachOnly; relax Bun fetch typing; align sandbox list output with config images.
|
||||
- Agents/Runtime: gate heartbeat prompt to default sessions; /stop aborts between tool calls; require explicit system-event session keys; guard small context windows; fix model fallback stringification; sessions_spawn inherits provider; failover on billing/credits; respect auth cooldown ordering; restore Anthropic OAuth tool dispatch + tool-name bypass; avoid OpenAI invalid reasoning replay; harden Gmail hook model defaults.
|
||||
@ -1139,7 +1197,8 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
- Onboarding/Configure: QuickStart single-select provider picker; avoid Codex CLI false-expiry warnings; clarify WhatsApp owner prompt; fix Minimax hosted onboarding (agents.defaults + msteams heartbeat target); remove configure Control UI prompt; honor gateway --dev flag.
|
||||
|
||||
### Maintenance
|
||||
- Dependencies: bump pi-* stack to 0.42.2.
|
||||
|
||||
- Dependencies: bump pi-\* stack to 0.42.2.
|
||||
- Dependencies: Pi 0.40.0 bump (#543) — thanks @mcinteerj.
|
||||
- Build: Docker build cache layer (#605) — thanks @zknicker.
|
||||
|
||||
@ -1148,6 +1207,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
## 2026.1.8
|
||||
|
||||
### Highlights
|
||||
|
||||
- Security: DMs locked down by default across providers; pairing-first + allowlist guidance.
|
||||
- Sandbox: per-agent scope defaults + workspace access controls; tool/session isolation tuned.
|
||||
- Agent loop: compaction, pruning, streaming, and error handling hardened.
|
||||
@ -1156,6 +1216,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
- CLI/Gateway/Doctor: daemon/logs/status, auth migration, and diagnostics significantly expanded.
|
||||
|
||||
### Breaking
|
||||
|
||||
- **SECURITY (update ASAP):** inbound DMs are now **locked down by default** on Telegram/WhatsApp/Signal/iMessage/Discord/Slack.
|
||||
- Previously, if you didn’t configure an allowlist, your bot could be **open to anyone** (especially discoverable Telegram bots).
|
||||
- New default: DM pairing (`dmPolicy="pairing"` / `discord.dm.policy="pairing"` / `slack.dm.policy="pairing"`).
|
||||
@ -1170,6 +1231,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
- CLI: remove `update`, `gateway-daemon`, `gateway {install|uninstall|start|stop|restart|daemon status|wake|send|agent}`, and `telegram` commands; move `login/logout` to `providers login/logout` (top-level aliases hidden); use `daemon` for service control, `send`/`agent`/`wake` for RPC, and `nodes canvas` for canvas ops.
|
||||
|
||||
### Fixes
|
||||
|
||||
- **CLI/Gateway/Doctor:** daemon runtime selection + improved logs/status/health/errors; auth/password handling for local CLI; richer close/timeout details; auto-migrate legacy config/sessions/state; integrity checks + repair prompts; `--yes`/`--non-interactive`; `--deep` gateway scans; better restart/service hints.
|
||||
- **Agent loop + compaction:** compaction/pruning tuning, overflow handling, safer bootstrap context, and per-provider threading/confirmations; opt-in tool-result pruning + compact tracking.
|
||||
- **Sandbox + tools:** per-agent sandbox overrides, workspaceAccess controls, session tool visibility, tool policy overrides, process isolation, and tool schema/timeout/reaction unification.
|
||||
@ -1181,13 +1243,15 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
- **Docs:** new FAQ/ClawdHub/config examples/showcase entries and clarified auth, sandbox, and systemd docs.
|
||||
|
||||
### Maintenance
|
||||
|
||||
- Skills additions (Himalaya email, CodexBar, 1Password).
|
||||
- Dependency refreshes (pi-* stack, Slack SDK, discord-api-types, file-type, zod, Biome, Vite).
|
||||
- Dependency refreshes (pi-\* stack, Slack SDK, discord-api-types, file-type, zod, Biome, Vite).
|
||||
- Refactors: centralized group allowlist/mention policy; lint/import cleanup; switch tsx → bun for TS execution.
|
||||
|
||||
## 2026.1.5
|
||||
|
||||
### Highlights
|
||||
|
||||
- Models: add image-specific model config (`agent.imageModel` + fallbacks) and scan support.
|
||||
- Agent tools: new `image` tool routed to the image model (when configured).
|
||||
- Config: default model shorthands (`opus`, `sonnet`, `gpt`, `gpt-mini`, `gemini`, `gemini-flash`).
|
||||
@ -1195,6 +1259,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
|
||||
- Bun: optional local install/build workflow without maintaining a Bun lockfile (see `docs/bun.md`).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Control UI: render Markdown in tool result cards.
|
||||
- Control UI: prevent overlapping action buttons in Discord guild rules on narrow layouts.
|
||||
- Android: tapping the foreground service notification brings the app to the front. (#179) — thanks @Syhids
|
||||
|
||||
@ -3,45 +3,51 @@ summary: "WhatsApp (web channel) integration: login, inbox, replies, media, and
|
||||
read_when:
|
||||
- Working on WhatsApp/web channel behavior or inbox routing
|
||||
---
|
||||
# WhatsApp (web channel)
|
||||
|
||||
# WhatsApp (web channel)
|
||||
|
||||
Status: WhatsApp Web via Baileys only. Gateway owns the session(s).
|
||||
|
||||
## Quick setup (beginner)
|
||||
1) Use a **separate phone number** if possible (recommended).
|
||||
2) Configure WhatsApp in `~/.clawdbot/moltbot.json`.
|
||||
3) Run `moltbot channels login` to scan the QR code (Linked Devices).
|
||||
4) Start the gateway.
|
||||
|
||||
1. Use a **separate phone number** if possible (recommended).
|
||||
2. Configure WhatsApp in `~/.clawdbot/moltbot.json`.
|
||||
3. Run `moltbot channels login` to scan the QR code (Linked Devices).
|
||||
4. Start the gateway.
|
||||
|
||||
Minimal config:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
whatsapp: {
|
||||
dmPolicy: "allowlist",
|
||||
allowFrom: ["+15551234567"]
|
||||
}
|
||||
}
|
||||
allowFrom: ["+15551234567"],
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Goals
|
||||
|
||||
- Multiple WhatsApp accounts (multi-account) in one Gateway process.
|
||||
- Deterministic routing: replies return to WhatsApp, no model routing.
|
||||
- Model sees enough context to understand quoted replies.
|
||||
|
||||
## Config writes
|
||||
|
||||
By default, WhatsApp is allowed to write config updates triggered by `/config set|unset` (requires `commands.config: true`).
|
||||
|
||||
Disable with:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: { whatsapp: { configWrites: false } }
|
||||
channels: { whatsapp: { configWrites: false } },
|
||||
}
|
||||
```
|
||||
|
||||
## Architecture (who owns what)
|
||||
|
||||
- **Gateway** owns the Baileys socket and inbox loop.
|
||||
- **CLI / macOS app** talk to the gateway; no direct Baileys use.
|
||||
- **Active listener** is required for outbound sends; otherwise send fails fast.
|
||||
@ -51,19 +57,21 @@ Disable with:
|
||||
WhatsApp requires a real mobile number for verification. VoIP and virtual numbers are usually blocked. There are two supported ways to run Moltbot on WhatsApp:
|
||||
|
||||
### Dedicated number (recommended)
|
||||
|
||||
Use a **separate phone number** for Moltbot. Best UX, clean routing, no self-chat quirks. Ideal setup: **spare/old Android phone + eSIM**. Leave it on Wi‑Fi and power, and link it via QR.
|
||||
|
||||
**WhatsApp Business:** You can use WhatsApp Business on the same device with a different number. Great for keeping your personal WhatsApp separate — install WhatsApp Business and register the Moltbot number there.
|
||||
|
||||
**Sample config (dedicated number, single-user allowlist):**
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
whatsapp: {
|
||||
dmPolicy: "allowlist",
|
||||
allowFrom: ["+15551234567"]
|
||||
}
|
||||
}
|
||||
allowFrom: ["+15551234567"],
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
@ -72,10 +80,12 @@ If you want pairing instead of allowlist, set `channels.whatsapp.dmPolicy` to `p
|
||||
`moltbot pairing approve whatsapp <code>`
|
||||
|
||||
### Personal number (fallback)
|
||||
|
||||
Quick fallback: run Moltbot on **your own number**. Message yourself (WhatsApp “Message yourself”) for testing so you don’t spam contacts. Expect to read verification codes on your main phone during setup and experiments. **Must enable self-chat mode.**
|
||||
When the wizard asks for your personal WhatsApp number, enter the phone you will message from (the owner/sender), not the assistant number.
|
||||
|
||||
**Sample config (personal number, self-chat):**
|
||||
|
||||
```json
|
||||
{
|
||||
"whatsapp": {
|
||||
@ -91,6 +101,7 @@ if `messages.responsePrefix` is unset. Set it explicitly to customize or disable
|
||||
the prefix (use `""` to remove it).
|
||||
|
||||
### Number sourcing tips
|
||||
|
||||
- **Local eSIM** from your country's mobile carrier (most reliable)
|
||||
- Austria: [hot.at](https://www.hot.at)
|
||||
- UK: [giffgaff](https://www.giffgaff.com) — free SIM, no contract
|
||||
@ -101,6 +112,7 @@ the prefix (use `""` to remove it).
|
||||
**Tip:** The number only needs to receive one verification SMS. After that, WhatsApp Web sessions persist via `creds.json`.
|
||||
|
||||
## Why Not Twilio?
|
||||
|
||||
- Early Moltbot builds supported Twilio’s WhatsApp Business integration.
|
||||
- WhatsApp Business numbers are a poor fit for a personal assistant.
|
||||
- Meta enforces a 24‑hour reply window; if you haven’t responded in the last 24 hours, the business number can’t initiate new messages.
|
||||
@ -108,6 +120,7 @@ the prefix (use `""` to remove it).
|
||||
- Result: unreliable delivery and frequent blocks, so support was removed.
|
||||
|
||||
## Login + credentials
|
||||
|
||||
- Login command: `moltbot channels login` (QR via Linked Devices).
|
||||
- Multi-account login: `moltbot channels login --account <id>` (`<id>` = `accountId`).
|
||||
- Default account (when `--account` is omitted): `default` if present, otherwise the first configured account id (sorted).
|
||||
@ -118,6 +131,7 @@ the prefix (use `""` to remove it).
|
||||
- Logged-out socket => error instructs re-link.
|
||||
|
||||
## Inbound flow (DM + group)
|
||||
|
||||
- WhatsApp events come from `messages.upsert` (Baileys).
|
||||
- Inbox listeners are detached on shutdown to avoid accumulating event handlers in tests/restarts.
|
||||
- Status/broadcast chats are ignored.
|
||||
@ -128,38 +142,44 @@ the prefix (use `""` to remove it).
|
||||
- Your linked WhatsApp number is implicitly trusted, so self messages skip `channels.whatsapp.dmPolicy` and `channels.whatsapp.allowFrom` checks.
|
||||
|
||||
### Personal-number mode (fallback)
|
||||
|
||||
If you run Moltbot on your **personal WhatsApp number**, enable `channels.whatsapp.selfChatMode` (see sample above).
|
||||
|
||||
Behavior:
|
||||
|
||||
- Outbound DMs never trigger pairing replies (prevents spamming contacts).
|
||||
- Inbound unknown senders still follow `channels.whatsapp.dmPolicy`.
|
||||
- Self-chat mode (allowFrom includes your number) avoids auto read receipts and ignores mention JIDs.
|
||||
- Read receipts sent for non-self-chat DMs.
|
||||
|
||||
## Read receipts
|
||||
|
||||
By default, the gateway marks inbound WhatsApp messages as read (blue ticks) once they are accepted.
|
||||
|
||||
Disable globally:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: { whatsapp: { sendReadReceipts: false } }
|
||||
channels: { whatsapp: { sendReadReceipts: false } },
|
||||
}
|
||||
```
|
||||
|
||||
Disable per account:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
whatsapp: {
|
||||
accounts: {
|
||||
personal: { sendReadReceipts: false }
|
||||
}
|
||||
}
|
||||
}
|
||||
personal: { sendReadReceipts: false },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- Self-chat mode always skips read receipts.
|
||||
|
||||
## WhatsApp FAQ: sending messages + pairing
|
||||
@ -169,6 +189,7 @@ No. Default DM policy is **pairing**, so unknown senders only get a pairing code
|
||||
|
||||
**How does pairing work on WhatsApp?**
|
||||
Pairing is a DM gate for unknown senders:
|
||||
|
||||
- First DM from a new sender returns a short code (message is not processed).
|
||||
- Approve with: `moltbot pairing approve whatsapp <code>` (list with `moltbot pairing list whatsapp`).
|
||||
- Codes expire after 1 hour; pending requests are capped at 3 per channel.
|
||||
@ -180,6 +201,7 @@ Yes, by routing each sender to a different agent via `bindings` (peer `kind: "dm
|
||||
The wizard uses it to set your **allowlist/owner** so your own DMs are permitted. It’s not used for auto-sending. If you run on your personal WhatsApp number, use that same number and enable `channels.whatsapp.selfChatMode`.
|
||||
|
||||
## Message normalization (what the model sees)
|
||||
|
||||
- `Body` is the current message body with envelope.
|
||||
- Quoted reply context is **always appended**:
|
||||
```
|
||||
@ -195,6 +217,7 @@ The wizard uses it to set your **allowlist/owner** so your own DMs are permitted
|
||||
- `<media:image|video|audio|document|sticker>`
|
||||
|
||||
## Groups
|
||||
|
||||
- Groups map to `agent:<agentId>:whatsapp:group:<jid>` sessions.
|
||||
- Group policy: `channels.whatsapp.groupPolicy = open|disabled|allowlist` (default `allowlist`).
|
||||
- Activation modes:
|
||||
@ -203,7 +226,7 @@ The wizard uses it to set your **allowlist/owner** so your own DMs are permitted
|
||||
- `/activation mention|always` is owner-only and must be sent as a standalone message.
|
||||
- Owner = `channels.whatsapp.allowFrom` (or self E.164 if unset).
|
||||
- **History injection** (pending-only):
|
||||
- Recent *unprocessed* messages (default 50) inserted under:
|
||||
- Recent _unprocessed_ messages (default 50) inserted under:
|
||||
`[Chat messages since your last reply - for context]` (messages already in the session are not re-injected)
|
||||
- Current message under:
|
||||
`[Current message - respond to this]`
|
||||
@ -211,6 +234,7 @@ The wizard uses it to set your **allowlist/owner** so your own DMs are permitted
|
||||
- Group metadata cached 5 min (subject + participants).
|
||||
|
||||
## Reply delivery (threading)
|
||||
|
||||
- WhatsApp Web sends standard messages (no quoted reply threading in the current gateway).
|
||||
- Reply tags are ignored on this channel.
|
||||
|
||||
@ -219,6 +243,7 @@ The wizard uses it to set your **allowlist/owner** so your own DMs are permitted
|
||||
WhatsApp can automatically send emoji reactions to incoming messages immediately upon receipt, before the bot generates a reply. This provides instant feedback to users that their message was received.
|
||||
|
||||
**Configuration:**
|
||||
|
||||
```json
|
||||
{
|
||||
"whatsapp": {
|
||||
@ -232,6 +257,7 @@ WhatsApp can automatically send emoji reactions to incoming messages immediately
|
||||
```
|
||||
|
||||
**Options:**
|
||||
|
||||
- `emoji` (string): Emoji to use for acknowledgment (e.g., "👀", "✅", "📨"). Empty or omitted = feature disabled.
|
||||
- `direct` (boolean, default: `true`): Send reactions in direct/DM chats.
|
||||
- `group` (string, default: `"mentions"`): Group chat behavior:
|
||||
@ -240,6 +266,7 @@ WhatsApp can automatically send emoji reactions to incoming messages immediately
|
||||
- `"never"`: Never react in groups
|
||||
|
||||
**Per-account override:**
|
||||
|
||||
```json
|
||||
{
|
||||
"whatsapp": {
|
||||
@ -257,6 +284,7 @@ WhatsApp can automatically send emoji reactions to incoming messages immediately
|
||||
```
|
||||
|
||||
**Behavior notes:**
|
||||
|
||||
- Reactions are sent **immediately** upon message receipt, before typing indicators or bot replies.
|
||||
- In groups with `requireMention: false` (activation: always), `group: "mentions"` will react to all messages (not just @mentions).
|
||||
- Fire-and-forget: reaction failures are logged but don't prevent the bot from replying.
|
||||
@ -264,18 +292,35 @@ WhatsApp can automatically send emoji reactions to incoming messages immediately
|
||||
- WhatsApp ignores `messages.ackReaction`; use `channels.whatsapp.ackReaction` instead.
|
||||
|
||||
## Agent tool (reactions)
|
||||
|
||||
- Tool: `whatsapp` with `react` action (`chatJid`, `messageId`, `emoji`, optional `remove`).
|
||||
- Optional: `participant` (group sender), `fromMe` (reacting to your own message), `accountId` (multi-account).
|
||||
- Reaction removal semantics: see [/tools/reactions](/tools/reactions).
|
||||
- Tool gating: `channels.whatsapp.actions.reactions` (default: enabled).
|
||||
|
||||
## Inbound reaction notifications
|
||||
|
||||
When someone reacts to a message in a WhatsApp chat, the gateway emits a system event so the agent sees it in its next prompt. System events appear as lines like:
|
||||
|
||||
```
|
||||
WhatsApp reaction added: 👍 by +1234567890 msg BAE5ABC123
|
||||
```
|
||||
|
||||
- Reaction removals (empty emoji) are silently skipped.
|
||||
- Self-reactions in DMs are correctly attributed to the bot's own JID.
|
||||
- Group reactions include the participant who reacted.
|
||||
- Events are deduplicated by message ID, sender, and emoji to avoid repeat notifications.
|
||||
- No configuration required; inbound reactions are always surfaced when the gateway is running.
|
||||
|
||||
## Limits
|
||||
|
||||
- Outbound text is chunked to `channels.whatsapp.textChunkLimit` (default 4000).
|
||||
- Optional newline chunking: set `channels.whatsapp.chunkMode="newline"` to split on blank lines (paragraph boundaries) before length chunking.
|
||||
- Inbound media saves are capped by `channels.whatsapp.mediaMaxMb` (default 50 MB).
|
||||
- Outbound media items are capped by `agents.defaults.mediaMaxMb` (default 5 MB).
|
||||
|
||||
## Outbound send (text + media)
|
||||
|
||||
- Uses active web listener; error if gateway not running.
|
||||
- Text chunking: 4k max per message (configurable via `channels.whatsapp.textChunkLimit`, optional `channels.whatsapp.chunkMode`).
|
||||
- Media:
|
||||
@ -288,17 +333,21 @@ WhatsApp can automatically send emoji reactions to incoming messages immediately
|
||||
- Gateway: `send` params include `gifPlayback: true`
|
||||
|
||||
## Voice notes (PTT audio)
|
||||
|
||||
WhatsApp sends audio as **voice notes** (PTT bubble).
|
||||
|
||||
- Best results: OGG/Opus. Moltbot rewrites `audio/ogg` to `audio/ogg; codecs=opus`.
|
||||
- `[[audio_as_voice]]` is ignored for WhatsApp (audio already ships as voice note).
|
||||
|
||||
## Media limits + optimization
|
||||
|
||||
- Default outbound cap: 5 MB (per media item).
|
||||
- Override: `agents.defaults.mediaMaxMb`.
|
||||
- Images are auto-optimized to JPEG under cap (resize + quality sweep).
|
||||
- Oversize media => error; media reply falls back to text warning.
|
||||
|
||||
## Heartbeats
|
||||
|
||||
- **Gateway heartbeat** logs connection health (`web.heartbeatSeconds`, default 60s).
|
||||
- **Agent heartbeat** can be configured per agent (`agents.list[].heartbeat`) or globally
|
||||
via `agents.defaults.heartbeat` (fallback when no per-agent entries are set).
|
||||
@ -306,12 +355,14 @@ WhatsApp sends audio as **voice notes** (PTT bubble).
|
||||
- Delivery defaults to the last used channel (or configured target).
|
||||
|
||||
## Reconnect behavior
|
||||
|
||||
- Backoff policy: `web.reconnect`:
|
||||
- `initialMs`, `maxMs`, `factor`, `jitter`, `maxAttempts`.
|
||||
- If maxAttempts reached, web monitoring stops (degraded).
|
||||
- Logged-out => stop and require re-link.
|
||||
|
||||
## Config quick map
|
||||
|
||||
- `channels.whatsapp.dmPolicy` (DM policy: pairing/allowlist/open/disabled).
|
||||
- `channels.whatsapp.selfChatMode` (same-phone setup; bot uses your personal WhatsApp number).
|
||||
- `channels.whatsapp.allowFrom` (DM allowlist). WhatsApp uses E.164 phone numbers (no usernames).
|
||||
@ -343,6 +394,7 @@ WhatsApp sends audio as **voice notes** (PTT bubble).
|
||||
- `web.reconnect.*`
|
||||
|
||||
## Logs + troubleshooting
|
||||
|
||||
- Subsystems: `whatsapp/inbound`, `whatsapp/outbound`, `web-heartbeat`, `web-reconnect`.
|
||||
- Log file: `/tmp/moltbot/moltbot-YYYY-MM-DD.log` (configurable).
|
||||
- Troubleshooting guide: [Gateway troubleshooting](/gateway/troubleshooting).
|
||||
@ -350,13 +402,16 @@ WhatsApp sends audio as **voice notes** (PTT bubble).
|
||||
## Troubleshooting (quick)
|
||||
|
||||
**Not linked / QR login required**
|
||||
|
||||
- Symptom: `channels status` shows `linked: false` or warns “Not linked”.
|
||||
- Fix: run `moltbot channels login` on the gateway host and scan the QR (WhatsApp → Settings → Linked Devices).
|
||||
|
||||
**Linked but disconnected / reconnect loop**
|
||||
|
||||
- Symptom: `channels status` shows `running, disconnected` or warns “Linked but disconnected”.
|
||||
- Fix: `moltbot doctor` (or restart the gateway). If it persists, relink via `channels login` and inspect `moltbot logs --follow`.
|
||||
|
||||
**Bun runtime**
|
||||
|
||||
- Bun is **not recommended**. WhatsApp (Baileys) and Telegram are unreliable on Bun.
|
||||
Run the gateway with **Node**. (See Getting Started runtime note.)
|
||||
|
||||
@ -3,6 +3,7 @@ summary: "Reaction semantics shared across channels"
|
||||
read_when:
|
||||
- Working on reactions in any channel
|
||||
---
|
||||
|
||||
# Reaction tooling
|
||||
|
||||
Shared reaction semantics across channels:
|
||||
@ -18,3 +19,4 @@ Channel notes:
|
||||
- **Telegram**: empty `emoji` removes the bot's reactions; `remove: true` also removes reactions but still requires a non-empty `emoji` for tool validation.
|
||||
- **WhatsApp**: empty `emoji` removes the bot reaction; `remove: true` maps to empty emoji (still requires `emoji`).
|
||||
- **Signal**: inbound reaction notifications emit system events when `channels.signal.reactionNotifications` is enabled.
|
||||
- **WhatsApp**: inbound reaction notifications are always surfaced as system events (no configuration required).
|
||||
|
||||
@ -0,0 +1,187 @@
|
||||
import "./test-helpers.js";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("../agents/pi-embedded.js", () => ({
|
||||
abortEmbeddedPiRun: vi.fn().mockReturnValue(false),
|
||||
isEmbeddedPiRunActive: vi.fn().mockReturnValue(false),
|
||||
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
|
||||
runEmbeddedPiAgent: vi.fn(),
|
||||
queueEmbeddedPiMessage: vi.fn().mockReturnValue(false),
|
||||
resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`,
|
||||
}));
|
||||
|
||||
import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js";
|
||||
import {
|
||||
drainSystemEvents,
|
||||
peekSystemEvents,
|
||||
resetSystemEventsForTest,
|
||||
} from "../infra/system-events.js";
|
||||
import { resetLogger, setLoggerOverride } from "../logging.js";
|
||||
import { resolveAgentRoute } from "../routing/resolve-route.js";
|
||||
import { monitorWebChannel } from "./auto-reply.js";
|
||||
import type { WebInboundReaction } from "./inbound.js";
|
||||
import { resetBaileysMocks, resetLoadConfigMock, setLoadConfigMock } from "./test-helpers.js";
|
||||
|
||||
let previousHome: string | undefined;
|
||||
let tempHome: string | undefined;
|
||||
|
||||
beforeEach(async () => {
|
||||
resetInboundDedupe();
|
||||
resetSystemEventsForTest();
|
||||
previousHome = process.env.HOME;
|
||||
tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-web-home-"));
|
||||
process.env.HOME = tempHome;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
process.env.HOME = previousHome;
|
||||
if (tempHome) {
|
||||
await fs.rm(tempHome, { recursive: true, force: true }).catch(() => {});
|
||||
tempHome = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
describe("web auto-reply – inbound reaction system events", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
resetBaileysMocks();
|
||||
resetLoadConfigMock();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
resetLogger();
|
||||
setLoggerOverride(null);
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("enqueues a system event when a reaction is received", async () => {
|
||||
setLoadConfigMock(() => ({
|
||||
channels: { whatsapp: { allowFrom: ["*"] } },
|
||||
messages: {},
|
||||
}));
|
||||
|
||||
let capturedOnReaction: ((reaction: WebInboundReaction) => void) | undefined;
|
||||
const listenerFactory = async (opts: {
|
||||
onMessage: (...args: unknown[]) => Promise<void>;
|
||||
onReaction?: (reaction: WebInboundReaction) => void;
|
||||
}) => {
|
||||
capturedOnReaction = opts.onReaction;
|
||||
return { close: vi.fn() };
|
||||
};
|
||||
|
||||
await monitorWebChannel(false, listenerFactory, false);
|
||||
expect(capturedOnReaction).toBeDefined();
|
||||
|
||||
const cfg = { channels: { whatsapp: { allowFrom: ["*"] } }, messages: {} };
|
||||
const route = resolveAgentRoute({
|
||||
cfg: cfg as Parameters<typeof resolveAgentRoute>[0]["cfg"],
|
||||
channel: "whatsapp",
|
||||
accountId: "default",
|
||||
peer: { kind: "dm", id: "999@s.whatsapp.net" },
|
||||
});
|
||||
|
||||
// Drain the "WhatsApp gateway connected" event so we only check reaction events
|
||||
drainSystemEvents(route.sessionKey);
|
||||
|
||||
capturedOnReaction!({
|
||||
messageId: "msg-abc",
|
||||
emoji: "👍",
|
||||
chatJid: "999@s.whatsapp.net",
|
||||
chatType: "direct",
|
||||
accountId: "default",
|
||||
senderJid: "999@s.whatsapp.net",
|
||||
senderE164: "+999",
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
const events = peekSystemEvents(route.sessionKey);
|
||||
expect(events).toHaveLength(1);
|
||||
expect(events[0]).toBe("WhatsApp reaction added: 👍 by +999 msg msg-abc");
|
||||
});
|
||||
|
||||
it("uses senderJid when senderE164 is unavailable", async () => {
|
||||
setLoadConfigMock(() => ({
|
||||
channels: { whatsapp: { allowFrom: ["*"] } },
|
||||
messages: {},
|
||||
}));
|
||||
|
||||
let capturedOnReaction: ((reaction: WebInboundReaction) => void) | undefined;
|
||||
const listenerFactory = async (opts: {
|
||||
onMessage: (...args: unknown[]) => Promise<void>;
|
||||
onReaction?: (reaction: WebInboundReaction) => void;
|
||||
}) => {
|
||||
capturedOnReaction = opts.onReaction;
|
||||
return { close: vi.fn() };
|
||||
};
|
||||
|
||||
await monitorWebChannel(false, listenerFactory, false);
|
||||
|
||||
const cfg = { channels: { whatsapp: { allowFrom: ["*"] } }, messages: {} };
|
||||
const route = resolveAgentRoute({
|
||||
cfg: cfg as Parameters<typeof resolveAgentRoute>[0]["cfg"],
|
||||
channel: "whatsapp",
|
||||
accountId: "default",
|
||||
peer: { kind: "dm", id: "999@s.whatsapp.net" },
|
||||
});
|
||||
|
||||
drainSystemEvents(route.sessionKey);
|
||||
|
||||
capturedOnReaction!({
|
||||
messageId: "msg-xyz",
|
||||
emoji: "❤️",
|
||||
chatJid: "999@s.whatsapp.net",
|
||||
chatType: "direct",
|
||||
accountId: "default",
|
||||
senderJid: "999@s.whatsapp.net",
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
const events = peekSystemEvents(route.sessionKey);
|
||||
expect(events).toHaveLength(1);
|
||||
expect(events[0]).toBe("WhatsApp reaction added: ❤️ by 999@s.whatsapp.net msg msg-xyz");
|
||||
});
|
||||
|
||||
it("falls back to 'someone' when no sender info available", async () => {
|
||||
setLoadConfigMock(() => ({
|
||||
channels: { whatsapp: { allowFrom: ["*"] } },
|
||||
messages: {},
|
||||
}));
|
||||
|
||||
let capturedOnReaction: ((reaction: WebInboundReaction) => void) | undefined;
|
||||
const listenerFactory = async (opts: {
|
||||
onMessage: (...args: unknown[]) => Promise<void>;
|
||||
onReaction?: (reaction: WebInboundReaction) => void;
|
||||
}) => {
|
||||
capturedOnReaction = opts.onReaction;
|
||||
return { close: vi.fn() };
|
||||
};
|
||||
|
||||
await monitorWebChannel(false, listenerFactory, false);
|
||||
|
||||
const cfg = { channels: { whatsapp: { allowFrom: ["*"] } }, messages: {} };
|
||||
const route = resolveAgentRoute({
|
||||
cfg: cfg as Parameters<typeof resolveAgentRoute>[0]["cfg"],
|
||||
channel: "whatsapp",
|
||||
accountId: "default",
|
||||
peer: { kind: "dm", id: "999@s.whatsapp.net" },
|
||||
});
|
||||
|
||||
drainSystemEvents(route.sessionKey);
|
||||
|
||||
capturedOnReaction!({
|
||||
messageId: "msg-noid",
|
||||
emoji: "🔥",
|
||||
chatJid: "999@s.whatsapp.net",
|
||||
chatType: "direct",
|
||||
accountId: "default",
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
const events = peekSystemEvents(route.sessionKey);
|
||||
expect(events).toHaveLength(1);
|
||||
expect(events[0]).toBe("WhatsApp reaction added: 🔥 by someone msg msg-noid");
|
||||
});
|
||||
});
|
||||
@ -28,6 +28,7 @@ import { whatsappHeartbeatLog, whatsappLog } from "./loggers.js";
|
||||
import { buildMentionConfig } from "./mentions.js";
|
||||
import { createEchoTracker } from "./monitor/echo.js";
|
||||
import { createWebOnMessageHandler } from "./monitor/on-message.js";
|
||||
import type { WebInboundReaction } from "../inbound.js";
|
||||
import type { WebChannelStatus, WebInboundMsg, WebMonitorTuning } from "./types.js";
|
||||
import { isLikelyWhatsAppCryptoError } from "./util.js";
|
||||
|
||||
@ -173,7 +174,10 @@ export async function monitorWebChannel(
|
||||
account,
|
||||
});
|
||||
|
||||
const inboundDebounceMs = resolveInboundDebounceMs({ cfg, channel: "whatsapp" });
|
||||
const inboundDebounceMs = resolveInboundDebounceMs({
|
||||
cfg,
|
||||
channel: "whatsapp",
|
||||
});
|
||||
const shouldDebounce = (msg: WebInboundMsg) => {
|
||||
if (msg.mediaPath || msg.mediaType) return false;
|
||||
if (msg.location) return false;
|
||||
@ -198,6 +202,31 @@ export async function monitorWebChannel(
|
||||
_lastInboundMsg = msg;
|
||||
await onMessage(msg);
|
||||
},
|
||||
onReaction: (reaction: WebInboundReaction) => {
|
||||
status.lastEventAt = Date.now();
|
||||
emitStatus();
|
||||
const route = resolveAgentRoute({
|
||||
cfg,
|
||||
channel: "whatsapp",
|
||||
accountId: reaction.accountId,
|
||||
peer: {
|
||||
kind: reaction.chatType === "group" ? "group" : "dm",
|
||||
id: reaction.chatJid,
|
||||
},
|
||||
});
|
||||
const senderLabel = reaction.senderE164 ?? reaction.senderJid ?? "someone";
|
||||
const text = `WhatsApp reaction added: ${reaction.emoji} by ${senderLabel} msg ${reaction.messageId}`;
|
||||
const contextKey = [
|
||||
"whatsapp",
|
||||
"reaction",
|
||||
"added",
|
||||
reaction.messageId,
|
||||
reaction.senderJid ?? "unknown",
|
||||
reaction.emoji,
|
||||
reaction.chatJid,
|
||||
].join(":");
|
||||
enqueueSystemEvent(text, { sessionKey: route.sessionKey, contextKey });
|
||||
},
|
||||
});
|
||||
|
||||
status.connected = true;
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
export { resetWebInboundDedupe } from "./inbound/dedupe.js";
|
||||
export { extractLocationData, extractMediaPlaceholder, extractText } from "./inbound/extract.js";
|
||||
export { monitorWebInbox } from "./inbound/monitor.js";
|
||||
export type { WebInboundMessage, WebListenerCloseReason } from "./inbound/types.js";
|
||||
export type {
|
||||
WebInboundMessage,
|
||||
WebInboundReaction,
|
||||
WebListenerCloseReason,
|
||||
} from "./inbound/types.js";
|
||||
|
||||
@ -20,13 +20,15 @@ import {
|
||||
} from "./extract.js";
|
||||
import { downloadInboundMedia } from "./media.js";
|
||||
import { createWebSendApi } from "./send-api.js";
|
||||
import type { WebInboundMessage, WebListenerCloseReason } from "./types.js";
|
||||
import type { WebInboundMessage, WebInboundReaction, WebListenerCloseReason } from "./types.js";
|
||||
|
||||
export async function monitorWebInbox(options: {
|
||||
verbose: boolean;
|
||||
accountId: string;
|
||||
authDir: string;
|
||||
onMessage: (msg: WebInboundMessage) => Promise<void>;
|
||||
/** Called when a reaction is received on a message. */
|
||||
onReaction?: (reaction: WebInboundReaction) => void;
|
||||
mediaMaxMb?: number;
|
||||
/** Send read receipts for incoming messages (default true). */
|
||||
sendReadReceipts?: boolean;
|
||||
@ -313,6 +315,67 @@ export async function monitorWebInbox(options: {
|
||||
};
|
||||
sock.ev.on("messages.upsert", handleMessagesUpsert);
|
||||
|
||||
// Baileys emits messages.reaction when someone reacts to a message.
|
||||
const handleMessagesReaction = async (
|
||||
reactions: Array<{
|
||||
key: proto.IMessageKey;
|
||||
reaction: { text?: string; key?: proto.IMessageKey };
|
||||
}>,
|
||||
) => {
|
||||
if (!options.onReaction) return;
|
||||
try {
|
||||
for (const entry of reactions) {
|
||||
try {
|
||||
const targetKey = entry.key;
|
||||
const reactionKey = entry.reaction?.key;
|
||||
const messageId = targetKey?.id;
|
||||
if (!messageId) continue;
|
||||
|
||||
const chatJid = targetKey.remoteJid;
|
||||
if (!chatJid) continue;
|
||||
if (chatJid.endsWith("@status") || chatJid.endsWith("@broadcast")) continue;
|
||||
|
||||
const emoji = entry.reaction?.text ?? "";
|
||||
// Empty emoji = reaction removed; skip removals (matches Signal behavior)
|
||||
if (!emoji) continue;
|
||||
|
||||
const group = isJidGroup(chatJid) === true;
|
||||
// In DMs, reactionKey.fromMe means we reacted (remoteJid is the partner, not us)
|
||||
const senderJid = reactionKey?.fromMe
|
||||
? (selfJid ?? undefined)
|
||||
: (reactionKey?.participant ?? reactionKey?.remoteJid ?? undefined);
|
||||
const senderE164 = senderJid ? await resolveInboundJid(senderJid) : null;
|
||||
|
||||
inboundLogger.info({ emoji, messageId, chatJid, senderJid }, "inbound reaction");
|
||||
|
||||
options.onReaction({
|
||||
messageId,
|
||||
emoji,
|
||||
chatJid,
|
||||
chatType: group ? "group" : "direct",
|
||||
accountId: options.accountId,
|
||||
senderJid: senderJid ?? undefined,
|
||||
senderE164: senderE164 ?? undefined,
|
||||
reactedToFromMe: targetKey.fromMe ?? undefined,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
} catch (err) {
|
||||
inboundLogger.error(
|
||||
{
|
||||
error: String(err),
|
||||
messageId: entry.key?.id,
|
||||
chatJid: entry.key?.remoteJid,
|
||||
},
|
||||
"failed handling inbound reaction",
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (outerErr) {
|
||||
inboundLogger.error({ error: String(outerErr) }, "reaction handler crashed");
|
||||
}
|
||||
};
|
||||
sock.ev.on("messages.reaction", handleMessagesReaction as (...args: unknown[]) => void);
|
||||
|
||||
const handleConnectionUpdate = (
|
||||
update: Partial<import("@whiskeysockets/baileys").ConnectionState>,
|
||||
) => {
|
||||
@ -350,14 +413,19 @@ export async function monitorWebInbox(options: {
|
||||
const messagesUpsertHandler = handleMessagesUpsert as unknown as (
|
||||
...args: unknown[]
|
||||
) => void;
|
||||
const messagesReactionHandler = handleMessagesReaction as unknown as (
|
||||
...args: unknown[]
|
||||
) => void;
|
||||
const connectionUpdateHandler = handleConnectionUpdate as unknown as (
|
||||
...args: unknown[]
|
||||
) => void;
|
||||
if (typeof ev.off === "function") {
|
||||
ev.off("messages.upsert", messagesUpsertHandler);
|
||||
ev.off("messages.reaction", messagesReactionHandler);
|
||||
ev.off("connection.update", connectionUpdateHandler);
|
||||
} else if (typeof ev.removeListener === "function") {
|
||||
ev.removeListener("messages.upsert", messagesUpsertHandler);
|
||||
ev.removeListener("messages.reaction", messagesReactionHandler);
|
||||
ev.removeListener("connection.update", connectionUpdateHandler);
|
||||
}
|
||||
sock.ws?.close();
|
||||
|
||||
@ -40,3 +40,24 @@ export type WebInboundMessage = {
|
||||
mediaUrl?: string;
|
||||
wasMentioned?: boolean;
|
||||
};
|
||||
|
||||
export type WebInboundReaction = {
|
||||
/** Message ID being reacted to. */
|
||||
messageId: string;
|
||||
/** Emoji text (empty string = reaction removed). */
|
||||
emoji: string;
|
||||
/** JID of the chat where the reaction occurred. */
|
||||
chatJid: string;
|
||||
chatType: "direct" | "group";
|
||||
/** Account that received the reaction. */
|
||||
accountId: string;
|
||||
/** JID of the person who reacted. */
|
||||
senderJid?: string;
|
||||
/** E.164 of the person who reacted. */
|
||||
senderE164?: string;
|
||||
/** Push name of the person who reacted. */
|
||||
senderName?: string;
|
||||
/** Whether the reacted message was sent by us. */
|
||||
reactedToFromMe?: boolean;
|
||||
timestamp?: number;
|
||||
};
|
||||
|
||||
460
src/web/monitor-inbox.inbound-reactions.test.ts
Normal file
460
src/web/monitor-inbox.inbound-reactions.test.ts
Normal file
@ -0,0 +1,460 @@
|
||||
import { vi } from "vitest";
|
||||
|
||||
vi.mock("../media/store.js", () => ({
|
||||
saveMediaBuffer: vi.fn().mockResolvedValue({
|
||||
id: "mid",
|
||||
path: "/tmp/mid",
|
||||
size: 1,
|
||||
contentType: "image/jpeg",
|
||||
}),
|
||||
}));
|
||||
|
||||
const mockLoadConfig = vi.fn().mockReturnValue({
|
||||
channels: {
|
||||
whatsapp: {
|
||||
allowFrom: ["*"],
|
||||
},
|
||||
},
|
||||
messages: {
|
||||
messagePrefix: undefined,
|
||||
responsePrefix: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const readAllowFromStoreMock = vi.fn().mockResolvedValue([]);
|
||||
const upsertPairingRequestMock = vi.fn().mockResolvedValue({ code: "PAIRCODE", created: true });
|
||||
|
||||
vi.mock("../config/config.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../config/config.js")>();
|
||||
return {
|
||||
...actual,
|
||||
loadConfig: () => mockLoadConfig(),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../pairing/pairing-store.js", () => ({
|
||||
readChannelAllowFromStore: (...args: unknown[]) => readAllowFromStoreMock(...args),
|
||||
upsertChannelPairingRequest: (...args: unknown[]) => upsertPairingRequestMock(...args),
|
||||
}));
|
||||
|
||||
vi.mock("./session.js", () => {
|
||||
const { EventEmitter } = require("node:events");
|
||||
const ev = new EventEmitter();
|
||||
const sock = {
|
||||
ev,
|
||||
ws: { close: vi.fn() },
|
||||
sendPresenceUpdate: vi.fn().mockResolvedValue(undefined),
|
||||
sendMessage: vi.fn().mockResolvedValue(undefined),
|
||||
readMessages: vi.fn().mockResolvedValue(undefined),
|
||||
updateMediaMessage: vi.fn(),
|
||||
logger: {},
|
||||
signalRepository: {
|
||||
lidMapping: {
|
||||
getPNForLID: vi.fn().mockResolvedValue(null),
|
||||
},
|
||||
},
|
||||
user: { id: "123@s.whatsapp.net" },
|
||||
groupMetadata: vi.fn().mockResolvedValue({ subject: "Test Group", participants: [] }),
|
||||
};
|
||||
return {
|
||||
createWaSocket: vi.fn().mockResolvedValue(sock),
|
||||
waitForWaConnection: vi.fn().mockResolvedValue(undefined),
|
||||
getStatusCode: vi.fn(() => 500),
|
||||
};
|
||||
});
|
||||
|
||||
const { createWaSocket } = await import("./session.js");
|
||||
|
||||
import fsSync from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
import { resetLogger, setLoggerOverride } from "../logging.js";
|
||||
import { monitorWebInbox, resetWebInboundDedupe } from "./inbound.js";
|
||||
|
||||
const ACCOUNT_ID = "default";
|
||||
let authDir: string;
|
||||
|
||||
describe("web monitor inbox – inbound reactions", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
readAllowFromStoreMock.mockResolvedValue([]);
|
||||
resetWebInboundDedupe();
|
||||
authDir = fsSync.mkdtempSync(path.join(os.tmpdir(), "moltbot-auth-"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
resetLogger();
|
||||
setLoggerOverride(null);
|
||||
vi.useRealTimers();
|
||||
fsSync.rmSync(authDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("calls onReaction for inbound reaction events", async () => {
|
||||
const onMessage = vi.fn(async () => {});
|
||||
const onReaction = vi.fn();
|
||||
|
||||
const listener = await monitorWebInbox({
|
||||
verbose: false,
|
||||
onMessage,
|
||||
onReaction,
|
||||
accountId: ACCOUNT_ID,
|
||||
authDir,
|
||||
});
|
||||
const sock = await createWaSocket();
|
||||
|
||||
const reactionEvent = [
|
||||
{
|
||||
key: {
|
||||
id: "msg-123",
|
||||
remoteJid: "999@s.whatsapp.net",
|
||||
fromMe: false,
|
||||
},
|
||||
reaction: {
|
||||
text: "👍",
|
||||
key: {
|
||||
remoteJid: "888@s.whatsapp.net",
|
||||
participant: "888@s.whatsapp.net",
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
sock.ev.emit("messages.reaction", reactionEvent);
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
expect(onReaction).toHaveBeenCalledTimes(1);
|
||||
expect(onReaction).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
messageId: "msg-123",
|
||||
emoji: "👍",
|
||||
chatJid: "999@s.whatsapp.net",
|
||||
chatType: "direct",
|
||||
accountId: ACCOUNT_ID,
|
||||
senderJid: "888@s.whatsapp.net",
|
||||
reactedToFromMe: false,
|
||||
}),
|
||||
);
|
||||
expect(onMessage).not.toHaveBeenCalled();
|
||||
|
||||
await listener.close();
|
||||
});
|
||||
|
||||
it("skips reaction removals (empty emoji)", async () => {
|
||||
const onReaction = vi.fn();
|
||||
|
||||
const listener = await monitorWebInbox({
|
||||
verbose: false,
|
||||
onMessage: vi.fn(async () => {}),
|
||||
onReaction,
|
||||
accountId: ACCOUNT_ID,
|
||||
authDir,
|
||||
});
|
||||
const sock = await createWaSocket();
|
||||
|
||||
sock.ev.emit("messages.reaction", [
|
||||
{
|
||||
key: {
|
||||
id: "msg-123",
|
||||
remoteJid: "999@s.whatsapp.net",
|
||||
fromMe: false,
|
||||
},
|
||||
reaction: {
|
||||
text: "",
|
||||
key: { remoteJid: "888@s.whatsapp.net" },
|
||||
},
|
||||
},
|
||||
]);
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
expect(onReaction).not.toHaveBeenCalled();
|
||||
|
||||
await listener.close();
|
||||
});
|
||||
|
||||
it("skips reactions on status/broadcast JIDs", async () => {
|
||||
const onReaction = vi.fn();
|
||||
|
||||
const listener = await monitorWebInbox({
|
||||
verbose: false,
|
||||
onMessage: vi.fn(async () => {}),
|
||||
onReaction,
|
||||
accountId: ACCOUNT_ID,
|
||||
authDir,
|
||||
});
|
||||
const sock = await createWaSocket();
|
||||
|
||||
sock.ev.emit("messages.reaction", [
|
||||
{
|
||||
key: {
|
||||
id: "msg-123",
|
||||
remoteJid: "status@status",
|
||||
fromMe: false,
|
||||
},
|
||||
reaction: {
|
||||
text: "👍",
|
||||
key: { remoteJid: "888@s.whatsapp.net" },
|
||||
},
|
||||
},
|
||||
]);
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
expect(onReaction).not.toHaveBeenCalled();
|
||||
|
||||
await listener.close();
|
||||
});
|
||||
|
||||
it("identifies group reactions by chatType", async () => {
|
||||
const onReaction = vi.fn();
|
||||
|
||||
const listener = await monitorWebInbox({
|
||||
verbose: false,
|
||||
onMessage: vi.fn(async () => {}),
|
||||
onReaction,
|
||||
accountId: ACCOUNT_ID,
|
||||
authDir,
|
||||
});
|
||||
const sock = await createWaSocket();
|
||||
|
||||
sock.ev.emit("messages.reaction", [
|
||||
{
|
||||
key: {
|
||||
id: "msg-456",
|
||||
remoteJid: "120363@g.us",
|
||||
fromMe: true,
|
||||
},
|
||||
reaction: {
|
||||
text: "❤️",
|
||||
key: {
|
||||
remoteJid: "120363@g.us",
|
||||
participant: "777@s.whatsapp.net",
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
expect(onReaction).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
messageId: "msg-456",
|
||||
emoji: "❤️",
|
||||
chatJid: "120363@g.us",
|
||||
chatType: "group",
|
||||
senderJid: "777@s.whatsapp.net",
|
||||
reactedToFromMe: true,
|
||||
}),
|
||||
);
|
||||
|
||||
await listener.close();
|
||||
});
|
||||
|
||||
it("resolves self-reactions in DMs to own JID", async () => {
|
||||
const onReaction = vi.fn();
|
||||
|
||||
const listener = await monitorWebInbox({
|
||||
verbose: false,
|
||||
onMessage: vi.fn(async () => {}),
|
||||
onReaction,
|
||||
accountId: ACCOUNT_ID,
|
||||
authDir,
|
||||
});
|
||||
const sock = await createWaSocket();
|
||||
|
||||
// Self-reaction: reactionKey.fromMe = true, remoteJid is the partner
|
||||
sock.ev.emit("messages.reaction", [
|
||||
{
|
||||
key: {
|
||||
id: "msg-789",
|
||||
remoteJid: "999@s.whatsapp.net",
|
||||
fromMe: false,
|
||||
},
|
||||
reaction: {
|
||||
text: "👍",
|
||||
key: {
|
||||
remoteJid: "999@s.whatsapp.net",
|
||||
fromMe: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
expect(onReaction).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
messageId: "msg-789",
|
||||
emoji: "👍",
|
||||
// senderJid should be our own JID, not the chat partner
|
||||
senderJid: "123@s.whatsapp.net",
|
||||
}),
|
||||
);
|
||||
|
||||
await listener.close();
|
||||
});
|
||||
|
||||
it("continues processing remaining reactions when callback throws", async () => {
|
||||
const onReaction = vi.fn().mockImplementationOnce(() => {
|
||||
throw new Error("boom");
|
||||
});
|
||||
|
||||
const listener = await monitorWebInbox({
|
||||
verbose: false,
|
||||
onMessage: vi.fn(async () => {}),
|
||||
onReaction,
|
||||
accountId: ACCOUNT_ID,
|
||||
authDir,
|
||||
});
|
||||
const sock = await createWaSocket();
|
||||
|
||||
sock.ev.emit("messages.reaction", [
|
||||
{
|
||||
key: { id: "msg-1", remoteJid: "999@s.whatsapp.net", fromMe: false },
|
||||
reaction: { text: "👍", key: { remoteJid: "888@s.whatsapp.net" } },
|
||||
},
|
||||
{
|
||||
key: { id: "msg-2", remoteJid: "999@s.whatsapp.net", fromMe: false },
|
||||
reaction: { text: "❤️", key: { remoteJid: "888@s.whatsapp.net" } },
|
||||
},
|
||||
]);
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
// Both reactions should be attempted despite first throwing
|
||||
expect(onReaction).toHaveBeenCalledTimes(2);
|
||||
|
||||
await listener.close();
|
||||
});
|
||||
|
||||
it("skips reactions with missing messageId", async () => {
|
||||
const onReaction = vi.fn();
|
||||
|
||||
const listener = await monitorWebInbox({
|
||||
verbose: false,
|
||||
onMessage: vi.fn(async () => {}),
|
||||
onReaction,
|
||||
accountId: ACCOUNT_ID,
|
||||
authDir,
|
||||
});
|
||||
const sock = await createWaSocket();
|
||||
|
||||
sock.ev.emit("messages.reaction", [
|
||||
{
|
||||
key: {
|
||||
id: undefined,
|
||||
remoteJid: "999@s.whatsapp.net",
|
||||
fromMe: false,
|
||||
},
|
||||
reaction: {
|
||||
text: "👍",
|
||||
key: { remoteJid: "888@s.whatsapp.net" },
|
||||
},
|
||||
},
|
||||
]);
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
expect(onReaction).not.toHaveBeenCalled();
|
||||
|
||||
await listener.close();
|
||||
});
|
||||
|
||||
it("skips reactions with missing remoteJid", async () => {
|
||||
const onReaction = vi.fn();
|
||||
|
||||
const listener = await monitorWebInbox({
|
||||
verbose: false,
|
||||
onMessage: vi.fn(async () => {}),
|
||||
onReaction,
|
||||
accountId: ACCOUNT_ID,
|
||||
authDir,
|
||||
});
|
||||
const sock = await createWaSocket();
|
||||
|
||||
sock.ev.emit("messages.reaction", [
|
||||
{
|
||||
key: {
|
||||
id: "msg-123",
|
||||
remoteJid: undefined,
|
||||
fromMe: false,
|
||||
},
|
||||
reaction: {
|
||||
text: "👍",
|
||||
key: { remoteJid: "888@s.whatsapp.net" },
|
||||
},
|
||||
},
|
||||
]);
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
expect(onReaction).not.toHaveBeenCalled();
|
||||
|
||||
await listener.close();
|
||||
});
|
||||
|
||||
it("handles missing reaction.key gracefully (senderJid undefined)", async () => {
|
||||
const onReaction = vi.fn();
|
||||
|
||||
const listener = await monitorWebInbox({
|
||||
verbose: false,
|
||||
onMessage: vi.fn(async () => {}),
|
||||
onReaction,
|
||||
accountId: ACCOUNT_ID,
|
||||
authDir,
|
||||
});
|
||||
const sock = await createWaSocket();
|
||||
|
||||
sock.ev.emit("messages.reaction", [
|
||||
{
|
||||
key: {
|
||||
id: "msg-123",
|
||||
remoteJid: "999@s.whatsapp.net",
|
||||
fromMe: false,
|
||||
},
|
||||
reaction: {
|
||||
text: "🔥",
|
||||
// no key — sender unknown
|
||||
},
|
||||
},
|
||||
]);
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
expect(onReaction).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
messageId: "msg-123",
|
||||
emoji: "🔥",
|
||||
senderJid: undefined,
|
||||
senderE164: undefined,
|
||||
}),
|
||||
);
|
||||
|
||||
await listener.close();
|
||||
});
|
||||
|
||||
it("works without onReaction callback (no-op)", async () => {
|
||||
const onMessage = vi.fn(async () => {});
|
||||
|
||||
const listener = await monitorWebInbox({
|
||||
verbose: false,
|
||||
onMessage,
|
||||
accountId: ACCOUNT_ID,
|
||||
authDir,
|
||||
});
|
||||
const sock = await createWaSocket();
|
||||
|
||||
// Should not throw when no onReaction is provided
|
||||
sock.ev.emit("messages.reaction", [
|
||||
{
|
||||
key: {
|
||||
id: "msg-123",
|
||||
remoteJid: "999@s.whatsapp.net",
|
||||
fromMe: false,
|
||||
},
|
||||
reaction: {
|
||||
text: "👍",
|
||||
key: { remoteJid: "888@s.whatsapp.net" },
|
||||
},
|
||||
},
|
||||
]);
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
await listener.close();
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user