Skip to content

feat(teams): Microsoft Teams adapter - #18

Merged
lao merged 28 commits into
mainfrom
worktree-teams-adapter
Jul 1, 2026
Merged

feat(teams): Microsoft Teams adapter#18
lao merged 28 commits into
mainfrom
worktree-teams-adapter

Conversation

@lao

@lao lao commented Jun 30, 2026

Copy link
Copy Markdown
Owner

Summary

Adds a Microsoft Teams (Azure Bot Framework) adapter, following the same facade-over-internal pattern as the existing platforms. Like WhatsApp, Teams is a webhook adapter: it runs its own HTTP server for inbound Activities and replies over the Bot Connector REST API.

What's included

  • internal/teams — the core.Adapter implementation: webhook server, JWT inbound auth, conversation→serviceUrl routing, Bot Connector token minting, and reply send.
  • teams/ — thin public package (teams.New, teams.RawMessage, Config/Message types) importing no platform SDK.
  • core.TeamsBotType + String(), re-exported as botbooter.TeamsBotType.
  • Example platform selector entry and CLAUDE.md docs.

Security

  • Every inbound request is authenticated against the Bot Connector JWKS: RS256 signature, audience == AppID, issuer, expiry, and a serviceurl claim bound to the Activity's serviceUrl.
  • Outbound replies are restricted to an allowlist of Bot Framework hosts to prevent SSRF; the broad *.trafficmanager.net namespace is deliberately not allowlisted.
  • JWKS refresh is rate-limited to defend against unknown-kid floods; the conversation map is bounded with FIFO eviction.
  • Operator must terminate TLS at a trusted reverse proxy: the Activity body is channel-trusted, not individually signed, and there is no replay tracking.

Isolation

  • golang-jwt/jwt/v5 is the adapter's only third-party dependency (a crypto lib, not a platform SDK). The per-package imports_test.go guard and the module-level isolation_deps_test.go confirm it stays confined to the teams closure and that a Teams-only binary pulls in no other platform SDK.

Testing

gofmt, go vet, and go test ./... all pass.

Summary by CodeRabbit

  • New Features

    • Added Microsoft Teams as a supported bot platform, including Teams-specific message parsing, outbound sending, and attachment decoding.
    • Introduced public Teams wrapper APIs (new bot constructor and Teams message detection) and a Teams bot type identifier.
  • Bug Fixes

    • Improved webhook/server shutdown handling to ensure in-flight dispatches complete more reliably.
  • Documentation

    • Updated README and platform docs to cover Microsoft Teams setup, credentials, and completion status.
  • Tests

    • Added extensive Teams adapter, auth/JWKS, sending, attachments, and wrapper coverage, plus dependency isolation checks.

lao added 6 commits June 30, 2026 16:57
Implements core.Adapter for the Azure Bot Framework. Inbound Activities
arrive over a webhook HTTP server; replies go out through the Bot
Connector REST API. Every inbound request is authenticated against the
Bot Connector JWKS (RS256 signature, audience, issuer, and a serviceurl
claim bound to the Activity), and outbound replies are restricted to
allowlisted Bot Framework hosts to prevent SSRF. Adds golang-jwt/jwt/v5
for token verification.
@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 399c1485-08a9-48d5-8a35-790a480d64d1

📥 Commits

Reviewing files that changed from the base of the PR and between 0cf1049 and 8e770ec.

📒 Files selected for processing (12)
  • _docs/platforms.md
  • internal/teams/attachments_test.go
  • internal/teams/auth.go
  • internal/teams/auth_test.go
  • internal/teams/http_test.go
  • internal/teams/message_test.go
  • internal/teams/send_test.go
  • internal/teams/server.go
  • internal/teams/server_test.go
  • internal/teams/teams_test.go
  • internal/whatsapp/whatsapp.go
  • internal/whatsapp/whatsapp_test.go

📝 Walkthrough

Walkthrough

This PR adds a Microsoft Teams adapter with internal auth, message parsing, attachment mapping, outbound send, and webhook lifecycle support, plus a public Teams wrapper, example wiring, docs, and dependency isolation updates. It also changes WhatsApp shutdown handling to use a detached dispatch context and force-cancel drains that exceed their budget.

Changes

Microsoft Teams Platform Adapter

