Commit 0f64db0
Foundation (subsystem 0): bootable bot, persistence, event bus, /server + /bind (#1)
* chore: add solution scaffolding and build configuration
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>
* chore: scaffold foundation solution and projects
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore: pin Discord.Net, EF Core, Persistord, hosting packages
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>
* chore: align Microsoft.Extensions.Hosting to 10.0.9
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat: add IClock abstraction
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>
* chore: disable CA1859 for test projects via editorconfig
Replaces the per-line pragma in SystemClockTests; interface references are intentional in tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat: add in-process event bus
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>
* refactor: complete event-bus channel writer on unsubscribe
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>
* feat: add Rust-domain entities and enums
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat: add BotDbContext and entity configurations
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>
* fix: configure PairedEntity/EventSubscription columns and require FCM 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>
* feat: add initial EF migration
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>
* feat: add Data Protection credential protector
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>
* refactor: make credential protector internal with InternalsVisibleTo
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>
* feat: add EF-backed credential store
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>
* test: assert both credential secret fields are protected
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>
* feat: add guild-scoped server service
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>
* feat: add guild-scoped channel binding service
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>
* test: assert channel bindings do not leak across guilds
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>
* feat: add persistence DI registration
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test: verify ICredentialStore is registered by AddBotPersistence
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>
* feat: add Discord options, hosted bot service, and registration
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>
* fix: register Discord commands once and narrow CA1873 suppression
(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>
* feat: add /server and /bind interaction modules
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat: wire Generic Host, config, DI, and startup migration
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>
* feat: validate Discord token at startup (fail fast)
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>
* docs: add README and local run guide
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: preserve supplied snowflake PK for GuildSettings
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>
* style: apply repository ReSharper formatting
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>
* fix: apply migrations in tests and validate server port range
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>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>1 parent 8fbcd86 commit 0f64db0
75 files changed
Lines changed: 3901 additions & 0 deletions
File tree
- .config
- .githooks
- .github
- .vscode
- docs/development
- src
- RustPlusBot.Abstractions
- Credentials
- Events
- Time
- RustPlusBot.Discord
- Modules
- RustPlusBot.Domain
- Connections
- Credentials
- Entities
- Events
- Guilds
- Servers
- RustPlusBot.Host
- Credentials
- Properties
- RustPlusBot.Persistence
- Bindings
- Configurations
- Credentials
- Migrations
- Servers
- tests
- RustPlusBot.Abstractions.Tests
- Events
- Time
- RustPlusBot.Persistence.Tests
- Bindings
- Credentials
- Servers
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
0 commit comments