Skip to content

Server pairing: confirm in #setup before adding (Accept/Dismiss prompt) - #48

Merged
HandyS11 merged 11 commits into
developfrom
feat/server-pairing-confirmation
Jul 12, 2026
Merged

Server pairing: confirm in #setup before adding (Accept/Dismiss prompt)#48
HandyS11 merged 11 commits into
developfrom
feat/server-pairing-confirmation

Conversation

@HandyS11

Copy link
Copy Markdown
Owner

Summary

New-server FCM pairings no longer auto-run the full registration cycle. Instead, a "New server detected — Add it?" prompt (Accept/Dismiss buttons) is posted in the guild's #setup channel, mirroring the switch/alarm/storage-monitor pairing flow. Only Accept replays the registration path (persist server + credential, publish ServerRegisteredEvent → channel provisioning + Rust+ connect).

  • New server → in-memory pending (ServerPairingCoordinator, exact SwitchPairingCoordinator mirror: no DB, no expiry, lost on restart → re-pair re-prompts) + prompt in #setup via new ISetupChannelLocator (guild-global sibling of CachingChannelLocator).
  • Existing server re-pair → unchanged silent path: Facepunch-GUID backfill + standby credential upsert, no event, no prompt.
  • Accept (any guild member) → verbatim replay of the old PairingHandler server path, using the pairing owner's identity/token; prompt edited into a buttonless "Server added" embed. Dismiss → pending dropped, prompt deleted.
  • Edge handling: #setup missing → warn-log + owner DM ("run /setup, then pair again"); prompt post failure → warn-log + drop (re-pair retries); restart-stale buttons → ephemeral "pairing expired" + prompt cleanup; concurrent detections serialized by a gate (no duplicate prompts); double-click races guarded by atomic pending removal.
  • Localization: 6 new server.prompt.* keys (en + fr), catalog count guard 268 → 274.

Test plan

  • 976 passed + 1 skipped across all 17 assemblies (-maxcpucount:1), baseline was 947+1
  • New: coordinator suite (10 tests incl. deterministic concurrency + failed-post drop/retry), renderer/tail parsing (8), setup locator (3), handler split rewrite, owner-notifier DM
  • jb cleanupcode --profile=ReformatAndReorder gate clean
  • Multi-agent review: per-task spec+quality gates, final whole-branch review → 2 Important findings found & fixed (detection race 39f4577, failed-post pending black hole 300db04), verdict ready-to-merge
  • Live #setup smoke: pair new server → prompt (no channels/connect yet) → re-pair (no duplicate) → Accept → channels + connect; Dismiss path; restart-while-pending → "expired"

🤖 Generated with Claude Code

