fix(bridge): redraw the pairing QR code on every rotation - #213
Open
LINTEL-1 wants to merge 162 commits into
Open
fix(bridge): redraw the pairing QR code on every rotation#213LINTEL-1 wants to merge 162 commits into
LINTEL-1 wants to merge 162 commits into
Conversation
- Save the names of group chats so you can query them - Ability to send messages to group chats by using the jid
minor fix
clarify the path in json setting.
…e-filtering Improve date handling
…udio feat: send files and audio messages
Fix typo in README.md
…ow names in group messages (verygoodplugins#34) * added support for recieving media + improved group printing * fix formatting
- Enhanced Go bridge with media handling and webhook notifications - Added real-time message forwarding to webhook endpoint - Complete media download/upload support for images, video, audio, docs - Ready for Claude Desktop integration
Introduces new REST API endpoints for health checks and typing indicators, improves media filename consistency by using message timestamps, and adds robust reconnection logic for WhatsApp client stability. Also updates .gitignore for Node.js dependencies and storage, and adds initial package.json and package-lock.json for Node.js tooling.
This is a maintained fork of lharries/whatsapp-mcp with bug fixes, improvements, and continued development. ## Fixed - Go compilation errors from whatsmeow API changes (context.Background()) - golangci.yml configuration (removed deprecated linters) - Python linting issues (185 errors fixed) ## Added - Contact names in message responses (sender_display: "Name (phone)") - Increased default limits (50 messages, 50 chats) - Max limits for large queries (500 messages, 200 chats) - CI/CD pipeline with GitHub Actions - Test suite with pytest - Comprehensive documentation ## Changed - Upgraded whatsmeow to latest version - Improved docstrings with date format examples - Removed unused npm dependencies from Go bridge ## Removed - Compiled binary from git tracking Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…lution (verygoodplugins#135) * fix(bridge): fall back to local whatsmeow_contacts for chat name resolution The bridge currently relies solely on client.Store.Contacts.GetContact() to resolve contact names for individual chats. When the WhatsApp server hasn't synced a contact yet, GetContact returns empty FullName and the bridge falls through to using the raw phone number (jid.User) as the chat name — even when the contact is saved in the local whatsmeow SQLite database. Add a fallback that queries the local whatsmeow_contacts table (COALESCE full_name, push_name, first_name) when the server-side lookup fails or returns an empty name. This ensures contact names are always resolved when available locally. Changes: - MessageStore: add waDB *sql.DB pointing to whatsmeow's store/whatsapp.db - NewMessageStore: open and store the whatsapp.db connection - Close: clean up the waDB connection on shutdown - GetChatName: add whatsmeow_contacts SQLite query as fallback before falling through to the raw jid.User number Tests: 99/99 pass. The fallback is guarded by waDB != nil, so existing tests (which use newTestMessageStore with waDB=nil) continue to work unchanged. * fix(bridge): scope local contact fallback --------- Co-authored-by: Evandro Fonseca Junior <evandrofonseca.junior@gmail.com> Co-authored-by: Jack Arturo <info@verygoodplugins.com>
* chore(main): release 0.4.0 * chore(release): trigger release PR checks --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Jack Arturo <info@verygoodplugins.com>
* chore(main): release 0.4.1 * chore(mcp): update uv lock for 0.4.1 --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Jack Arturo <info@verygoodplugins.com>
* fix(bridge): authenticate outbound webhook POSTs with bridge token The bridge POSTs inbound WhatsApp messages to WEBHOOK_URL but sent no auth header. AutoHub PR #898 makes the hub's POST /whatsapp/webhook route fail-closed, requiring the shared bridge token as `Authorization: Bearer <token>`. Without this change, inbound forwarding breaks the moment the hub's WHATSAPP_BRIDGE_TOKEN is set. sendWebhookPayload now builds the request via http.NewRequest and attaches the token (loaded once at startup into a package-level var from the same loadOrCreateBridgeToken() used for inbound /api/* auth) via Do(). The header is only sent when a token is configured, so nothing breaks before rollout. All inbound paths (SendWebhook, SendWebhookWithMedia, SendReactionWebhook) funnel through sendWebhookPayload, so this one change covers all of them. Docs (README + .env.example) now state the hub's WHATSAPP_BRIDGE_TOKEN must equal this bridge's token. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(bridge): address Codex review on webhook bridge-auth (PR verygoodplugins#153) Two confirmed issues from Codex review: 1. Startup race (main.go): webhookAuthToken was assigned ~230 lines after client.AddEventHandler registered the message handler, and after client.Connect() + a 2s stabilization sleep. WhatsApp can deliver a burst of history-sync backlog as soon as the connection succeeds, so early messages could be forwarded with no bridge token attached. Fixed by loading the token and assigning webhookAuthToken before the event handler is registered, so no event can ever be handled first. 2. Basic-auth clobbering (webhook.go): loadOrCreateBridgeToken() always returns a non-empty token in real deployments (auto-generated on first run), so the Authorization: Bearer header was unconditionally attached post-startup. net/http derives "Authorization: Basic" from credentials embedded in WEBHOOK_URL (http://user:pass@host/...) only when the request's Authorization header is otherwise unset, so this silently broke any receiver relying on URL userinfo for auth. Fixed by moving the bridge token to a dedicated X-Bridge-Token header instead of Authorization - autohub PR #898's bridge-auth middleware already accepts either, so this is a no-downside, in-contract change. Added TestSendWebhookPreservesURLBasicAuth (red before the fix) and updated the two existing webhook tests for the X-Bridge-Token header name. Docs updated to match. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(bridge): address second Codex review round on webhook bridge-auth (PR verygoodplugins#153) Three confirmed issues from a fresh Codex pass on commit ac64806: 1. Token leak to implicit default (P1, webhook.go): the REST bridge token also authorizes /api/* calls (e.g. send). sendWebhookPayload was attaching it to WEBHOOK_URL even when that var was left unset, i.e. to the hardcoded local fallback http://localhost:8769/whatsapp/webhook - an address nobody configured or vetted. Any other local process binding that port could capture the token just by being reachable. Fixed by only attaching X-Bridge-Token when WEBHOOK_URL was explicitly set by the operator; the implicit default never receives it, matching the existing threat model in auth.go (loopback is not a trust boundary). 2. Banner-timing regression (P2, main.go): the previous fix moved loadOrCreateBridgeToken() before the connect/pairing loop to close a startup race, but the one-time setup banner still only printed after a successful connection. loadOrCreateBridgeToken() persists a freshly generated token to disk immediately, so a QR-pairing timeout or early exit would leave a token on disk the user was never shown - and the next run would report fresh=false forever, permanently skipping the banner. Fixed by resolving the REST port (pure env parsing, no connection dependency) alongside the token load and printing the banner immediately, before the connect/pairing loop even starts. 3. Token leak via redirect (P2, webhook.go): Go's default http.Client follows redirects and forwards custom headers to the redirect target regardless of host - unlike Authorization/Cookie, which it strips cross-origin. A misconfigured or malicious WEBHOOK_URL returning a 3xx could cause the bridge to forward X-Bridge-Token to an arbitrary third-party host. Fixed by setting CheckRedirect on webhookClient to stop following redirects entirely (returns the pre-redirect response instead), since WEBHOOK_URL is a single fixed operator-configured endpoint with no legitimate need to redirect. Added TestSendWebhookOmitsBridgeTokenOnImplicitDefaultURL and TestSendWebhookDoesNotFollowRedirects (both red before their respective fixes). Docs updated to match. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…#177) * fix: exit orphaned stdio MCP servers on parent death Clients can abandon sessions while a wrapper keeps stdin open, so EOF never arrives and leaked servers thrash swap. Add a parent-liveness watchdog (same approach as mcp-automem). * fix: pin parent pid early; document soft stdin EOF Capture os.getppid() before any await and pass it into the watchdog. Soft stdin EOF remains intentional (no hard-exit on EOF alone); hard exit stays on parent reparent / OS signals. * fix: ship parent_watchdog in setuptools py-modules Installed wheels omitted the watchdog module, so entrypoints failed with ModuleNotFoundError. Add it to py-modules alongside the other top-level modules. * fix: document WHATSAPP_PARENT_WATCHDOG_S and fix ruff imports Document the new stdio parent-watchdog knob in AGENTS.md, README, and .env.example. Sort imports so Python Lint passes. * fix: ruff-format stdio watchdog call site Python Lint runs ruff format --check on whatsapp-mcp-server.
verygoodplugins#149) * fix(bridge): preserve original timestamp on retry-redelivered messages When the bridge has been offline for a while (an outage, or the periodic linked-device session expiry), incoming messages can fail to decrypt on first delivery because the local Signal session lacks the current sender key. whatsmeow sends a retry receipt and WhatsApp re-sends the message, but the re-sent stanza's `t` attribute is the resend time, not the original send time. The bridge stored that resend time, so messages actually sent hours or days earlier were stamped at reconnect time — corrupting recency-ordered views (last-message-per-chat, chat sort order) for the affected messages until they aged out. The original timestamp is recoverable: the first (undecryptable) delivery carries it, and whatsmeow surfaces it via events.UndecryptableMessage moments before the retry resend arrives. Cache the original timestamp by message ID when the UndecryptableMessage fires, then reuse it when the decrypted retry lands in handleMessage — guarded to only override when strictly earlier, entries consumed on use, with a soft size cap so a burst of never-retried messages can't grow the map unbounded. Applied to both the content and reaction store paths and to the chat's last-message time. Adds unit tests for the timestamp-cache helpers. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(bridge): build retry-media filenames from the corrected timestamp extractMediaInfo was still fed the raw resend-time msg.Info.Timestamp, so for retry-redelivered media the webhook MediaFilename used the resend time while downloadMedia rebuilds the on-disk name from the stored (original) timestamp — metadata pointed at a filename that never exists. Pass the same retry-corrected msgTimestamp used for storage. Addresses Codex review feedback on verygoodplugins#149. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…y with 2 updates (verygoodplugins#185) Bumps the actions-version-updates group with 2 updates in the / directory: [actions/setup-python](https://github.com/actions/setup-python) and [actions/setup-go](https://github.com/actions/setup-go). Updates `actions/setup-python` from 6 to 7 - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](actions/setup-python@v6...v7) Updates `actions/setup-go` from 6 to 7 - [Release notes](https://github.com/actions/setup-go/releases) - [Commits](actions/setup-go@v6...v7) --- updated-dependencies: - dependency-name: actions/setup-python dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions-version-updates - dependency-name: actions/setup-go dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions-version-updates ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps the gomod-version-updates group with 1 update in the /whatsapp-bridge directory: [github.com/mattn/go-sqlite3](https://github.com/mattn/go-sqlite3). Updates `github.com/mattn/go-sqlite3` from 1.14.48 to 1.14.49 - [Release notes](https://github.com/mattn/go-sqlite3/releases) - [Commits](mattn/go-sqlite3@v1.14.48...v1.14.49) --- updated-dependencies: - dependency-name: github.com/mattn/go-sqlite3 dependency-version: 1.14.47 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gomod-version-updates ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…erygoodplugins#164) Bumps [mcp](https://github.com/modelcontextprotocol/python-sdk) from 1.27.1 to 1.28.1. - [Release notes](https://github.com/modelcontextprotocol/python-sdk/releases) - [Changelog](https://github.com/modelcontextprotocol/python-sdk/blob/main/RELEASE.md) - [Commits](modelcontextprotocol/python-sdk@v1.27.1...v1.28.1) --- updated-dependencies: - dependency-name: mcp dependency-version: 1.28.1 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
verygoodplugins#157) WhatsApp shows every paired session under Settings > Linked Devices using whatsmeow's default label "whatsmeow" (store.DeviceProps.Os). That name is opaque to end users and can look suspicious enough that they remove the device, which unpairs the bridge. Add an optional WHATSAPP_DEVICE_NAME env var. When set, it is assigned to store.DeviceProps.Os before whatsmeow.NewClient so the linked device shows a recognisable label. Empty/unset keeps the whatsmeow default, so existing behaviour is unchanged. Like the history-sync options it only takes effect at pair time; re-pair to change an already-paired session. The platform icon (DeviceProps.PlatformType) is intentionally left untouched: this is a labelling convenience, not a way to impersonate an official client. Docs added to README, AGENTS.md, and .env.example; unit test covers trimming and the unset/empty default. Closes verygoodplugins#156 Co-authored-by: Francis <francis@glints.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Jack Arturo <info@verygoodplugins.com>
Sending a message whose text contains @<number> renders as plain text on recipients' devices: WhatsApp only renders a highlighted, tappable, notifying mention when the message carries the mentioned users' JIDs in ContextInfo.MentionedJID. Neither the bridge API nor the send_message MCP tool had any way to set that, so agent-sent group updates could not tag anyone. - bridge: add an optional 'mentions' field ([]string of phone numbers or JIDs) to /api/send. Text messages with mentions are sent as ExtendedTextMessage with ContextInfo.MentionedJID (composes with the existing quoted-reply ContextInfo); image/video/document captions get MentionedJID on their own ContextInfo. - bridge: new resolveMentionJIDs() resolves phone-number entries to both the phone JID and, when the LID cache knows it, the LID form — mirrors the resolveRecipientJID LID handling so mentions render in LID-addressed groups as well as legacy ones. - mcp: send_message gains mentions: list[str] | None, forwarded in the /api/send payload; docstring documents that the message text must contain a matching @<number> token per entry. Behavior without the new field is unchanged. Co-authored-by: ladia <eulittleapps@gmail.com>
…lugins#168) * feat(bridge): add on-demand history sync for a single chat `--full-history-pair` only applies to a fresh pair, so recovering a gap in a single chat otherwise means deleting whatsapp.db, re-scanning the QR and re-syncing everything. whatsmeow already exposes Client.BuildHistorySyncRequest; it just was not reachable from the bridge. This adds POST /api/history, which requests older messages for one chat at runtime, anchored on the oldest message already stored for that chat so the phone returns messages from before it. Results arrive through the existing events.HistorySync handler, so no new storage or parsing path is introduced. The endpoint is additive and opt-in: nothing changes for callers that do not use it. As with pair-time sync, the phone decides how much it actually returns, so count is a request rather than a guarantee. Closes verygoodplugins#167 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(bridge): send history request via SendPeerMessage whatsmeow documents SendPeerMessage as the transport for the message built by BuildHistorySyncRequest. It resolves the account's own JID and sets the peer flag internally, so calling SendMessage with SendRequestExtra{Peer:true} duplicated that addressing logic and would drift if whatsmeow ever changes how peer messages are addressed. Also records whatsmeow's documented recommendation of 50 messages per request next to the default, and notes that the on-demand response arrives as events.HistorySync with type ON_DEMAND. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(bridge): address review on on-demand history endpoint Responds to @edmenendez's review on verygoodplugins#168. - Drop the anchor's Sender/IsGroup resolution. BuildHistorySyncRequest, and the HistorySyncOnDemandRequest wire message it builds, read only Chat, ID, IsFromMe and Timestamp, so that branch (and 5 of its tests) populated fields that are discarded before the request is sent. - Distinguish an empty chat (sql.ErrNoRows -> 404) from a real lookup failure (any other error -> 500). Previously every DB error became 404 "send a message first", which misdirects the caller when the read fails for a locked or corrupt DB rather than an empty result. - Parse cgo go-sqlite3's timestamp write format in anchorTime, so a messages.db written by the cgo driver and read by a pure-Go build no longer fails the anchor. Kept as string literals rather than importing the driver's SQLiteTimestampFormats, which lives in a cgo file and would break CGO_ENABLED=0 builds. - Add httptest coverage for the handler's validation paths (method, body, missing chat_jid, not-connected). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Jack Arturo <info@verygoodplugins.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…s#188) * fix(deps): preserve Intel macOS cryptography installs * fix(deps): constrain MCP to v1 * chore(deps): preserve lockfile marker layout * fix(deps): align lockfile package version
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Closes verygoodplugins#199. Supersedes verygoodplugins#200. Includes Codex P1: resolve PN→LID before MarkRead.
…nc backfill (verygoodplugins#155) Closes verygoodplugins#194. Codex P2 polish (migration last_read_time, history-sync presence, deterministic join, etc.) lands in a same-day follow-up.
…goodplugins#201 (verygoodplugins#203) Same-day Codex P2 follow-up + mark-read local last_read_time glue.
* feat(bridge): add WEBHOOK_ENABLED to opt out of outbound webhooks AGENTS.md documents WEBHOOK_URL as "(empty = disabled)", but an empty value falls back to defaultWebhookURL, so there is currently no way to turn outbound webhooks off. os.Getenv cannot distinguish "unset" from "explicitly empty", and the fallback is deliberate and covered by TestSendWebhookOmitsBridgeTokenOnImplicitDefaultURL. Deployments with no webhook consumer therefore POST to the default for every inbound message and log a connection refused error each time. WEBHOOK_ENABLED defaults to true and is read through the existing getEnvBool helper already used by FORWARD_SELF, so current behavior is unchanged unless an operator opts out. * fix(bridge): honor webhook opt-out in all paths * fix(bridge): skip webhook-only image downloads --------- Co-authored-by: Ed Menendez <ed@menendez.com>
send_file could only send a bare attachment, so a caption meant a second send_message — which arrives as its own bubble, can interleave with other traffic, and reads as two messages rather than one. The bridge already fills the WA media-message Caption field from the request's `message`, so this is a plumbing gap rather than a missing capability: send_file(recipient, media_path, caption) now forwards it and the file arrives captioned, in one message. The field is omitted entirely when the caption is empty, so an uncaptioned send is byte-for-byte what it was before and no existing caller changes behaviour. Tests: caption present goes out as `message` in a single /send call with the auth header; absent and empty-string cases assert the key is not in the payload; a missing file still returns early without touching the bridge. Python suite green; ruff clean.
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
whatsmeow rotates the pairing code roughly every 20 seconds and only the newest one is scannable, but the pairing loop rendered only the first code it received. The expired QR stays on screen looking identical to a live one, so pairing silently fails until "Ran out of QR codes" scrolls past. Redraw on every "code" event, clear the screen first when stdout is a terminal, and label each code with a counter and generation time so it is obvious which one is current. Non-TTY stdout (pipes, log files) skips the escape sequence and just appends each code. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Companion to the QR redraw fix: the troubleshooting list had no entry for a QR that renders fine but is silently rejected. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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
whatsmeow rotates the pairing QR roughly every 20 seconds and only the newest code is scannable, but the pairing loop guarded rendering behind a
qrCodeShownboolean and drew only the first one. The expired QR stays on screen looking identical to a live one, so pairing silently fails untilRan out of QR codesscrolls past — with no visible indication of why.codeevent, labelled with a counter and generation time.os.Stdout.Stat()/os.ModeCharDevice— stdlib only, no new dependency.Type of change
fix— bug fixfeat— new featurechore/docs/ci/refactor/test/perf!in commit, orBREAKING CHANGE:in body)Scope check
ROADMAP.md"in scope" list, or I've opened an issue first to discuss~24 lines changed in
whatsapp-bridge/main.go, 3 inREADME.md. No dependency, API, schema or config changes. Affects the interactive pairing path only.Linked issues
None — found while pairing a fresh device on Windows.
Testing
uv run pytest -v(Python changes) — n/a, no Python touchedgolangci-lint runandgo build ./...(Go changes)Details:
go build ./...andgo vet ./...clean;gofmt -lclean.golangci-lintv2.7.1 (the version pinned inci.yml) — 0 issues.Ran out of QR codes.Tee-Objectinto a log file): codes append cleanly with no escape sequences in the log.Not unit-tested: the QR loop lives inside
main()and isn't reachable without refactoring the pairing flow. Happy to extract it into a testable function if you'd prefer coverage.Docs
README.md(if user-visible)AGENTS.md/CLAUDE.md(if contributor-visible)whatsapp-mcp-server/main.py(if MCP tools changed).env.example(if env vars changed)Risk / rollback
Low. The change is confined to terminal output during first-time pairing; no behaviour change once a session exists. The screen clear is gated on
os.ModeCharDevice, so non-TTY output is unaffected. Revert is a single-commit revert with no migration.One caveat worth naming: on a terminal that does not interpret ANSI escapes, the clear sequence would print as literal characters. In practice
ModeCharDeviceplus modern conhost/Windows Terminal VT support covers this, and the fallback is cosmetic — the QR itself still renders.