Skip to content

Notify users via management-room notice when Discord invalidates their token#32

Merged
Just-Insane merged 8 commits into
mainfrom
fix/token-invalidation-notice
Jul 2, 2026
Merged

Notify users via management-room notice when Discord invalidates their token#32
Just-Insane merged 8 commits into
mainfrom
fix/token-invalidation-notice

Conversation

@Just-Insane

Copy link
Copy Markdown

Summary

Closes #30.

The bridge already detects bad-credentials events (invalidAuthHandler, handlePossible40002) and pushes a BridgeState update, but BridgeState is a webhook-only signal most Matrix clients never render — users only find out bridging stopped once they notice messages have stopped syncing, sometimes hours or days later.

  • Adds an m.notice to the user's management room alongside the existing BridgeState push, gated by a new token_invalidation_notices config option (default on).
  • Dedupes on a single sentBadCredentialsNotice flag shared by both trigger points, since handlePossible40002 can be hit repeatedly by retried REST calls while the account stays broken. The flag releases on send failure so a later event can retry, and resets on the next successful Login.
  • disconnectedHandler (transient disconnects) is untouched, so a plain reconnect never notices — verified by diff, not just by inspection.
  • A code-review pass caught that the message told users to run the prefixed login command (`!discord login`), but the notice always lands in user.ManagementRoom, where the bridge's own help text says commands never need the prefix — fixed before merge.

Includes the investigation writeup (docs/specs/2026-07-02-token-invalidation-notice.md) that led here, including a separate, more severe finding (a live reconnect storm) tracked in a companion PR rather than bundled into this one, since the code doesn't overlap.

Test plan

  • go build ./... / go vet ./... / gofmt — clean
  • go test ./... — clean
  • Unit tests: gating decision (badCredentialsNoticeSkipReason) table-driven across config-toggle × management-room-present; dedup flag lifecycle.
  • Coverage gap, called out explicitly: no test double exists yet for the Matrix intent (user.bridge.Bot), so the actual SendMessageEvent dispatch isn't covered by an automated test — recommend a manual end-to-end check against a disposable test account (force a 4004/40002, confirm the notice lands and a re-login doesn't duplicate it) before wide rollout.

🤖 Generated with Claude Code

Just-Insane and others added 2 commits July 2, 2026 12:50
…r token