HandyS11 and others added 10 commits July 12, 2026 02:25
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ation

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…iring

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… prompts

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
DiscordChannelMessenger.EnsureAsync returns null on any post failure
(including a stale channel id from the locator's 30s TTL cache), but
HandleDetectedAsync stored the pending pairing regardless. A
null-MessageId pending permanently short-circuited every later
in-game "Pair" press into the refresh branch, so no prompt ever
existed and the endpoint was stuck until restart. Now a failed post
logs a warning and drops the pairing so re-pairing in-game retries
cleanly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 12, 2026 01:37

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

This PR changes the server-pairing flow so that newly detected servers are not immediately registered; instead, the bot posts an Accept/Dismiss prompt in the guild’s #setup channel and only proceeds with registration when accepted, aligning the behavior with other pairing coordinators.

Changes:

  • Routes new server pairings to a new ServerPairingCoordinator that posts an Accept/Dismiss prompt in #setup and replays registration only on Accept.
  • Introduces a guild-global ISetupChannelLocator/SetupChannelLocator to resolve the provisioned #setup channel row (server id null) with TTL caching.
  • Adds prompt rendering + interaction handling (component module), plus localization keys and expanded test coverage across pairing/workspace/localization.

Reviewed changes

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

Show a summary per file
File Description
tests/RustPlusBot.Localization.Tests/StringsResourceParityTests.cs Updates expected localization key count for added server prompt strings.
tests/RustPlusBot.Features.Workspace.Tests/WorkspaceRegistrationTests.cs Asserts workspace DI now registers the new ISetupChannelLocator.
tests/RustPlusBot.Features.Workspace.Tests/Locating/SetupChannelLocatorTests.cs Adds coverage for resolving/caching the global #setup channel row.
tests/RustPlusBot.Features.Pairing.Tests/ServerPairingPromptRendererTests.cs Tests prompt/confirmation rendering and component-id tail parsing.
tests/RustPlusBot.Features.Pairing.Tests/ServerPairingCoordinatorTests.cs Adds coordinator behavior tests including concurrency + failure/drop paths.
tests/RustPlusBot.Features.Pairing.Tests/PairingRegistrationTests.cs Ensures pairing DI resolves the new coordinator and related services.
tests/RustPlusBot.Features.Pairing.Tests/PairingHandlerTests.cs Updates server pairing tests for “new routes to coordinator / existing silent upsert”.
tests/RustPlusBot.Features.Pairing.Tests/Fakes/RecordingOwnerNotifier.cs Extends notifier fake to record “#setup missing” notifications.
tests/RustPlusBot.Features.Pairing.Tests/DiscordOwnerNotifierTests.cs Adds test for the new owner DM path when #setup is missing.
src/RustPlusBot.Localization/Strings.resx Adds English strings for server pairing prompt + confirmation.
src/RustPlusBot.Localization/Strings.fr.resx Adds French strings for server pairing prompt + confirmation.
src/RustPlusBot.Features.Workspace/WorkspaceServiceCollectionExtensions.cs Registers ISetupChannelLocator in workspace DI.
src/RustPlusBot.Features.Workspace/Locating/SetupChannelLocator.cs Implements guild-global #setup channel locator with TTL cache and scoped store reads.
src/RustPlusBot.Features.Workspace/Locating/ISetupChannelLocator.cs Adds public abstraction for resolving the guild-global #setup channel id.
src/RustPlusBot.Features.Pairing/RustPlusBot.Features.Pairing.csproj Adds localization project reference for prompt rendering strings.
src/RustPlusBot.Features.Pairing/Rendering/ServerPairingPromptRenderer.cs Renders the server “Add it?” prompt and post-accept confirmation embed.
src/RustPlusBot.Features.Pairing/Rendering/ServerPairingComponentIds.cs Defines Accept/Dismiss custom-id prefixes and {ip}:{port} tail parsing.
src/RustPlusBot.Features.Pairing/Posting/ISetupChannelPoster.cs Introduces abstraction for posting/editing prompts in #setup.
src/RustPlusBot.Features.Pairing/Posting/DiscordSetupChannelPoster.cs Discord-backed poster shim using the shared channel messenger.
src/RustPlusBot.Features.Pairing/PairingServiceCollectionExtensions.cs Wires new coordinator/prompt/poster into pairing DI.
src/RustPlusBot.Features.Pairing/Pairing/ServerPairingCoordinator.cs Holds pending new-server pairings and performs registration on Accept.
src/RustPlusBot.Features.Pairing/Pairing/PairingHandler.cs Splits server pairing handling: new → coordinator prompt; existing → silent upsert/backfill.
src/RustPlusBot.Features.Pairing/Pairing/IServerPairingCoordinator.cs Defines coordinator interface + accept outcome enum.
src/RustPlusBot.Features.Pairing/Notifications/IOwnerNotifier.cs Adds notifier method for “#setup missing” DM.
src/RustPlusBot.Features.Pairing/Notifications/DiscordOwnerNotifier.cs Implements the “#setup missing” owner DM message.
src/RustPlusBot.Features.Pairing/Modules/ServerPairingComponentModule.cs Handles Accept/Dismiss button interactions for the server prompt.

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

Comment on lines +56 to +62
var key = (guildId, notification.Ip, notification.Port);
if (_pending.TryGetValue(key, out var existing))
{
// Repeat in-game "Pair" press: keep the prompt, refresh the held pairing (latest token wins).
_pending[key] = new Pending(ownerUserId, notification, existing.MessageId);
return;
}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 5827525. Verified the concern reproduces: with the old refresh branch a re-pair only updated the token, so a moderated/deleted prompt left the pairing pending with no message to accept until a restart — and it contradicted the class's own "re-pair to re-prompt" doc.

The two detection branches are now unified: EnsureAsync is seeded with the known message id (null on first detection). Since DiscordChannelMessenger.EnsureAsync edits when the message exists (RenderGate skips identical re-renders, so no PATCH is spent) and reposts when the id is 404/deleted, a repeat "Pair" press self-heals a deleted prompt and tracks the new id, while never posting a duplicate — the detect-gate still serializes concurrent detections. First-post failure still drops pending so a re-pair retries (the 300db04 contract); a transient re-ensure failure with a prompt already shown keeps the live prompt/pending untouched.

Covered by a new test (Repeat_detection_reensures_prompt_so_a_deleted_message_self_heals) plus updated concurrency/refresh assertions. 977 passed + 1 skipped across all assemblies; format gate clean.

…leted message self-heals

A re-detected pairing refreshed the held token but returned without
re-ensuring the prompt. If the original #setup prompt was deleted, every
later re-pair kept the pairing pending yet never recreated the prompt,
blocking acceptance until a restart cleared pending state.

Seed EnsureAsync with the known message id (null on first detection) so an
edit self-heals a deleted prompt (repost) and a repeat press never posts a
duplicate; the gate still serializes detections. First-post failure still
drops pending (retry on re-pair); a transient re-ensure failure keeps the
live prompt/pending untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@HandyS11
HandyS11 merged commit 0153279 into develop Jul 12, 2026
3 checks passed
@HandyS11
HandyS11 deleted the feat/server-pairing-confirmation branch July 12, 2026 01:59
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.

2 participants