Layer / File(s) Summary
Core Teams type wiring
internal/core/core.go, internal/core/core_test.go
Adds TeamsBotType, maps it to "teams", and updates attachment URL documentation.
Teams config and HTTP helpers
internal/teams/teams.go, internal/teams/http.go, go.mod
Defines Teams configuration and adapter state, constructor normalization, and shared JSON fetch/decode helpers, with jwt/v5 added as a dependency.
Inbound Teams auth and JWKS
internal/teams/auth.go, internal/teams/auth_test.go
Implements JWT validation, service URL and channel endorsement checks, JWKS caching/refresh, and supporting host/token comparison helpers with tests.
Teams message and attachment mapping
internal/teams/message.go, internal/teams/attachments.go, internal/teams/message_test.go, internal/teams/attachments_test.go
Adds Teams message parsing, mention stripping, attachment conversion, and tests for message and attachment mapping.
Teams webhook server and dispatch
internal/teams/server.go, internal/teams/server_test.go
Implements webhook handling, conversation routing, dispatch draining, and server lifecycle behavior, with coverage for auth, routing, shutdown, and record-keeping.
Teams send and token cache
internal/teams/send.go, internal/teams/send_test.go
Implements outbound activity sending and cached client-credentials token acquisition, with request and refresh tests.
Public Teams wrapper
teams/teams.go, teams/wrapper_test.go, teams/imports_test.go
Re-exports the Teams API through the public package and adds wrapper and import-guard tests.
Facade and isolation wiring
botbooter.go, botbooter_test.go, isolation_deps_test.go
Adds the top-level Teams bot type alias and test coverage, and updates dependency isolation checks for jwt/v5.
Examples and docs
_examples/v1/platforms.go, README.md, CLAUDE.md, _docs/platforms.md
Adds Teams to the example launcher and updates docs to list Teams support and setup details.

Estimated code review effort: 4 (Complex) | ~75 minutes

WhatsApp Dispatch Context and Drain Refactor

Layer / File(s) Summary
Detached dispatch context and drain handling
internal/whatsapp/whatsapp.go, internal/whatsapp/whatsapp_test.go
Adds detached cancel state, dispatches using a per-connection context, updates shutdown drain behavior, and adds tests for drain timeout and cancellation paths.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Teams as Microsoft Teams
  participant Server as handleMessages
  participant Auth as validateInbound
  participant Store as recordConversation
  participant Dispatch as deps.Dispatch
  participant Send as adapter.Send

  Teams->>Server: POST activity + Bearer token
  Server->>Auth: validateInbound(activity, token)
  Auth-->>Server: claims and serviceUrl accepted
  Server->>Store: recordConversation(channelID, serviceURL, bot)
  Server->>Dispatch: dispatch on detached context
  Dispatch->>Send: Send(channelID, replyText)
  Send->>Teams: POST /v3/conversations/{id}/activities
Loading

Possibly related PRs

  • lao/botbooter#1: Extends the same bot-type and adapter lifecycle model used by the shared platform framework.
  • lao/botbooter#2: Touches the same internal/core and facade wiring pattern for adding a platform type.
  • lao/botbooter#13: Uses the same SDK-isolation and dependency-guard structure updated here for the Teams package.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.83% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding a Microsoft Teams adapter.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-teams-adapter

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov-commenter

codecov-commenter commented Jun 30, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.07895% with 27 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/teams/auth.go 89.26% 8 Missing and 8 partials ⚠️
internal/whatsapp/whatsapp.go 77.77% 4 Missing and 2 partials ⚠️
internal/teams/server.go 95.41% 3 Missing and 2 partials ⚠️

📢 Thoughts on this report? Let us know!

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds first-class Microsoft Teams (Azure Bot Framework) support to botbooter via a new internal adapter plus a thin public wrapper package, maintaining the existing “public facade over internal adapter” architecture and isolation guarantees.

Changes:

  • Introduces internal/teams adapter implementing core.Adapter (webhook server, inbound JWT validation via Bot Connector JWKS, outbound reply send, attachments mapping).
  • Adds public teams/ wrapper package (New, RawMessage, and type aliases) and updates BotType plumbing (TeamsBotType + String() + re-export).
  • Extends isolation/import-guard tests, examples, and developer docs to include Teams and ensure github.com/golang-jwt/jwt/v5 remains Teams-only.

Reviewed changes

