feat(whatsapp): add native WhatsApp channel with whatsmeow#720
Merged
feat(whatsapp): add native WhatsApp channel with whatsmeow#720
Conversation
…tials struct Bridge URL now stored in channel config instead of credentials table, simplifying factory logic. Added legacy fallback for existing instances. Added require_mention config option.
… messages Track active keepTyping() goroutines per chatID via typingCancel sync.Map. Cancel previous loop before starting new one when recipient is typing. Stop all typing loops in Stop() to prevent orphaned goroutines. Clear typing indicator when message sent.
…dling
Bridge now sends QR codes and status events. Go side caches latest QR PNG (base64) and broadcasts to UI wizard via message bus. handleBridgeStatus() marks channel healthy/degraded based on connection state and clears QR once authenticated.
Media support: bridge sends [{type, mimetype, filename, path}] array. Go side parses into MediaInfo structs and feeds into agent pipeline (matches Telegram pattern).
Added:
- qr_methods.go: RPC handler for whatsapp.qr.start
- format.go: markdownToWhatsApp() text conversion
- format_test.go: format unit tests
- WhatsApp UI components + locale strings
Go's regexp treats $1_ as submatch named "1_" (nonexistent → empty),
silently dropping content for <em>, <i>, <del>, <s>, <code>, and <a> tags.
Use ${1} to disambiguate. Tests added and passing.
…ling - Bridge: new media message handler reads file from path, determines Baileys content type from MIME, sends via sock.sendMessage() - Bridge: fix inbound media by adding null/empty buffer check, properly binding sock.updateMediaMessage to preserve context - Go: Send() now iterates msg.Media, sends each as media message to bridge; first media gets content as caption, remaining text sent separately Fixes #703
Add shared volumes (goclaw-workspace, whatsapp-media) so bridge and GoClaw containers can access each other's media files. Set MEDIA_DIR env var to route downloads to shared volume for GoClaw read access.
3 tasks
WhatsApp uses two identifier systems: phone JID (@s.whatsapp.net) and Link ID (@lid). Group mentions may use either format, but the bot only knew its phone JID — so LID-based mentions always failed silently. Three bugs fixed: - Bridge didn't replay bot JID on GoClaw reconnect (myJID always empty) - Bridge sent raw Baileys JID with device suffix (:N@) causing mismatch - Only phone JID was checked; LID mentions were never matched Now the bridge caches and sends both bare JID + LID, and the Go mention check normalizes and compares against both identifiers.
- Use go.mau.fi/whatsmeow for in-process WhatsApp protocol - Auth state in PostgreSQL (standard) / SQLite (desktop) via sqlstore - QR auth driven by whatsmeow GetQRChannel() directly - Remove bridge/whatsapp/, docker-compose.whatsapp.yml, bridge_url config - Clean bridge_url from UI schemas, i18n locales, NETWORK_KEYS - Fix Reauth() race condition with mutex + context reset - Lazy client init in StartQRFlow for wizard timing race Resolves #703
Set wastore.DeviceProps.Os so WhatsApp displays "GoClaw" instead of "Other device" in the phone's Linked Devices screen.
After instance creation, the async reload may not have registered the channel in the manager yet. Poll up to 5s (10x500ms) for the channel to appear before returning "channel not found" error.
WhatsApp uses dual identity: phone JID and LID (Link ID). Group messages may use LID addressing where evt.Info.Sender is in LID format. This caused policy checks, pairing lookups, and allowlists to fail silently for group messages. - Normalize LID-addressed senders to phone JID via SenderAlt - Capture bot's LID from device store on connect - Check both JID and LID in group mention detection
WhatsApp channel links a personal account as a device, not a bot. Updated comments and user-facing pairing text accordingly.
When a user replies to a message with @mention, extract the quoted (replied-to) message text and prepend it as [Replying to: ...]. Without this, the agent only saw the mention text and had no context about what the user was referring to.
When require_mention is enabled, record non-mentioned group messages in PendingHistory (matching Telegram/Slack/Discord pattern). When the account IS mentioned, prepend accumulated group context so the LLM has conversational context from the group chat.
- Chunk outbound messages at 4096 chars, splitting at paragraph/line/ word boundaries to avoid truncation on long responses - Add history_limit config (default 200) matching Telegram/Slack/Discord - Pass PendingMessageStore for DB-persisted group history
WhatsApp has no token — show as configured only when enabled, not unconditionally true.
WhatsApp has no native table support. Markdown tables now render as ASCII-aligned text inside ``` blocks (monospace), matching Telegram's approach. Tables are detected before code block extraction so they don't get mangled by other formatting passes.
This reverts commit dac6e29.
- Move DeviceProps.Os to package init (set once, not per New() call) - Remove redundant QR done bus broadcast from handleConnected (QR session already handles this via direct client events) - Remove "from bridge" text in i18n QR waiting messages (all 3 locales) - Fix inline code test expectation
…leanup, reauth race - Classify media download errors (timeout/decrypt/expired/unsupported) in log - Add mention_test.go with 10 test cases covering dual JID/LID detection - Schedule temp media file cleanup after 5 minutes via goroutine - Serialize Reauth() and StartQRFlow() with reauthMu to prevent rapid double-click race - Fix stale format_test.go expectation for inline code conversion
|
nice...this is working. Working Flawless 💯 |
Split whatsapp.go (980 lines) into 6 focused files:
- whatsapp.go: struct, lifecycle, event dispatch (221 lines)
- inbound.go: message processing, text extraction, mentions (266 lines)
- outbound.go: Send, media upload, typing indicators (182 lines)
- media_download.go: download, mime mapping, temp cleanup (140 lines)
- auth.go: StartQRFlow, Reauth (102 lines)
- policy.go: DM/group policy, pairing reply (141 lines)
Fixes from code review:
- Remove fragile detectDialect (%T sniffing); pass explicit dialect string
to New() and FactoryWithDB() from callers
- Safe two-value type assertions for typingCancel sync.Map entries
- Reauth() uses parentCtx (stored in Start()) instead of context.Background()
- scheduleMediaCleanup uses time.AfterFunc instead of goroutine+sleep
- mimeToExt: fix PDF to use strings.HasPrefix("application/pdf")
- Add emptyMessageSentinel const to replace string literals
- Restore "whatsapp" in IsDefaultChannelInstance (breaking change fix)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Resolves #703 — implements a full WhatsApp channel integration for GoClaw using whatsmeow (native Go library, no external bridge service needed).
internal/channels/whatsapp/) — connects directly to WhatsApp multi-device protocol via whatsmeow, no separate bridge processwhatsapp.qr.start) + React UI for QR-code-based authentication and re-auth@mentiongating in groupsWhat was wrong
Users trying to integrate WhatsApp (#703) had to run a separate Node.js bridge with a mismatched protocol (different field names, no
typefield). GoClaw didn't respond because it expectedtype:"message",from,contentbut bridges sentsender,body.What this PR does
@mentiongatingKey files
internal/channels/whatsapp/{whatsapp,factory,format,qr_methods}.gointernal/config/config_channels.go(WhatsAppConfig)pkg/protocol/{events,methods}.gointernal/gateway/methods/access.go(QR handler registration)internal/store/{pairing_store,pg/pairing,sqlitestore/pairing}.goui/web/src/pages/channels/whatsapp/(3 files)internal/gateway/event_filter.goTest plan
go build ./...passesgo build -tags sqliteonly ./...passesgo vet ./...passesgo test ./internal/channels/whatsapp/...passes@mentiongating in groups