Foundation (subsystem 0): bootable bot, persistence, event bus, /server + /bind - #1
Conversation
Central package management, Directory.Build.props analyzers, .editorconfig, git hooks, NuGet config, and the (empty) solution file for the RustPlusBot foundation. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add Discord.Net 3.20.0, Microsoft.EntityFrameworkCore.Sqlite/Design 10.0.9, Microsoft.AspNetCore.DataProtection 10.0.9, and Persistord.Core 1.0.0-beta.1 to Directory.Packages.props and wire package references into each project. Remove redundant ImplicitUsings/Nullable from project csproj files (already set globally in Directory.Build.props). Add trailing newlines to test csproj files. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Introduces IClock and SystemClock in RustPlusBot.Abstractions.Time, making time-dependent logic testable via dependency injection. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replaces the per-line pragma in SystemClockTests; interface references are intentional in tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Introduces IEventBus contract and InMemoryEventBus implementation backed by per-subscriber Channel<object> instances, enabling type-safe publish/subscribe between producers (connection manager, FCM listener) and feature modules. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Explicitly completes the per-subscriber channel in finally for prompt resource release, and documents the no-back-pressure tradeoff. From Task 4 code review. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implements BotDbContext (inheriting Persistord's DiscordDbContext for the Discord skeleton and global ulong<->long snowflake conversion) plus five internal IEntityTypeConfiguration classes for RustServer, PlayerCredential, ChannelBinding, ConnectionState, and GuildSettings. Uses explicit ApplyConfiguration calls (method-chained) to satisfy CA1812. Covered by a TDD round-trip test over an in-memory SQLite connection. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… credentials Addresses Task 6 code-review findings: - Add PairedEntityConfiguration with explicit HasMaxLength(128) on Name and a composite index on (GuildId, RustServerId), replacing convention-only mapping. - Add EventSubscriptionConfiguration with explicit HasMaxLength(64) on EventKey and a composite index on (GuildId, RustServerId), replacing convention-only mapping. - Register both configurations in BotDbContext.OnModelCreating. - Mark ProtectedFcmCredentials as IsRequired() in PlayerCredentialConfiguration, consistent with ProtectedPlayerToken. - Strengthen snowflake round-trip test: AddedByUserId uses ulong.MaxValue (maps to -1L, exercises high-bit path), and asserts Id != Guid.Empty and AddedByUserId round-trips correctly alongside the existing GuildId assertion. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds DesignTimeDbContextFactory for dotnet-ef tooling, suppresses CS1591 in the Migrations editorconfig section, and generates the InitialCreate migration covering all 12 tables (7 Rust-domain + 5 Persistord Discord skeleton) with the correct unique index on ChannelBindings(GuildId,Feature). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implements Task 8: ICredentialProtector contract in Abstractions, backed by EphemeralDataProtectionProvider in Host; contract test in Persistence.Tests. Adapted from DataProtectionProvider.Create() (absent in net10) to EphemeralDataProtectionProvider, and added CA1515 suppression for Host public types intentionally referenced from test projects. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replaces the broad CA1515 disable on the Host section of .editorconfig with `internal sealed` + `InternalsVisibleTo` (the same pattern Worker uses), keeping CA1515 active on the composition root. The test assembly (RustPlusBot.Persistence.Tests) reaches the now-internal type via the MSBuild InternalsVisibleTo item in RustPlusBot.Host.csproj — no AssemblyInfo.cs file needed. Also documents the CryptographicException on ICredentialProtector.Unprotect so callers know decryption can fail when the payload is tampered with or produced by a different key/purpose. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implements ICredentialStore and StoreCredentialRequest contracts in Abstractions, and CredentialStore (EF-backed) in Persistence. Tokens are protected via ICredentialProtector before write; credentials are stored as Standby. Tests verify protection, persistence, and filtering by (guild, server). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Closes the gap where ProtectedFcmCredentials was unverified; adds Received() call-count checks on the protector. From Task 9 code review. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implements IServerService and ServerService (Add/List/Remove) with TDD. Three tests verify guild scoping: persistence, cross-guild isolation, and remove guards. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implements IBindingService and BindingService with upsert logic (bind or re-bind a feature to a channel per guild), backed by the existing ChannelBindings DbSet and its UNIQUE(GuildId, Feature) index. Two passing tests cover initial bind and rebind scenarios. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds explicit cross-guild isolation coverage matching the ServerService test bar. From Task 11 code review. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Descriptor-presence check (it can't be resolved in isolation without the Host-supplied ICredentialProtector). From Task 12 code review. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds DiscordOptions (bound from "Discord" config section), DiscordBotService (IHostedService that logs in, loads interaction modules, and registers slash commands per guild), and DiscordServiceCollectionExtensions.AddDiscordBot(). Also adds Microsoft.Extensions.Hosting.Abstractions 10.0.9 and suppresses CA1848/CA1873 globally in .editorconfig for this bot's low-volume logging. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(1) The Discord gateway fires the Ready event on every successful (re)connect, so OnReadyAsync previously bulk-overwrote all guild slash commands on each reconnect — each call counts against Discord's per-app daily command-write limit. Added a private _hasRegisteredCommands flag (set to true only after a successful registration pass, so transient failures retry on the next Ready) to ensure registration happens exactly once per process lifetime. No locking or volatile is needed because Ready is dispatched serially on the gateway thread. (2) CA1873 was suppressed globally in .editorconfig, which would silently hide genuinely expensive log-argument evaluation in future code elsewhere in the solution. Removed the global suppression and replaced it with a type-level [SuppressMessage] on DiscordBotService, co-located with the code where the log-bridge arguments (LogMessage property reads) are confirmed cheap. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replaces the Task-1 Worker stub with the full Generic Host wiring: registers DataProtection, IClock, IEventBus, ICredentialProtector, persistence (AddBotPersistence), and the Discord hosted service (AddDiscordBot). Runs EF Core migrations in a scoped pre-start block before the host loop begins. All awaits carry ConfigureAwait(false) per CA2007. appsettings.json extended with Discord and Database sections. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replaces Configure<DiscordOptions> with a validated AddOptions+ValidateOnStart so a missing token throws a clear OptionsValidationException at boot instead of a runtime Discord 401. From Task 15 code review. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
GuildSettings.GuildId is a Discord guild snowflake that serves as the primary key. Without ValueGeneratedNever(), EF Core's default convention treats the long-mapped integer PK as store-generated (Sqlite:Autoincrement), which causes SQLite to replace any supplied guild id with an autoincrement rowid — silently corrupting the guild key on insert. Added ValueGeneratedNever() to GuildSettingsConfiguration to match the pattern already used by all Persistord snowflake-PK entities. Regenerated the InitialCreate migration so the schema no longer carries the Autoincrement annotation and the model snapshot no longer shows ValueGeneratedOnAdd for this property. Added a round-trip regression test that inserts a large real snowflake guild id and asserts it survives SaveChanges unchanged. From final code review. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Line-wrapping and initializer layout from the .githooks/pre-push ReformatAndReorder profile; no behavior change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Implements the “foundation” layer of RustPlusBot by introducing a bootable .NET host, a Discord gateway with initial slash commands, and an EF Core (SQLite) persistence layer with migrations and tests.
Changes:
- Added persistence subsystem:
BotDbContext, entity configurations, SQLite migrations, and guild-scoped services (servers, bindings, credentials). - Added Discord subsystem: hosted gateway service plus
/server add|list|removeand/bindinteraction modules. - Added abstractions + tests + repo tooling/config to support development and enforce analyzers/formatting.
Reviewed changes
Copilot reviewed 73 out of 75 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/RustPlusBot.Persistence.Tests/SqliteContextFixture.cs | Test fixture for in-memory SQLite BotDbContext instances. |
| tests/RustPlusBot.Persistence.Tests/Servers/ServerServiceTests.cs | Tests for guild-scoped server CRUD behavior. |
| tests/RustPlusBot.Persistence.Tests/RustPlusBot.Persistence.Tests.csproj | Persistence test project definition and dependencies. |
| tests/RustPlusBot.Persistence.Tests/PersistenceRegistrationTests.cs | DI registration test for persistence layer services. |
| tests/RustPlusBot.Persistence.Tests/Credentials/CredentialStoreTests.cs | Tests for credential protection + persistence invariants. |
| tests/RustPlusBot.Persistence.Tests/Credentials/CredentialProtectorContractTests.cs | Contract test for protect/unprotect round-trip. |
| tests/RustPlusBot.Persistence.Tests/BotDbContextTests.cs | EF Core mapping tests (snowflake PK + round-trip). |
| tests/RustPlusBot.Persistence.Tests/Bindings/BindingServiceTests.cs | Tests for guild-scoped channel binding behavior. |
| tests/RustPlusBot.Abstractions.Tests/Time/SystemClockTests.cs | Test for IClock/SystemClock behavior. |
| tests/RustPlusBot.Abstractions.Tests/RustPlusBot.Abstractions.Tests.csproj | Abstractions test project definition. |
| tests/RustPlusBot.Abstractions.Tests/Events/InMemoryEventBusTests.cs | Tests for in-process event bus delivery and isolation. |
| src/RustPlusBot.Persistence/Servers/ServerService.cs | EF-backed guild-scoped server management service. |
| src/RustPlusBot.Persistence/Servers/IServerService.cs | Contract for server management. |
| src/RustPlusBot.Persistence/RustPlusBot.Persistence.csproj | Persistence project definition and package refs. |
| src/RustPlusBot.Persistence/PersistenceServiceCollectionExtensions.cs | DI wiring for persistence layer. |
| src/RustPlusBot.Persistence/Migrations/BotDbContextModelSnapshot.cs | EF Core model snapshot for migrations. |
| src/RustPlusBot.Persistence/Migrations/20260614180746_InitialCreate.Designer.cs | Migration designer for initial schema. |
| src/RustPlusBot.Persistence/Migrations/20260614180746_InitialCreate.cs | Initial EF Core migration (schema creation). |
| src/RustPlusBot.Persistence/DesignTimeDbContextFactory.cs | dotnet-ef design-time context factory. |
| src/RustPlusBot.Persistence/Credentials/CredentialStore.cs | EF-backed ICredentialStore that protects secrets before write. |
| src/RustPlusBot.Persistence/Configurations/RustServerConfiguration.cs | EF entity configuration for RustServer. |
| src/RustPlusBot.Persistence/Configurations/PlayerCredentialConfiguration.cs | EF entity configuration for PlayerCredential. |
| src/RustPlusBot.Persistence/Configurations/PairedEntityConfiguration.cs | EF entity configuration for PairedEntity. |
| src/RustPlusBot.Persistence/Configurations/GuildSettingsConfiguration.cs | EF entity configuration for GuildSettings PK strategy. |
| src/RustPlusBot.Persistence/Configurations/EventSubscriptionConfiguration.cs | EF entity configuration for EventSubscription. |
| src/RustPlusBot.Persistence/Configurations/ConnectionStateConfiguration.cs | EF entity configuration for ConnectionState. |
| src/RustPlusBot.Persistence/Configurations/ChannelBindingConfiguration.cs | EF entity configuration for unique (GuildId, Feature) bindings. |
| src/RustPlusBot.Persistence/BotDbContext.cs | EF Core context combining Persistord skeleton + Rust domain sets. |
| src/RustPlusBot.Persistence/Bindings/IBindingService.cs | Contract for channel binding operations. |
| src/RustPlusBot.Persistence/Bindings/BindingService.cs | EF-backed guild-scoped binding service. |
| src/RustPlusBot.Host/RustPlusBot.Host.csproj | Generic Host entrypoint project + InternalsVisibleTo for tests. |
| src/RustPlusBot.Host/Properties/launchSettings.json | Local dev launch profile/environment. |
| src/RustPlusBot.Host/Program.cs | Host wiring: options validation, DI setup, migration at startup. |
| src/RustPlusBot.Host/Credentials/DataProtectionCredentialProtector.cs | Data Protection-backed credential protector implementation. |
| src/RustPlusBot.Host/appsettings.json | Default configuration scaffold (token empty, SQLite file DB). |
| src/RustPlusBot.Domain/Servers/RustServer.cs | Domain entity for guild-scoped Rust server targets. |
| src/RustPlusBot.Domain/RustPlusBot.Domain.csproj | Domain project definition. |
| src/RustPlusBot.Domain/Guilds/GuildSettings.cs | Domain entity for per-guild settings keyed by snowflake. |
| src/RustPlusBot.Domain/Guilds/ChannelBinding.cs | Domain entity for channel-to-feature binding. |
| src/RustPlusBot.Domain/Guilds/BoundFeature.cs | Enum for bindable bot features. |
| src/RustPlusBot.Domain/Events/EventSubscription.cs | Domain entity for per-guild event subscriptions. |
| src/RustPlusBot.Domain/Entities/PairedEntityKind.cs | Enum for paired device kinds. |
| src/RustPlusBot.Domain/Entities/PairedEntity.cs | Domain entity for paired devices. |
| src/RustPlusBot.Domain/Credentials/PlayerCredential.cs | Domain entity for protected Rust+ credentials. |
| src/RustPlusBot.Domain/Credentials/CredentialStatus.cs | Enum for credential lifecycle state. |
| src/RustPlusBot.Domain/Connections/ConnectionState.cs | Domain entity for persisted connection state. |
| src/RustPlusBot.Discord/RustPlusBot.Discord.csproj | Discord layer project definition and dependencies. |
| src/RustPlusBot.Discord/Modules/ServerModule.cs | /server command module (add/list/remove). |
| src/RustPlusBot.Discord/Modules/BindModule.cs | /bind command module. |
| src/RustPlusBot.Discord/DiscordServiceCollectionExtensions.cs | DI wiring for Discord socket client + hosted service. |
| src/RustPlusBot.Discord/DiscordOptions.cs | Options type for Discord token binding. |
| src/RustPlusBot.Discord/DiscordBotService.cs | Hosted Discord gateway + interaction module registration/dispatch. |
| src/RustPlusBot.Abstractions/Time/SystemClock.cs | IClock wall-clock implementation. |
| src/RustPlusBot.Abstractions/Time/IClock.cs | Clock abstraction contract. |
| src/RustPlusBot.Abstractions/RustPlusBot.Abstractions.csproj | Abstractions project definition. |
| src/RustPlusBot.Abstractions/Events/InMemoryEventBus.cs | Channel-backed in-process event bus implementation. |
| src/RustPlusBot.Abstractions/Events/IEventBus.cs | Event bus abstraction contract. |
| src/RustPlusBot.Abstractions/Credentials/StoreCredentialRequest.cs | Request DTO for credential storage. |
| src/RustPlusBot.Abstractions/Credentials/ICredentialStore.cs | Credential store abstraction contract. |
| src/RustPlusBot.Abstractions/Credentials/ICredentialProtector.cs | Secret protection abstraction contract. |
| RustPlusBot.slnx.DotSettings | JetBrains cleanup profile settings for formatting/reordering. |
| RustPlusBot.slnx | Solution file including src + tests projects. |
| README.md | Project overview, stack, build/run instructions, roadmap. |
| NuGet.config | NuGet feed configuration. |
| docs/development/running-locally.md | Local run instructions using user-secrets and slash commands. |
| Directory.Packages.props | Central package version management. |
| Directory.Build.props | Repo-wide build/analyzer settings + git hooks bootstrap target. |
| .vscode/settings.json | VS Code .NET background analysis configuration. |
| .vscode/extensions.json | VS Code recommended extensions list. |
| .markdownlint.json | Markdownlint configuration. |
| .gitignore | Git ignore rules (including SQLite DB artifacts). |
| .github/dependabot.yml | Dependabot NuGet update configuration. |
| .githooks/pre-push | Pre-push hook running ReSharper cleanup profile on pushed files. |
| .editorconfig | Repo-wide code style + analyzer configuration. |
| .config/dotnet-tools.json | Local tool manifest (jb, ef, docfx, stryker). |
Files not reviewed (1)
- src/RustPlusBot.Persistence/Migrations/20260614180746_InitialCreate.Designer.cs: Generated file
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @@ -0,0 +1,2 @@ | |||
| <wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> | |||
There was a problem hiding this comment.
Not changing this. urn:shemas-jetbrains-com:settings-storage-xaml is the actual JetBrains settings-storage namespace URN — a long-standing upstream typo that is part of the .DotSettings format itself, not an error in this repo. Every JetBrains-generated .DotSettings uses this exact spelling (e.g. the sibling RustPlusApi and Persistord repos), and the pre-push ReSharper cleanup (jb cleanupcode via .githooks/pre-push) loads this file and runs successfully on every push. Renaming it to schemas would actually break Rider/ReSharper loading the cleanup profile.
| var context = new BotDbContext(options); | ||
| context.Database.EnsureCreated(); | ||
| return (context, connection); |
There was a problem hiding this comment.
Fixed in 16a20af. The fixture now calls Database.Migrate() instead of EnsureCreated(), so tests exercise the committed migration — the same schema path the Host applies at startup — and will catch migration/model drift. All 16 tests still pass.
| { | ||
| var server = new RustServer | ||
| { |
There was a problem hiding this comment.
Addressed in 16a20af with a 1-65535 range check. I put it in the /server add command handler (returns a clear ephemeral "Port must be between 1 and 65535.") rather than throwing from the persistence service: there is no global interaction error handler yet (deferred to a later subsystem), so a throw at the persistence boundary would surface to the user as a bare "interaction failed". The command layer is where user-facing input validation belongs here.
Addresses PR review: the SQLite test fixture now uses Database.Migrate() instead of EnsureCreated() so tests exercise the committed migration; /server add rejects ports outside 1-65535 with a clear ephemeral message. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Summary
Implements the foundation (subsystem 0) of RustPlusBot — a self-hosted, multi-guild Discord bot for the Rust+ companion, built on .NET 10, Discord.Net 3.20, and the prerelease RustPlusApi / Persistord libraries. This branch delivers a bootable, well-tested base; live Rust+ features are deferred to later subsystems.
What's included:
Abstractions(seams:IClock,IEventBus,ICredentialProtector,ICredentialStore),Domain(entities/enums),Persistence(BotDbContexton Persistord'sDiscordDbContext+ guild-scoped services + EF Core migration),Discord(gateway, hosted service,/server+/bindinteraction modules),Host(Generic Host entrypoint).ulong↔longsnowflake conversion inherited from Persistord; Rust-domain schema (servers, player credentials, connection state, guild settings, channel bindings, paired entities, event subscriptions);InitialCreatemigration applied at startup.GuildId; every query and command is guild-scoped (isolation enforced and tested).OptionsValidationException).System.Threading.Channels) decoupling future producers/consumers.GuildSettingsPK, event delivery/type-isolation, and DI registration.TreatWarningsAsErrorswith NetAnalyzers/Roslynator/Sonar atlatest-all.Deferred to later subsystems (carry-forwards)
!commands, map + live events, smart devices, cameras.RustServeris removed; read-then-write upsert concurrency hardening.RustPlusApiis not yet referenced anywhere (first wired in subsystem 1).Test plan
dotnet build RustPlusBot.slnx→ 0/0dotnet test RustPlusBot.slnx→ 16 passingrustplusbot.db, fails fast on missing tokenDiscord:Tokenvia user-secrets, invite the bot, run/server add|list|removeand/bindin a guild — seedocs/development/running-locally.md🤖 Generated with Claude Code