Copilot reviewed 13 out of 14 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
teams/wrapper_test.go Verifies the public Teams facade (New, RawMessage) behavior.
teams/teams.go Public Teams wrapper: constructor, raw-message accessor, and type/error re-exports.
teams/imports_test.go Enforces that the public Teams wrapper imports no platform SDKs directly.
isolation_deps_test.go Extends transitive dependency isolation checks to include Teams and jwt/v5 confinement.
internal/teams/teams.go New Teams adapter: webhook server, JWT/JWKS auth, outbound send, routing, attachments.
internal/teams/teams_test.go Comprehensive unit tests for Teams adapter behavior and security invariants.
internal/core/core.go Adds TeamsBotType and String() mapping.
internal/core/core_test.go Tests TeamsBotType.String().
go.mod Adds github.com/golang-jwt/jwt/v5 dependency.
go.sum Adds jwt/v5 checksums.
CLAUDE.md Updates architecture/docs to include Teams adapter and isolation expectations.
botbooter.go Re-exports TeamsBotType and updates package docs to mention Teams.
botbooter_test.go Exercises Teams raw accessor and teams.New via the root integration tests.
_examples/v1/platforms.go Adds Teams option to the example platform selector and its error message.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/teams/teams.go Outdated
Comment thread internal/teams/teams.go Outdated
lao added 15 commits July 1, 2026 10:01
…nostics

Send now records the bot and user accounts alongside the serviceUrl and
sets the reply's from (required by Bot Connector) and recipient fields.
Raise the JWKS/OpenID/token read cap to 4 MiB so the real ~1 MB Bot
Connector JWKS decodes instead of truncating to "unexpected EOF". Pin
jwks_uri to the metadata scheme and host (not just host) to block a
cleartext downgrade. Wrap 401 rejection reasons and log them, escaping
attacker-controlled aud with %q.

Also rename the example env var to TEAMS_APP_TENANT_ID and document the
Teams adapter in the README and platforms guide.
The webhook adapters passed the run context into deps.Dispatch, but core
cancels that context before Disconnect drains in-flight dispatch — so a
handler replying via SendMessageContext(ctx) failed with "context canceled"
mid-drain and the acked message was dropped. Dispatch on
context.WithoutCancel so an acked reply can finish within the drain window;
apply the same fix to Teams and WhatsApp.

Teams: decode the Activity's attachments once at parse time onto Message
instead of re-unmarshaling the (up to 1 MiB) Raw body on every Attachments
call, matching the WhatsApp adapter's parse-once shape.

CLAUDE.md: document that each adapter deliberately owns its own setup and
that shared correctness fixes must be swept across the sibling adapters.
Disconnect shared one 5s context across srv.Shutdown and drainDispatch,
so a slow shutdown could burn the budget and leave the drain ~0s,
abandoning an already-acked in-flight dispatch mid-reply. Give each its
own 5s deadline, mirroring the Teams adapter.

Adds a hermetic drain-cancel test and an env-gated end-to-end
slow-shutdown regression (BOTBOOTER_WHATSAPP_DRAIN_TIMING_TEST).
- Force a JWKS refresh once the cached key set ages past 24h (jwksMaxAge)
  so a signing key Microsoft has retired stops being trusted, rather than
  lingering until an unknown-kid token flushes the map. A refresh that
  fails falls back to the cached key so a transient JWKS outage does not
  reject otherwise-valid tokens.
- Drop bot-authored Activities by from.role == "bot" instead of the
  never-firing from.id == recipient.id check, matching how the Slack,
  Discord and Telegram adapters ignore all bots to avoid reply loops.
- Consolidate the non-2xx / JSON-decode / keep-alive-drain tail of the
  outbound calls into a shared decodeJSON helper (Send, accessToken,
  getJSON), capping decodes at maxMetaBytes.
Break internal/teams/teams.go (1094 lines) into seven same-package files by
responsibility: teams.go (front door), server.go (webhook lifecycle), auth.go
(JWT/JWKS + SSRF), send.go (reply + token), message.go (parsing), attachments.go
(mapping) and http.go (shared plumbing). Declarations moved verbatim, then
comments trimmed to the essential rationale. No behavior change.
@lao
lao requested a review from Copilot July 1, 2026 16:46
@lao
lao marked this pull request as ready for review July 1, 2026 16:46

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 23 out of 24 changed files in this pull request and generated 2 comments.

Comment thread internal/teams/auth.go Outdated
Comment thread _docs/platforms.md Outdated
lao added 2 commits July 1, 2026 18:54
 the line is missing punctuation after the bold heading, making the numbered step read awkwardly.
