Skip to content

Subsystem 1b-i: credential intake & FCM pairing → server registration - #5

Merged
HandyS11 merged 15 commits into
developfrom
feat/credentials-fcm-pairing
Jun 15, 2026
Merged

Subsystem 1b-i: credential intake & FCM pairing → server registration#5
HandyS11 merged 15 commits into
developfrom
feat/credentials-fcm-pairing

Conversation

@HandyS11

Copy link
Copy Markdown
Owner

Summary

Subsystem 1b-i: users connect their Rust+ FCM credentials and the bot auto-registers servers when they pair in-game. No live Rust+ socket — opening sockets, hot-swap, auto-failover and #info live status are deferred to 1b-ii.

  • Credential intake — a "Connect account" button in #setup opens an ephemeral modal; the pasted FCM credentials JSON is validated, protected at rest, and stored as an FcmRegistration.
  • Per-user FCM listenerPairingSupervisor (singleton) runs one RustPlusApi.Fcm listener per active registration (behind an IPairingSource seam), reports the first connect outcome for immediate modal feedback, retries a timed-out connect with capped backoff, and on rejection marks the registration Expired + DMs the owner.
  • Auto server registration — on a server pairing, PairingHandler resolve-or-creates the RustServer by (GuildId, Ip, Port), upserts a per-owner PlayerCredential (first owner → Active, others → Standby — the credential pool), and fires the real ServerRegisteredEvent, which the existing 1a reconciler consumes to provision the server's channels.
  • Data model — split the bloated credential row into FcmRegistration (per guild+user) + a slimmed PlayerCredential (per guild+server+owner); migration PairingCredentials drops the old FCM column, adds the new table, unique indexes on (GuildId,Ip,Port) / (GuildId,RustServerId,OwnerUserId) / (GuildId,OwnerUserId), and a cascade FK so removing a server cleans its credentials.

New RustPlusBot.Features.Pairing project (source seam + supervisor + handler + notifier + hosted service + modal); the real RustPlusApi.Fcm adapter is the only piece without unit tests (integration shim, faked everywhere else).

Notable decisions (documented deviations from the spec)

  • ICredentialStore.SetStatusAsync is deferred to 1b-ii (no caller in 1b-i, and adding a Domain.CredentialStatus parameter would force ICredentialStore out of the dependency-free Abstractions project). The Invalid → Standby reset on re-pairing lives inside CredentialStore.UpsertFromPairingAsync instead.
  • The supervisor retries only the initial connect on timeout; mid-session reconnection is left to 1b-ii.

Test Plan

  • dotnet build — 0 warnings / 0 errors (strict analyzers)
  • dotnet test — 81 passed, 0 failed (Abstractions 4, Workspace 30, Pairing 18, Persistence 29)
  • dotnet jb cleanupcode --profile=ReformatAndReorder clean (matches CI format gate)
  • EF migration PairingCredentials has no model drift (has-pending-model-changes clean)
  • Manual: connect FCM creds via #setup modal, pair a server in-game, confirm the server's channels are provisioned and a PlayerCredential is stored

🤖 Generated with Claude Code

HandyS11 and others added 14 commits June 15, 2026 11:02
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implements IPairingHandler / PairingHandler: resolves-or-creates the
server via IServerService, upserts the credential via ICredentialStore,
and publishes ServerRegisteredEvent only when the server is newly
created. Entity notifications are silently ignored.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 15, 2026 10:28

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 implements Subsystem 1b-i by adding credential intake + per-user FCM pairing listeners and using server pairings to auto-register Rust servers and upsert per-owner player credentials, backed by a new persistence model and EF migration.

Changes:

  • Add new RustPlusBot.Features.Pairing feature (modal + supervisor + handler + hosted service) to ingest FCM credentials and listen for in-game pairings.
  • Split credentials persistence into FcmRegistration (per guild+user) and PlayerCredential (per guild+server+owner), including migration + new unique indexes and cascade delete.
  • Update workspace #setup message rendering/localization to expose a “Connect account” button handled by the Pairing feature.

Reviewed changes