The bridge already detects bad-credentials events (invalidAuthHandler,
handlePossible40002) and pushes a BridgeState update, but BridgeState
is a webhook-only signal that most Matrix clients never render --
users only find out bridging stopped once they notice messages have
stopped syncing, sometimes hours or days later (see
docs/specs/2026-07-02-token-invalidation-notice.md, filed as #30).

Add an m.notice to the user's management room alongside the existing
BridgeState push, gated by a new token_invalidation_notices config
option (default on). Dedupes on a single sentBadCredentialsNotice flag
shared by both trigger points, since handlePossible40002 can be hit
repeatedly by retried REST calls while the account stays broken; the
flag releases on send failure so a later event can retry, and resets
on the next successful Login. disconnectedHandler (transient
disconnects) is untouched, so plain reconnects never notice.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Found during code review: the notice always posts to
user.ManagementRoom, where commands never require the !discord
prefix (per the bridge's own help text) -- the message was telling
users to type the prefixed form, which would confuse anyone copying
it literally. Also matches the existing ⚠ escape convention
used for the warning glyph elsewhere in the file instead of a literal
unicode character in source.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 2, 2026 17:01
Comment thread user.go Outdated

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 a user-visible Matrix management-room notification when Discord invalidates a user token, complementing the existing BridgeState bad-credentials signal that many Matrix clients don’t surface.

Changes:

  • Send an m.notice into the user’s management room on invalidAuthHandler (4004) and handlePossible40002 (40002), with per-invalidation deduping via sentBadCredentialsNotice.
  • Introduce bridge.token_invalidation_notices config toggle (documented in example-config.yaml) to enable/disable these notices.
  • Add unit tests for gating logic and dedup lifecycle; include a detailed investigation/spec writeup.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
user.go Adds notice-sending logic + per-invalidation dedup flag, wired into existing bad-credentials trigger points.
user_notice_test.go Adds tests for notice gating and dedup flag lifecycle behavior.
example-config.yaml Documents new token_invalidation_notices option (default true).
docs/specs/2026-07-02-token-invalidation-notice.md Adds investigation/spec writeup motivating the change and describing scope.
config/bridge.go Adds the new config field to the bridge config struct.

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

Comment thread config/bridge.go

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 245684ad3a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread user.go Outdated
Comment thread user.go Outdated
Follow-up on code-review suggestions that were flagged non-blocking
but worth acting on:

- Added badCredentialsNoticeRetryCooldown (1 min) so a burst of
  handlePossible40002 calls during a sustained homeserver outage
  can't turn into a real send attempt on every single failed call.
  The dedup/cooldown decision is extracted into
  badCredentialsNoticeAttemptDecision, a pure function taking the
  current time as a parameter so the cooldown interaction is
  unit-testable without real timers.
- Extracted a minimal noticeSender interface (matching
  *appservice.IntentAPI's SendMessageEvent signature) and moved the
  actual content-building/dispatch into
  sendBadCredentialsNoticeContent, which now has a real test double
  (fakeNoticeSender) exercising room targeting, event type, message
  body, and error propagation -- closing the "no integration test for
  actual dispatch" gap flagged in review, without needing a live
  Matrix homeserver or a bigger interface change to DiscordBridge.Bot
  itself.
- Added an inline comment on invalidAuthHandler's early unlock
  explaining why (avoids deadlocking with sendBadCredentialsNotice,
  and brings it in line with handlePossible40002's existing locking),
  which was previously only explained in a commit message.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@Just-Insane

Copy link
Copy Markdown
Author

Follow-up push addressing the remaining non-blocking review suggestions from the original pass:

  • Added a 1-minute retry cooldown so a burst of handlePossible40002 calls during a sustained homeserver outage can't turn into a real send attempt on every single failed call. Extracted as a pure, time-injectable badCredentialsNoticeAttemptDecision function so this is unit-tested without real timers.
  • Extracted a minimal noticeSender interface + sendBadCredentialsNoticeContent, with a real fake test double covering room targeting, event type, message body, and error propagation — closes the "no integration test for actual dispatch" gap called out in the original test plan, without needing a live homeserver or a broader interface change to DiscordBridge.Bot.
  • Added an inline comment explaining invalidAuthHandler's early-unlock (previously only in the commit message).

go build, go test ./..., go test ./... -race, go vet, and gofmt all clean.

Just-Insane and others added 2 commits July 2, 2026 13:27
…overy step

Three issues raised by automated review (Copilot, Codex, Sentry), all
verified before fixing:

- config/upgrade.go didn't copy token_invalidation_notices, so a config
  upgrade would silently discard a user's customized value back to
  whatever example-config.yaml currently defaults to. Added the
  missing helper.Copy call, matching message_error_notices right above
  it.
- sendBadCredentialsNotice read user.ManagementRoom twice with no lock
  shared with SetManagementRoom, so a concurrent room change between
  the gating check and the send could route the notice to the wrong
  room. Now read once into a local and reused for both.
- Verified empirically (commands.go's fnLoginToken/fnLoginQR both
  check IsLoggedIn() and refuse with "You're already logged in"):
  handlePossible40002 never clears DiscordToken, so the notice's old
  universal "Run `login`" instruction was actively wrong for that
  path -- the user is still logged in, running login would just get
  refused. sendBadCredentialsNotice now takes an explicit recovery
  string per caller: invalidAuthHandler keeps "Run `login`"
  (Logout does clear the token there), handlePossible40002 gets
  "resolve it on Discord, no need to log in again".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The spec documented real production hostnames, incident timestamps,
and account-specific session details from the investigation behind
this PR. Per repo owner's decision, moving this out of git entirely
rather than redacting in place -- it's preserved outside the repo for
anyone who needs the full investigation trail.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@Just-Insane Just-Insane enabled auto-merge July 2, 2026 17:34
Comment thread user.go
Just-Insane and others added 3 commits July 2, 2026 13:39
sentBadCredentialsNotice only reset on the next Login, but
handlePossible40002 never logs the user out or clears DiscordToken --
so a session can keep running for a long time after one successful
notice with no further Login to reset the flag. Any later
bad-credentials event in that same session (the same still-broken
account, or an entirely distinct new issue, including a real 4004)
would have been silently suppressed for the rest of the session.

Disagreed with the suggested fix (reset in readyHandler): a gateway
reconnect has nothing to do with whether the underlying account issue
actually resolved, especially for the REST-triggered 40002 path, and
resetting there risked reintroducing notice spam on ordinary
reconnects while a problem is still unresolved.

Instead, replaced the permanent "already sent" block with a resend
cooldown (1 hour) parallel to the existing failure-retry cooldown (1
minute) -- alreadySent now means "the last attempt succeeded", gating
a longer cooldown rather than blocking forever. A persisting or
recurring issue gets a fresh reminder well within a day instead of
never again. Added a direct regression test for the exact scenario
Sentry flagged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
PR #31 (the backoff-reset fix) merged into main while this PR was
still in review, bringing user_reconnect_test.go's newTestUser helper
into the same package as this PR's own copy in user_notice_test.go --
CI caught the redeclaration once both were combined. Removed the
duplicate here; the shared helper now lives in user_reconnect_test.go
only.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@Just-Insane Just-Insane merged commit c5e7124 into main Jul 2, 2026
5 checks passed

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1a13ee79d9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread user.go
}

user.bridgeStateLock.Lock()
attempt, skipReason := badCredentialsNoticeAttemptDecision(user.sentBadCredentialsNotice, user.lastBadCredentialsNoticeAttempt, time.Now())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Allow different bad-credentials notices through cooldown

When a 40002 notice is sent successfully, this decision treats any later bad-credentials notice as already sent for the next hour, regardless of errorCode or recovery text. If Discord then closes the gateway with 4004 during that window, the invalidAuthHandler notice is skipped even though that path logs the user out and needs the opposite instruction (login instead of “do not run login”), leaving the user with only stale 40002 advice after their token has been cleared. Key the cooldown by the notice type/recovery, or let the logout/4004 notice override the 40002 cooldown.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Confirmed, good catch — this landed after #32 had already merged, so fixed as a follow-up: #33. badCredentialsNoticeAttemptDecision now takes both the incoming and last-notified error codes and only applies either cooldown when they match.

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.

Bridge can sometimes have it's token invalidated

2 participants