Skip to content

fix(bridge): redraw the pairing QR code on every rotation - #213

Open
LINTEL-1 wants to merge 162 commits into
verygoodplugins:mainfrom
LINTEL-1:fix/qr-code-refresh
Open

fix(bridge): redraw the pairing QR code on every rotation#213
LINTEL-1 wants to merge 162 commits into
verygoodplugins:mainfrom
LINTEL-1:fix/qr-code-refresh

Conversation

@LINTEL-1

Copy link
Copy Markdown

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 qrCodeShown boolean and drew only the first one. The expired QR stays on screen looking identical to a live one, so pairing silently fails until Ran out of QR codes scrolls past — with no visible indication of why.

  • Redraw on every code event, labelled with a counter and generation time.
  • Clear the screen first only when stdout is a terminal, so pipes and log files don't get escape sequences. Detected with os.Stdout.Stat() / os.ModeCharDevice — stdlib only, no new dependency.
  • Add a README troubleshooting entry for the "QR renders fine but isn't accepted" case.

Type of change

  • fix — bug fix
  • feat — new feature
  • chore / docs / ci / refactor / test / perf
  • Breaking change (! in commit, or BREAKING CHANGE: in body)

Scope check

  • This change fits the ROADMAP.md "in scope" list, or I've opened an issue first to discuss
  • PR is focused on one concern (split if not)
  • PR is ≤ ~300 LOC, or justified in the description

~24 lines changed in whatsapp-bridge/main.go, 3 in README.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

  • Added or updated tests
  • Ran uv run pytest -v (Python changes) — n/a, no Python touched
  • Ran golangci-lint run and go build ./... (Go changes)
  • Manually exercised the affected code path

Details:

  • go build ./... and go vet ./... clean; gofmt -l clean.
  • golangci-lint v2.7.1 (the version pinned in ci.yml) — 0 issues.
  • Paired a fresh device on Windows: 8 codes rendered in sequence, each replacing the last; scanning the newest succeeded. Before this change the first code stayed on screen until Ran out of QR codes.
  • Verified against non-TTY stdout (piped through Tee-Object into 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

  • Updated README.md (if user-visible)
  • Updated AGENTS.md / CLAUDE.md (if contributor-visible)
  • Updated tool descriptions in whatsapp-mcp-server/main.py (if MCP tools changed)
  • Updated .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 ModeCharDevice plus modern conhost/Windows Terminal VT support covers this, and the fallback is cosmetic — the QR itself still renders.

lharries and others added 30 commits March 30, 2025 14:49
- Save the names of group chats so you can query them
- Ability to send messages to group chats by using the jid
clarify the path in json setting.
…udio

feat: send files and audio messages
…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>
evandrodevbr and others added 28 commits June 26, 2026 03:32
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.