Copilot reviewed 55 out of 56 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tests/RustPlusBot.Persistence.Tests/Servers/ServerServiceTests.cs Adds tests for endpoint-based resolve-or-create.
tests/RustPlusBot.Persistence.Tests/PersistenceRegistrationTests.cs Verifies IFcmRegistrationStore DI registration.
tests/RustPlusBot.Persistence.Tests/Credentials/PairingSchemaTests.cs Verifies new unique indexes + cascade delete behavior.
tests/RustPlusBot.Persistence.Tests/Credentials/FcmRegistrationStoreTests.cs Covers upsert/list/status/get behavior for FCM registrations.
tests/RustPlusBot.Persistence.Tests/Credentials/CredentialStoreTests.cs Updates tests for new pairing-based credential upsert semantics.
tests/RustPlusBot.Features.Workspace.Tests/Messages/RendererTests.cs Asserts setup message includes the connect button.
tests/RustPlusBot.Features.Pairing.Tests/TestDb.cs Test helper DB context with migrations.
tests/RustPlusBot.Features.Pairing.Tests/RustPlusBot.Features.Pairing.Tests.csproj New test project for pairing feature.
tests/RustPlusBot.Features.Pairing.Tests/PairingSupervisorTests.cs Unit tests for supervisor connect/retry/notify behavior.
tests/RustPlusBot.Features.Pairing.Tests/PairingRegistrationTests.cs Validates DI wiring for the pairing feature.
tests/RustPlusBot.Features.Pairing.Tests/PairingHandlerTests.cs Tests server creation, credential upsert, and event publishing.
tests/RustPlusBot.Features.Pairing.Tests/FcmCredentialValidatorTests.cs Tests JSON structural validator.
tests/RustPlusBot.Features.Pairing.Tests/Fakes/RecordingOwnerNotifier.cs Fake notifier for tests.
tests/RustPlusBot.Features.Pairing.Tests/Fakes/FakePairingSource.cs Scripted pairing source + listener fake.
src/RustPlusBot.Persistence/Servers/ServerService.cs Adds ResolveOrCreateByEndpointAsync with concurrency handling.
src/RustPlusBot.Persistence/Servers/IServerService.cs Adds endpoint resolve-or-create API contract.
src/RustPlusBot.Persistence/PersistenceServiceCollectionExtensions.cs Registers IFcmRegistrationStore.
src/RustPlusBot.Persistence/Migrations/BotDbContextModelSnapshot.cs Updates snapshot for new entities/indexes/FKs.
src/RustPlusBot.Persistence/Migrations/20260615085447_PairingCredentials.Designer.cs Adds migration designer for new schema.
src/RustPlusBot.Persistence/Migrations/20260615085447_PairingCredentials.cs Migration to split credentials + add unique indexes + cascade FK.
src/RustPlusBot.Persistence/Credentials/IFcmRegistrationStore.cs New persistence API for per-user FCM registrations.
src/RustPlusBot.Persistence/Credentials/FcmRegistrationStore.cs EF-backed FCM registration store.
src/RustPlusBot.Persistence/Credentials/CredentialStore.cs Replaces store API with pairing-based upsert + active/standby selection.
src/RustPlusBot.Persistence/Configurations/RustServerConfiguration.cs Adds unique index on (GuildId, Ip, Port).
src/RustPlusBot.Persistence/Configurations/PlayerCredentialConfiguration.cs Adds unique index per (GuildId, Server, Owner) + cascade delete FK.
src/RustPlusBot.Persistence/Configurations/FcmRegistrationConfiguration.cs Adds unique index + required secret field for registrations.
src/RustPlusBot.Persistence/BotDbContext.cs Adds DbSet<FcmRegistration> and config application.
src/RustPlusBot.Host/RustPlusBot.Host.csproj References new Pairing feature project.
src/RustPlusBot.Host/Program.cs Adds Pairing options binding/validation and feature registration.
src/RustPlusBot.Features.Workspace/WorkspaceComponentIds.cs Shared component ID contract for setup connect button.
src/RustPlusBot.Features.Workspace/Messages/SetupMessageRenderer.cs Adds “Connect account” button to setup message.
src/RustPlusBot.Features.Workspace/Localization/LocalizationCatalog.cs Updates setup text + adds button localization keys.
src/RustPlusBot.Features.Pairing/Validation/FcmCredentialValidator.cs Lightweight JSON-object validator for modal input.
src/RustPlusBot.Features.Pairing/Supervisor/PairingSupervisor.cs Runs/controls one listener per registration + retry/expire flows.
src/RustPlusBot.Features.Pairing/Supervisor/IPairingSupervisor.cs Supervisor abstraction.
src/RustPlusBot.Features.Pairing/RustPlusBot.Features.Pairing.csproj New Pairing feature project definition.
src/RustPlusBot.Features.Pairing/PairingServiceCollectionExtensions.cs DI registration + hosted service + interaction module assembly export.
src/RustPlusBot.Features.Pairing/PairingOptions.cs Config options for probe timeout + backoff.
src/RustPlusBot.Features.Pairing/Pairing/PairingHandler.cs Converts pairing notifications into server+credential+event.
src/RustPlusBot.Features.Pairing/Pairing/IPairingHandler.cs Handler abstraction.
src/RustPlusBot.Features.Pairing/Notifications/IOwnerNotifier.cs Notifier abstraction.
src/RustPlusBot.Features.Pairing/Notifications/DiscordOwnerNotifier.cs DM-based notifier implementation.
src/RustPlusBot.Features.Pairing/Modules/CredentialModule.cs Interaction handlers for button + modal submit.
src/RustPlusBot.Features.Pairing/Modules/ConnectModal.cs Modal definition for credential intake.
src/RustPlusBot.Features.Pairing/Listening/RustPlusFcmPairingSource.cs Production FCM listener adapter.
src/RustPlusBot.Features.Pairing/Listening/PairingNotification.cs Pairing notification + enums.
src/RustPlusBot.Features.Pairing/Listening/IPairingSource.cs Pairing source/listener abstractions.
src/RustPlusBot.Features.Pairing/Hosting/PairingHostedService.cs Starts/stops listeners with host lifecycle.
src/RustPlusBot.Features.Pairing/AssemblyInfo.cs InternalsVisibleTo for pairing tests.
src/RustPlusBot.Domain/Credentials/PlayerCredential.cs Removes FCM blob from per-server credentials.
src/RustPlusBot.Domain/Credentials/FcmRegistrationStatus.cs Adds registration lifecycle status enum.
src/RustPlusBot.Domain/Credentials/FcmRegistration.cs Adds new per-user registration domain model.
src/RustPlusBot.Abstractions/Credentials/StoreCredentialRequest.cs Removes FCM JSON from credential request; pairing-focused inputs.
src/RustPlusBot.Abstractions/Credentials/ICredentialStore.cs Renames to pairing upsert API + documents semantics.
RustPlusBot.slnx Adds Pairing feature + tests to solution.
Directory.Packages.props Adds RustPlusApi.Fcm package version.
Files not reviewed (1)
  • src/RustPlusBot.Persistence/Migrations/20260615085447_PairingCredentials.Designer.cs: Generated file

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