Using %q with gotAud (an untyped claim value) can yield fmt formatting errors like %!q(...) when aud is not a string (it can be an array per JWT spec). This makes the returned error/log output misleading and defeats the intended escaping.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (2)
internal/teams/send.go (1)

89-134: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Serialize token refreshes after a cache miss.

The cache check releases a.mu before minting, so concurrent Send calls with an empty/expiring token can all hit the client-credentials endpoint. Gate refresh with a token-specific mutex and re-check the cache after acquiring it.

♻️ Suggested shape
 func (a *adapter) accessToken(ctx context.Context) (string, error) {
 	a.mu.Lock()
 	if a.token.value != "" && time.Until(a.token.expiry) > tokenRefreshSkew {
 		v := a.token.value
 		a.mu.Unlock()
 		return v, nil
 	}
 	a.mu.Unlock()
 
+	a.tokenMu.Lock()
+	defer a.tokenMu.Unlock()
+
+	a.mu.Lock()
+	if a.token.value != "" && time.Until(a.token.expiry) > tokenRefreshSkew {
+		v := a.token.value
+		a.mu.Unlock()
+		return v, nil
+	}
+	a.mu.Unlock()
+
 	form := url.Values{

Add tokenMu sync.Mutex to the adapter struct.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/teams/send.go` around lines 89 - 134, The accessToken method in
adapter currently drops a.mu before minting, so concurrent Send calls can all
refresh the Teams token at once after a cache miss or near expiry. Add a
dedicated tokenMu field to adapter and serialize the refresh path in accessToken
by locking it before contacting the token endpoint. After acquiring tokenMu,
re-check the cached token state under a.mu guard before creating the request so
only one goroutine mints a new token while others reuse the refreshed value.
internal/whatsapp/whatsapp_test.go (1)

519-534: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Keep dispatch blocked so this test exercises drain-before-cancel ordering.

The Dispatch callback returns immediately, so inflight may already be zero before Line 533 calls Disconnect. This test would still pass if Disconnect canceled before draining. Hold the callback until the test releases it, then assert the context stays live while Disconnect is waiting.

Test-shape adjustment
 	gotCtx := make(chan context.Context, 1)
-	deps := core.AdapterDeps{Dispatch: func(c context.Context, _ *core.Message) { gotCtx <- c }}
+	releaseDispatch := make(chan struct{})
+	deps := core.AdapterDeps{Dispatch: func(c context.Context, _ *core.Message) {
+		gotCtx <- c
+		<-releaseDispatch
+	}}
@@
 	asserts.NoError(t, c.Err(), "dispatch ctx live before Disconnect")
-	asserts.NoError(t, a.Disconnect(), "Disconnect")
+	done := make(chan error, 1)
+	go func() { done <- a.Disconnect() }()
+	select {
+	case <-c.Done():
+		t.Fatal("dispatch ctx canceled before drain completed")
+	case <-time.After(50 * time.Millisecond):
+	}
+	close(releaseDispatch)
+	asserts.NoError(t, <-done, "Disconnect")
 	asserts.ErrorIs(t, c.Err(), context.Canceled, "dispatch ctx canceled after drain")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/whatsapp/whatsapp_test.go` around lines 519 - 534, The test around
handleWebhook and Dispatch does not keep the in-flight request blocked, so it
can miss the intended drain-before-cancel ordering. Update the test to have the
core.AdapterDeps.Dispatch callback wait on a release signal before returning,
then call a.Disconnect while Dispatch is still blocked and only release it
afterward. Keep the assertions on the captured context’s liveness and
cancellation to verify that c.Err() stays nil until Disconnect drains inflight,
using the existing gotCtx, dispatchCtx, and a.Disconnect flow.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@_docs/platforms.md`:
- Around line 238-349: Normalize the Teams documentation heading hierarchy in
the markdown section by changing the new Teams subheads to the next proper level
under the existing section so the outline does not skip heading depths and
markdownlint MD001 passes; update the headings in the Teams walkthrough
consistently (including the Step 1–Step 5 titles) and keep the surrounding
content structure intact.

In `@internal/teams/message.go`:
- Around line 106-115: Only remove a leading self-mention in the message
normalization path, rather than stripping every mention entity addressed to the
bot. Update the logic in message normalization around the text/entity handling
in the message parsing flow so it checks the first mention entity at the start
of the content before calling strings.Replace, and leave any later self-mentions
intact. Keep the existing anchored-command matching behavior while preserving
user payload in messages like echo <at>Bot</at>.

In `@internal/teams/server.go`:
- Around line 164-188: Disconnect should surface a shutdown failure when the
drain times out and in-flight dispatches are force-canceled, instead of
returning nil after a successful srv.Shutdown. Update the Disconnect flow around
drainCtx, a.drainDispatch, and the in-flight check so that a drain timeout
sets/returns an error while still preserving the existing cleanup of a.srv,
a.detachedCancel, and cancelDispatch. Keep the normal nil return only when the
drain completes without dropping acked dispatches.

In `@internal/whatsapp/whatsapp.go`:
- Around line 257-283: The adapter-wide inflight tracker is being shared across
reconnects, so a new connection can interfere with the prior Disconnect drain
state. Update the connection lifecycle in adapter.Connect/Disconnect to capture
a per-connection inflight tracker alongside srv and detachedCancel, and have the
dispatch handler increment/decrement that tracker instead of a.inflight. Make
sure drainDispatch and any drain logging/reporting use the tracker tied to the
srv being shut down, not the current adapter instance state.
- Around line 285-310: Update Disconnect in whatsapp.go so a drain timeout is
surfaced to callers instead of returning nil after force-canceling in-flight
dispatches. Use the existing inflight check and cancelDispatch path to set or
wrap an error when a deadline is reached, and ensure the final return reflects
that timeout even if srv.Shutdown itself succeeded. Keep the cleanup of a.srv
and a.detachedCancel unchanged, but make the timeout state visible through
Disconnect’s returned error.

---

Nitpick comments:
In `@internal/teams/send.go`:
- Around line 89-134: The accessToken method in adapter currently drops a.mu
before minting, so concurrent Send calls can all refresh the Teams token at once
after a cache miss or near expiry. Add a dedicated tokenMu field to adapter and
serialize the refresh path in accessToken by locking it before contacting the
token endpoint. After acquiring tokenMu, re-check the cached token state under
a.mu guard before creating the request so only one goroutine mints a new token
while others reuse the refreshed value.

In `@internal/whatsapp/whatsapp_test.go`:
- Around line 519-534: The test around handleWebhook and Dispatch does not keep
the in-flight request blocked, so it can miss the intended drain-before-cancel
ordering. Update the test to have the core.AdapterDeps.Dispatch callback wait on
a release signal before returning, then call a.Disconnect while Dispatch is
still blocked and only release it afterward. Keep the assertions on the captured
context’s liveness and cancellation to verify that c.Err() stays nil until
Disconnect drains inflight, using the existing gotCtx, dispatchCtx, and
a.Disconnect flow.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5c80cf17-876e-46b0-b420-c4d1942564d6

📥 Commits

Reviewing files that changed from the base of the PR and between a9a6046 and 0cf1049.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (23)
  • CLAUDE.md
  • README.md
  • _docs/platforms.md
  • _examples/v1/platforms.go
  • botbooter.go
  • botbooter_test.go
  • go.mod
  • internal/core/core.go
  • internal/core/core_test.go
  • internal/teams/attachments.go
  • internal/teams/auth.go
  • internal/teams/http.go
  • internal/teams/message.go
  • internal/teams/send.go
  • internal/teams/server.go
  • internal/teams/teams.go
  • internal/teams/teams_test.go
  • internal/whatsapp/whatsapp.go
  • internal/whatsapp/whatsapp_test.go
  • isolation_deps_test.go
  • teams/imports_test.go
  • teams/teams.go
  • teams/wrapper_test.go

Comment thread _docs/platforms.md
Comment thread internal/teams/message.go
Comment thread internal/teams/server.go Outdated
Comment thread internal/whatsapp/whatsapp.go
Comment thread internal/whatsapp/whatsapp.go Outdated
Mirror the source split: move the 72 tests from the single 1483-line
teams_test.go into server_test.go, auth_test.go, send_test.go, message_test.go,
attachments_test.go and http_test.go, keeping shared fixtures in teams_test.go.
Identical test set, no behavior change.
@lao
lao force-pushed the worktree-teams-adapter branch from 684014a to 528070a Compare July 1, 2026 17:07
Teams and WhatsApp Disconnect returned nil after a successful Shutdown even
when the drain deadline expired with dispatches still in-flight, hiding
force-canceled acked messages as a clean shutdown. Surface a drain-timeout
error instead. Adds env-gated ~5s regression tests to both adapters.
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.

3 participants