Skip to content

Commit 0f64db0

Browse files
HandyS11claude
andauthored
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

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.config/dotnet-tools.json

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"version": 1,
3+
"isRoot": true,
4+
"tools": {
5+
"dotnet-stryker": {
6+
"version": "4.14.2",
7+
"commands": [
8+
"dotnet-stryker"
9+
],
10+
"rollForward": false
11+
},
12+
"jetbrains.resharper.globaltools": {
13+
"version": "2026.1.2",
14+
"commands": [
15+
"jb"
16+
],
17+
"rollForward": false
18+
},
19+
"dotnet-ef": {
20+
"version": "10.0.9",
21+
"commands": [
22+
"dotnet-ef"
23+
],
24+
"rollForward": false
25+
},
26+
"docfx": {
27+
"version": "2.78.5",
28+
"commands": [
29+
"docfx"
30+
],
31+
"rollForward": false
32+
}
33+
}
34+
}

.editorconfig

Lines changed: 480 additions & 0 deletions
Large diffs are not rendered by default.

.githooks/pre-push

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
#!/bin/sh
2+
# pre-push: run the ReSharper formatter + member reordering (ReformatAndReorder
3+
# profile in RustPlusBot.slnx.DotSettings) on the files being pushed and block the
4+
# push when it produces changes, so unformatted/unordered code never leaves the
5+
# machine. Bypass for emergencies with `git push --no-verify`.
6+
set -u
7+
8+
repo_root=$(git rev-parse --show-toplevel) || exit 1
9+
cd "$repo_root" || exit 1
10+
11+
default_branch=origin/develop
12+
13+
# Collect the files touched by the commits being pushed (stdin lines:
14+
# "<local ref> <local sha> <remote ref> <remote sha>").
15+
changed=""
16+
while read -r _local_ref local_sha _remote_ref remote_sha; do
17+
# Deleting a remote branch pushes the zero sha — nothing to format.
18+
case "$local_sha" in
19+
*[!0]*) ;;
20+
*) continue ;;
21+
esac
22+
23+
case "$remote_sha" in
24+
*[!0]*) base=$remote_sha ;;
25+
*)
26+
# New remote branch: compare against the default branch when
27+
# possible, otherwise format everything the branch contains.
28+
base=$(git merge-base "$local_sha" "$default_branch" 2>/dev/null) \
29+
|| base=$(git rev-list --max-parents=0 "$local_sha" | tail -n 1)
30+
;;
31+
esac
32+
33+
files=$(git diff --name-only --diff-filter=ACMR "$base" "$local_sha" -- \
34+
'*.cs' '*.csproj' '*.props' '*.targets')
35+
changed=$(printf '%s\n%s' "$changed" "$files")
36+
done
37+
38+
changed=$(printf '%s' "$changed" | sort -u | sed '/^$/d')
39+
[ -z "$changed" ] && exit 0
40+
41+
# Only files that still exist in the working tree can be cleaned up.
42+
existing=""
43+
for f in $changed; do
44+
[ -f "$f" ] && existing=$(printf '%s\n%s' "$existing" "$f")
45+
done
46+
existing=$(printf '%s' "$existing" | sed '/^$/d')
47+
[ -z "$existing" ] && exit 0
48+
49+
include=$(printf '%s' "$existing" | paste -sd ';' -)
50+
51+
echo "pre-push: checking ReSharper formatting on $(printf '%s\n' "$existing" | wc -l) file(s)..."
52+
53+
dotnet tool restore >/dev/null || exit 1
54+
55+
# Hash each file's working-tree diff so we can tell exactly which files the
56+
# formatter modifies (pre-existing local edits must not trip the check).
57+
hash_diffs() {
58+
for f in $existing; do
59+
printf '%s %s\n' "$(git diff -- "$f" | git hash-object --stdin)" "$f"
60+
done
61+
}
62+
63+
before=$(hash_diffs)
64+
65+
dotnet jb cleanupcode RustPlusBot.slnx \
66+
--profile="ReformatAndReorder" \
67+
--include="$include" \
68+
--no-build \
69+
--verbosity=ERROR
70+
# cleanupcode exits 3 ("No items were found to cleanup.") when the include set has
71+
# nothing to reformat (e.g. a push that only touches .csproj/.props files). That is
72+
# a success for our purposes; only a genuine failure (other non-zero codes) aborts.
73+
cleanup_status=$?
74+
[ "$cleanup_status" -eq 0 ] || [ "$cleanup_status" -eq 3 ] || exit 1
75+
76+
after=$(hash_diffs)
77+
78+
if [ "$before" != "$after" ]; then
79+
echo ""
80+
echo "pre-push: formatting issues found — the formatter changed:"
81+
printf '%s\n%s\n' "$before" "$after" | sort | uniq -u | cut -d' ' -f2- | sort -u | sed 's/^/ /'
82+
echo ""
83+
echo "Review the changes, amend/commit them, then push again."
84+
echo "(Bypass once with: git push --no-verify)"
85+
exit 1
86+
fi
87+
88+
echo "pre-push: formatting OK."
89+
exit 0

.github/dependabot.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# To get started with Dependabot version updates, you'll need to specify which
2+
# package ecosystems to update and where the package manifests are located.
3+
# Please see the documentation for all configuration options:
4+
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
5+
6+
version: 2
7+
updates:
8+
- package-ecosystem: "nuget"
9+
directory: "/"
10+
schedule:
11+
interval: "weekly"

0 commit comments

Comments
 (0)