Comment on lines +40 to +42
var isFirstForServer = !await context.PlayerCredentials
.AnyAsync(c => c.GuildId == request.GuildId && c.RustServerId == request.RustServerId, cancellationToken)
.ConfigureAwait(false);

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 b8e2ec2. The Active/Standby designation now derives from the created flag returned by ResolveOrCreateByEndpointAsync rather than a separate AnyAsync read. Only one concurrent pairing can create the server (the (GuildId, Ip, Port) unique index guarantees it), so only that owner's credential is Active; the rest are Standby. The TOCTOU window is gone and no new migration was needed.

Comment on lines +37 to +48
var registration = new FcmRegistration
{
GuildId = guildId,
OwnerUserId = ownerUserId,
ProtectedFcmCredentials = protector.Protect(fcmCredentialsJson),
Status = FcmRegistrationStatus.Active,
UpdatedAt = clock.UtcNow,
};

context.FcmRegistrations.Add(registration);
await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
return registration.Id;

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 b8e2ec2. UpsertAsync now catches the DbUpdateException from the (GuildId, OwnerUserId) unique index on the insert path, detaches the conflicting entry, re-reads the winning row, and applies the update — the same recovery pattern already used in ServerService.ResolveOrCreateByEndpointAsync. A concurrent double-submit of the modal no longer surfaces as a failed interaction.

Comment on lines +13 to +19
/// <inheritdoc />
public IPairingListener Create(
string fcmCredentialsJson,
Func<PairingNotification, CancellationToken, Task> onNotification) =>
new RustPlusFcmListener(fcmCredentialsJson, onNotification, logger);

private sealed partial class RustPlusFcmListener : IPairingListener

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 b8e2ec2. Create is now non-throwing: a construction/deserialization failure is logged and returns a RejectedListener whose ConnectAsync returns Rejected, so the supervisor reports the outcome as rejected and the modal shows "those credentials were rejected" instead of "still verifying". Added RustPlusFcmPairingSourceTests asserting Create doesn't throw and yields Rejected for unparseable credentials.

Comment on lines +17 to +41
private bool _started;

/// <inheritdoc />
public Task StartAsync(CancellationToken cancellationToken)
{
client.Ready += OnReadyAsync;
return Task.CompletedTask;
}

/// <inheritdoc />
public async Task StopAsync(CancellationToken cancellationToken)
{
client.Ready -= OnReadyAsync;
await supervisor.StopAllAsync().ConfigureAwait(false);
}

private async Task OnReadyAsync()
{
// Ready fires on every (re)connect; only start listeners once per process.
if (_started)
{
return;
}

_started = true;

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.

Kept the plain bool here intentionally, for consistency with the repo's other two hosted services (DiscordBotService and WorkspaceHostedService), which rely on Discord.Net dispatching Ready serially on the gateway thread — I've expanded the comment in b8e2ec2 to document that rationale. _started is also set before the await, and a double-run would be benign anyway: EnsureListenerAsync stops-then-restarts a listener per owner idempotently, so the worst case is redundant work, not corruption. If you'd prefer, I can standardize all three hosted services on Interlocked.Exchange as a separate cleanup.

…hrowing FCM Create)

- CredentialStore: derive Active from server-creation (race-free) instead of a TOCTOU read
- FcmRegistrationStore: recover from concurrent-insert unique violation by re-reading the winner
- RustPlusFcmPairingSource: Create no longer throws on bad credentials; maps to Rejected
- PairingHostedService: document the serial-Ready start-once rationale

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@HandyS11
HandyS11 merged commit 413eacf into develop Jun 15, 2026
3 checks passed
@HandyS11
HandyS11 deleted the feat/credentials-fcm-pairing branch June 15, 2026 11:01
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