Add clan support: detection, conditional channels, chat bridge and info embeds - #60
Merged
Conversation
Design for clan detection, a conditional #clanchat bridge, and a #claninfo channel with anchored embeds plus a live clan-change feed. Introduces a generic workspace capability seam so channels are provisioned only while a clan exists and torn down when it does not. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Thirteen TDD tasks covering the abstractions, the connection seam, clan persistence, the workspace capability gate, the chat bridge, the snapshot differ, the embeds, the MOTD modal, module wiring, localization and host composition. Also amend the spec to drop the logo thumbnail, which MessagePayload cannot carry, and to record the exact verified API types. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rework the plan and spec so one ChatRelay, ChatInboundProcessor and webhook poster serve both team and clan chat via a ChatChannelKind discriminator, replacing ITeamChatSender and ITeamChatChannelLocator with unified seams. Features.Clans now owns no chat code. Also drop the unreachable ClanMotdWriteResult.NotConnected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Introduce the RustPlusApi-free clan snapshot shapes, the three-state clan probe result, and the two event-bus events the clan feature consumes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add ClanMapping, which is the only place clan protobuf models and RustPlusErrorCode are interpreted, and extend IRustServerConnection with the clan probe, clan send, MOTD write, and the two clan events. no_clan maps to a definitive NoClan; every other failure degrades to Unavailable so a transient fault cannot look like a departed clan. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Task 2's socket methods copied a broad catch without the when (!cancellationToken.IsCancellationRequested) guard every sibling method in RustPlusSocketSource uses, which swallowed caller cancellation. Fix the plan's code and add the rule to the global constraints so later tasks do not repeat it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rt full clan snapshot mapping GetClanInfoAsync and SetClanMotdAsync were missing the `when (!cancellationToken.IsCancellationRequested)` guard that every other timeout-guarded method in RustPlusSocketSource uses on its broad catch, so a caller-initiated cancellation was being swallowed and mapped to a fallback value instead of propagating. Also strengthens Maps_a_populated_clan_to_a_snapshot to assert every field of the positional snapshot records, catching future constructor-argument mis-mapping. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Subscribe to the socket's clan events for the lifetime of a connected window, probe the clan once on connect so state survives a restart, and publish both onto the event bus. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the ClanState and ClanPlayerName entities, their configurations, the scoped IClanStore, and the ClanSupport migration. Members, roles and invites are stored as JSON because they are only ever read and written whole. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…njection, skip-write test coverage RecordNameAsync's existing-row lookup filtered on GuildId even though ClanPlayerName's primary key is (ServerId, SteamId) only, risking a DbUpdateException if a row's stored GuildId ever diverged from the caller-passed value. ClanStore also called DateTimeOffset.UtcNow directly instead of injecting IClock like every other store in the persistence layer, leaving its writes untestable with a controlled clock. Added a test that proves RecordNameAsync's skip-write guard by asserting UpdatedUtc is unchanged when the same name is recorded twice, and renamed the no-ids lookup test since an empty IN() clause returns nothing regardless of whether the empty-collection guard exists, so the assertion cannot actually prove the guard runs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a generic IWorkspaceCapabilityProvider seam so a ChannelSpec can name a capability: the reconciler provisions it only while available and deletes the channel and its anchored messages when it is not. A capability with no registered provider counts as unavailable. Also add MessageSpec.Pinned, pinned on first post only, and declare the two clan channels and three clan embeds. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Task 12 must update the hard-coded key-count assertion in the localization parity test. Task 11's capability provider must never answer false on a transient failure, must register exactly one provider per name, and should cache within a reconcile pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Task 5 already added the InternalsVisibleTo grant, and its review surfaced three rules the clan capability provider must follow: never answer false on a transient failure, register exactly once, and cache within a reconcile pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Task 5's review showed ServerInfoRefresher will full-reconcile on every tick for clanless servers unless the clan renderers return an empty payload when no clan is stored. Record that as a load-bearing requirement in Task 9. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace ITeamChatSender and ITeamChatChannelLocator with kind-parameterised IChatSender and IChatChannelLocator, and move RelayDedupBuffer to Abstractions keyed by channel kind so team and clan echoes cannot cross-cancel. One seam per concept, so the clan bridge does not need a parallel set. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Registration count alone can't catch a swapped Kind between TeamChatChannelLocator and ClanChatChannelLocator, which would silently misroute team/clan chat in production. Resolve the locators and assert each Kind pairs with its expected concrete type; verified the assertion fails when the two Kind values are swapped and passes when restored. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Generalise the relay, webhook poster and inbound processor over ChatChannelKind so one bridge serves both team and clan chat, and subscribe the hosted service to both message events. The inbound path picks its kind from whichever locator claims the channel. Clan chat senders are recorded as the name source for the clan roster. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The rule was written for socket probes that must distinguish an internal timeout token from a caller token. Applied to hosted-service consumer loops it is actively harmful: those own their CTS, already catch OperationCanceledException first, and are joined by a StopAsync that only swallows OCE, so the guard would let a post-cancellation exception fault shutdown. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Isolate the clan display-name recording failure so it can no longer kill the clan relay loop; replace reflection-based webhook name pinning with an internal method and move its test to a dedicated file; log a warning when ChatRelay has no locator for a channel kind instead of failing silently; remove a subsumed duplicate hosted-service test. The redundant VSTHRD003 pragma flagged in the review turned out to be load-bearing (removing it reintroduces the warning under TreatWarningsAsErrors), so it was left in place. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Compare consecutive clan snapshots into an ordered, deterministic change list. A first snapshot and a switch to a different clan both re-baseline silently rather than announcing every existing member, and an invite-to-member transition is reported once as an acceptance. Scaffolds the RustPlusBot.Features.Clans project and its test project, mirroring the Features.Chat project structure, and wires both into the solution file. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The counts still assumed the superseded Task 7, which created a clan chat bridge inside Features.Clans. That bridge now lives in Features.Chat, so the Features.Clans.Tests baseline is 19, not 36, and every later task's expected total shifts down by 17. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…napshotDiffer Close two coverage gaps found in review of Task 8: deleting the differ's .Order()/.OrderBy() calls previously left all 19 tests green because every fixture already declared its collections in ascending id order and each output category had at most one entry. New tests use descending-order fixtures for MemberJoined, MemberLeft and InviteSent so the sort is actually observable, plus a test for the untested "old role id unresolvable" half of the role-change OR-guard. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the three anchored #claninfo embeds and the name resolver behind them, which falls back to a Steam profile link for ids we have never seen a name for. The invites embed renders empty when there are none so the reconciler skips posting it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The clan.roster.perm.* keys are composed from permission flag names and never appear as quoted literals, so the grep-based collection step would miss them. A missed key fails no test - it renders as raw key text in a Discord embed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…inned Close five test-coverage gaps from Task 9 review: the three renderers' empty-payload paths, the Set MOTD button's credential/role hide-branches, and the roster field-truncation arithmetic all had implementations that were correct but not pinned by tests, so six reviewer mutations to src/ left all 43 existing tests green. Also decorrelate the roster ordering test's Rank/RoleId fixture, pin one resolver call per render, and add a cross-assembly test asserting the renderer key literals still match WorkspaceMessageKeys so a future rename can't silently unhook a renderer. No src/ changes; each new assertion was verified to fail against the corresponding mutation before being restored. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Route the Set MOTD interaction through ClanMotdWriter, which re-checks the acting player's in-game clan permission rather than trusting the button's presence, then writes through a new IRustServerQuery method. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Persist each snapshot, drive the workspace clan capability from its presence, and post diffed changes into #claninfo. Reconcile runs only on a none-to-clan or clan-to-none transition, and an Unavailable probe changes nothing at all. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ClanCapabilityProvider had no behavioural tests at all, yet a "false" answer from it makes the workspace reconciler delete #clanchat and #claninfo together with their whole history. Wrapping its store call in a try/catch that returned false passed the entire suite. Add ClanCapabilityProviderTests covering the load-bearing contracts: a store failure faults instead of answering false and is not cached, the global scope answers false without opening a scope or touching the store, the answer follows the stored row, the 5s TTL both serves and expires, and Invalidate drops the entry. Close the TOCTOU window where a read that queried the store before a transition committed could write its stale answer into the cache after Invalidate returned, silently losing the transition reconcile until the next transition or a process restart (WorkspaceHostedService heals only at startup and on ChannelDestroyed). A version counter now guards the cache write: a read that an invalidation overtook simply declines to populate the cache. Pin the invalidation call itself with the real provider wired to a substituted store, so a wrong key or a post-reconcile ordering fails. Pin the InteractionModuleAssembly registration that ClanMotdModule discovery depends on, and give Unavailable_changes_nothing a non-null snapshot case so the early-return guard is genuinely load-bearing rather than shadowed by the missing-snapshot guard. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the English and French strings for the clan channel names, the three anchored embeds, the MOTD modal, and every clan feed event. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Every other channel name is localized per locale (tchat-equipe, moniteurs-stockage, interrupteurs), so the clan channels should be too. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
channel.info.name is the singular "info", so the clan variant reads info-clan rather than introducing a third plural form. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Register AddClans, document the feature and its API limits in the README, and apply the repository formatting profile. Full suite: 1217 passed, 1 skipped, 0 failed across 19 test assemblies (Clans 82, Connections 112, Chat 54, Workspace 102, Persistence 125, Commands 149, Alarms 67, ItemData.Generator 63, Pairing 56, ItemData 53, Events 52, Abstractions 48, Switches 40, StorageMonitors 37, Wipes 26, Players 22, Discord 20, Localization 12, Map 97+1 skipped). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Render an explicit "no pending invites" embed when a clan is stored but has none. An empty payload means "leave the previous message on screen" to both the reconciler and ServerInfoRefresher, so the pinned invites embed listed resolved invitations forever. The two paths that must stay empty — no server id, and no clan stored — are unchanged, since a non-empty payload on a clanless server would trigger a full reconcile on every refresh tick. Isolate each clan state change in its own try/catch inside the consumer loop. The broad catch sat outside the await foreach, so one transient store failure or Discord 5xx ended the loop for the life of the process, after which the clan channels were never created and, worse, never torn down again. Harvest member names from the live team snapshot on the HasClan path, the second of the two name sources the design calls for. Without it a fresh install renders every roster entry, leader, creator and feed line as a bare Steam profile link until that member happens to speak in clan chat. It is best-effort: a disconnected socket or a failure never blocks persistence, the reconcile or the feed. Refuse to build a WorkspaceRegistry whose channel specs name a capability no provider answers for. The reconciler reads "no provider" as "unavailable", and unavailable deletes the channel and its history, so a host composing AddWorkspace() without AddClans() would silently destroy every guild's #clanchat and #claninfo on its first heal. It must fail to start instead. Budget the roster embed's total text. Per-field 1024 truncation left the 6000-char whole-embed cap unguarded, and EmbedBuilder.Build() throws rather than trimming; six role groups at their ceiling breach it. Groups are now added while a running total fits, and a localized notice names how many were omitted. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ClanCapabilityProvider's CacheTtl doc justified its value with "the heal path reconciles every server on a timer". WorkspaceHostedService heals at startup and on ChannelDestroyed only; there is no timer. Say what actually bounds the staleness: reconciles are event-driven and Invalidate, not expiry, is what guarantees a transition is seen. ClanMessageKeyParityTests claimed Features.Clans cannot see the internal WorkspaceMessageKeys. It can — Features.Workspace grants it InternalsVisibleTo. The constants are independent by choice, which is what makes the assertion meaningful. ClanMotdWriter claimed to re-check the permission because roles can change between render and click. It reads the same cached snapshot the button was rendered from, so a demotion the poller has not observed yet still gets through. Describe what it does defend against: a forged component id and a button clicked from scrollback. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
WorkspaceHostedService.StartAsync now resolves IWorkspaceRegistry itself, outside any try/catch, before scheduling any heal work. Previously the registry's constructor guard against an unprovided channel capability was only ever triggered lazily from inside broad catches (the startup heal and each event-loop consumer), so a host composing AddWorkspace() without the module owning a capability would start cleanly and only fault quietly on the first reconcile, contradicting the guard's own comment. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ClanStateService.RecordTeamNamesAsync now checks IClanStore.GetNamesAsync against the full roster first and only calls IRustServerQuery.GetTeamInfoAsync when a roster member's name is still unknown. OnClanChanged fires on every clan edit, including score changes, and score moves on every kill, so the previous unconditional call issued a companion-API RPC per kill per server. In steady state this now costs zero extra RPCs; the socket is only touched when a genuinely unknown member appears. Best-effort semantics are unchanged: a null snapshot, a failing query, or an unknown name still never blocks persistence or the feed post. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds first-class support for Rust clans, including conditional provisioning of #clanchat / #claninfo, a generalized chat bridge that supports both team+clan channels, and persistence + UI surfaces (pinned info embeds, change feed, and Set MOTD interaction).
Changes:
- Generalizes chat bridging over
ChatChannelKindand adds clan chat relay + inbound processing alongside existing team chat. - Introduces capability-gated workspace channels and a “clan” capability provider so clan channels exist only while a clan snapshot exists.
- Adds clan persistence (EF tables + store), clan state loop + renderers, and updates localization/resources/tests accordingly.
Reviewed changes
Copilot reviewed 127 out of 129 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/RustPlusBot.Persistence.Tests/Workspace/WorkspaceStoreTests.cs | Adds coverage for channel deletion removing anchored messages. |
| tests/RustPlusBot.Localization.Tests/StringsResourceParityTests.cs | Updates expected resource key count. |
| tests/RustPlusBot.Features.Workspace.Tests/WorkspaceRegistrationTests.cs | Updates DI assertions for generalized chat locators + capability provider guard. |
| tests/RustPlusBot.Features.Workspace.Tests/RustPlusBot.Features.Workspace.Tests.csproj | Adds test-only reference to clan feature for key parity tests. |
| tests/RustPlusBot.Features.Workspace.Tests/Registry/WorkspaceRegistryTests.cs | Updates registry ctor usage + adds gated-capability guard tests. |
| tests/RustPlusBot.Features.Workspace.Tests/Reconciler/ReconcilerHarness.cs | Extends harness for capability-gated channels + pinned messages. |
| tests/RustPlusBot.Features.Workspace.Tests/Messages/ClanMessageKeyParityTests.cs | Pins clan renderer keys to workspace message keys. |
| tests/RustPlusBot.Features.Workspace.Tests/Hosting/WorkspaceHostedServiceStartupGuardTests.cs | Verifies host fails fast when gated capability lacks provider. |
| tests/RustPlusBot.Features.Workspace.Tests/Hosting/WorkspaceConnectionStatusTests.cs | Adjusts minimal container for new registry resolution behavior. |
| tests/RustPlusBot.Features.Workspace.Tests/Fakes/FakeWorkspaceStore.cs | Adds DeleteChannelAsync behavior in fake store. |
| tests/RustPlusBot.Features.Workspace.Tests/Fakes/FakeWorkspaceGateway.cs | Adds pinning support + failure path simulation. |
| tests/RustPlusBot.Features.Workspace.Tests/CapabilityGatedChannelTests.cs | Adds reconciler tests for gated channels and pinning behavior. |
| tests/RustPlusBot.Features.Players.Tests/Hosting/PlayersHostedServiceTests.cs | Updates to ChatSendResult enum. |
| tests/RustPlusBot.Features.Connections.Tests/TeamChatSenderTests.cs | Updates sender seam to IChatSender(kind, …) and result enum. |
| tests/RustPlusBot.Features.Connections.Tests/Fakes/FakeRustSocketSource.cs | Adds clan probe/chat support to socket fake to eliminate setup races. |
| tests/RustPlusBot.Features.Connections.Tests/ClanMappingTests.cs | Adds unit tests for clan probe classification + snapshot mapping. |
| tests/RustPlusBot.Features.Connections.Tests/BotTeamChatSenderTests.cs | Updates BotTeamChatSender tests for generalized chat sender. |
| tests/RustPlusBot.Features.Commands.Tests/Hosting/CommandsHostedServiceTests.cs | Updates to ChatSendResult enum. |
| tests/RustPlusBot.Features.Clans.Tests/Writing/ClanMotdWriterTests.cs | Adds tests for permission-gated clan MOTD writes. |
| tests/RustPlusBot.Features.Clans.Tests/State/ClanCapabilityProviderTests.cs | Adds tests for cache, TTL, invalidation, and race handling. |
| tests/RustPlusBot.Features.Clans.Tests/RustPlusBot.Features.Clans.Tests.csproj | Adds new clans test project. |
| tests/RustPlusBot.Features.Clans.Tests/Names/ClanNameResolverTests.cs | Adds tests for name resolution and fallback profile links. |
| tests/RustPlusBot.Features.Clans.Tests/Hosting/ClansHostedServiceTests.cs | Ensures clan state loop survives per-item failures. |
| tests/RustPlusBot.Features.Clans.Tests/ClansRegistrationTests.cs | Validates DI composition and avoids double chat bridge registration. |
| tests/RustPlusBot.Features.Chat.Tests/TeamChatRelayTests.cs | Removed legacy team-only relay tests (replaced by generalized ones). |
| tests/RustPlusBot.Features.Chat.Tests/TeamChatInboundProcessorTests.cs | Removed legacy team-only inbound tests (replaced by generalized ones). |
| tests/RustPlusBot.Features.Chat.Tests/RelayDedupBufferTests.cs | Updates dedup buffer to include channel kind + adds clan/team isolation tests. |
| tests/RustPlusBot.Features.Chat.Tests/DiscordChatWebhookPosterTests.cs | Pins webhook naming for team vs clan kinds. |
| tests/RustPlusBot.Features.Chat.Tests/ChatRegistrationTests.cs | Updates to generalized relay/processor + dual locator routing assertions. |
| tests/RustPlusBot.Features.Alarms.Tests/Hosting/AlarmsHostedServiceTests.cs | Updates to ChatSendResult enum. |
| tests/RustPlusBot.Features.Alarms.Tests/AlarmStateRelayTests.cs | Updates to ChatSendResult enum. |
| src/RustPlusBot.Persistence/Workspace/WorkspaceStore.cs | Adds DeleteChannelAsync to delete channel + anchored messages. |
| src/RustPlusBot.Persistence/Workspace/IWorkspaceStore.cs | Adds DeleteChannelAsync contract. |
| src/RustPlusBot.Persistence/RustPlusBot.Persistence.csproj | Adds InternalsVisibleTo for tests/proxies. |
| src/RustPlusBot.Persistence/PersistenceServiceCollectionExtensions.cs | Registers IClanStore. |
| src/RustPlusBot.Persistence/Migrations/BotDbContextModelSnapshot.cs | Updates EF snapshot to include clan tables. |
| src/RustPlusBot.Persistence/Migrations/20260721212722_ClanSupport.cs | Adds ClanStates + ClanPlayerNames migration. |
| src/RustPlusBot.Persistence/Configurations/ClanStateConfiguration.cs | Configures clan state entity + cascade behavior. |
| src/RustPlusBot.Persistence/Configurations/ClanPlayerNameConfiguration.cs | Configures clan name cache entity + cascade behavior. |
| src/RustPlusBot.Persistence/Clans/IClanStore.cs | Introduces persistence contract for clan snapshots + name cache. |
| src/RustPlusBot.Persistence/Clans/ClanStore.cs | Implements EF-backed clan snapshot/name store. |
| src/RustPlusBot.Persistence/Clans/ClanSnapshotSerializer.cs | Adds JSON (de)serialization helper for clan collections. |
| src/RustPlusBot.Persistence/BotDbContext.cs | Adds DbSets + applies clan configurations. |
| src/RustPlusBot.Localization/Strings.resx | Adds clan channel names + clan UI/feed strings (English). |
| src/RustPlusBot.Host/RustPlusBot.Host.csproj | Adds Clans feature reference to host. |
| src/RustPlusBot.Host/Program.cs | Composes AddClans() into host startup. |
| src/RustPlusBot.Features.Workspace/WorkspaceServiceCollectionExtensions.cs | Registers clan chat locator + claninfo locator + generalized chat locator interface. |
| src/RustPlusBot.Features.Workspace/WorkspaceKeys.cs | Adds clan channel/message keys + WorkspaceCapabilities.Clan. |
| src/RustPlusBot.Features.Workspace/Specs/ServerWorkspaceSpecProvider.cs | Adds gated clan channels + pinned claninfo message specs. |
| src/RustPlusBot.Features.Workspace/RustPlusBot.Features.Workspace.csproj | Adds InternalsVisibleTo for clans + clan tests. |
| src/RustPlusBot.Features.Workspace/Registry/WorkspaceRegistry.cs | Adds capability provider validation + capability availability queries. |
| src/RustPlusBot.Features.Workspace/Registry/MessageSpec.cs | Adds Pinned flag support. |
| src/RustPlusBot.Features.Workspace/Registry/IWorkspaceRegistry.cs | Adds IsCapabilityAvailableAsync API. |
| src/RustPlusBot.Features.Workspace/Registry/IWorkspaceCapabilityProvider.cs | Introduces capability provider interface. |
| src/RustPlusBot.Features.Workspace/Registry/ChannelSpec.cs | Adds optional Capability gate to channel specs. |
| src/RustPlusBot.Features.Workspace/Reconciler/WorkspaceReconciler.cs | Implements gated channel deletion + message pinning on first post. |
| src/RustPlusBot.Features.Workspace/Reconciler/ServerInfoRefresher.cs | Adds clan embeds to periodic refresh list. |
| src/RustPlusBot.Features.Workspace/Locating/TeamChatChannelLocator.cs | Adapts locator to IChatChannelLocator with Kind=Team. |
| src/RustPlusBot.Features.Workspace/Locating/ITeamChatChannelLocator.cs | Removed team-only locator interface (replaced by IChatChannelLocator). |
| src/RustPlusBot.Features.Workspace/Locating/IClanInfoChannelLocator.cs | Adds locator for bot-only claninfo posting. |
| src/RustPlusBot.Features.Workspace/Locating/IChatChannelLocator.cs | Adds generalized chat locator contract with Kind discriminator. |
| src/RustPlusBot.Features.Workspace/Locating/ClanInfoChannelLocator.cs | Implements claninfo channel locator via caching locator. |
| src/RustPlusBot.Features.Workspace/Locating/ClanChatChannelLocator.cs | Adds clan chat locator + reverse resolution support. |
| src/RustPlusBot.Features.Workspace/Hosting/WorkspaceHostedService.cs | Forces registry construction at startup to fail fast on missing capabilities. |
| src/RustPlusBot.Features.Workspace/Gateway/IWorkspaceGateway.cs | Adds PinMessageAsync contract. |
| src/RustPlusBot.Features.Workspace/Gateway/DiscordWorkspaceGateway.cs | Implements PinMessageAsync using Discord.Net RequestOptions. |
| src/RustPlusBot.Features.Connections/Listening/RustPlusSocketSource.cs | Adds clan probe/query + clan chat + clan change event wiring. |
| src/RustPlusBot.Features.Connections/Listening/ITeamChatSender.cs | Removed team-only sender/result (replaced by IChatSender/ChatSendResult). |
| src/RustPlusBot.Features.Connections/Listening/IRustServerConnection.cs | Adds clan probing + clan chat send + MOTD set + new events. |
| src/RustPlusBot.Features.Connections/Listening/IChatSender.cs | Adds generalized chat sending seam and ChatSendResult. |
| src/RustPlusBot.Features.Connections/Listening/IBotTeamChatSender.cs | Updates to use ChatSendResult + references IChatSender. |
| src/RustPlusBot.Features.Connections/Listening/ClanMapping.cs | Adds mapping from RustPlusApi clan responses to bot snapshot/probe status. |
| src/RustPlusBot.Features.Connections/Listening/ClanChatLine.cs | Introduces raw clan chat line model. |
| src/RustPlusBot.Features.Connections/Listening/BotTeamChatSender.cs | Routes bot-prefixed lines via IChatSender(kind=Team). |
| src/RustPlusBot.Features.Connections/ConnectionServiceCollectionExtensions.cs | Registers IChatSender via ConnectionSupervisor. |
| src/RustPlusBot.Features.Clans/Writing/IClanMotdWriter.cs | Adds result enum + writer contract for Set MOTD feature. |
| src/RustPlusBot.Features.Clans/Writing/ClanMotdWriter.cs | Implements permission-gated MOTD writes via stored snapshot + live query seam. |
| src/RustPlusBot.Features.Clans/State/ClanSnapshotDiffer.cs | Adds pure differ to generate feed events from snapshot transitions. |
| src/RustPlusBot.Features.Clans/State/ClanChange.cs | Adds change kinds + change model for feed rendering. |
| src/RustPlusBot.Features.Clans/State/ClanCapabilityProvider.cs | Implements “clan” capability (cached, invalidatable) for workspace gating. |
| src/RustPlusBot.Features.Clans/RustPlusBot.Features.Clans.csproj | Adds new Clans feature project. |
| src/RustPlusBot.Features.Clans/Posting/IClanFeedPoster.cs | Adds contract for posting clan feed lines. |
| src/RustPlusBot.Features.Clans/Posting/DiscordClanFeedPoster.cs | Implements plain bot posting of clan feed lines. |
| src/RustPlusBot.Features.Clans/Names/IClanNameResolver.cs | Adds public resolver contract for renderers to resolve Steam IDs. |
| src/RustPlusBot.Features.Clans/Names/ClanNameResolver.cs | Implements resolver using cached names + Steam profile fallback links. |
| src/RustPlusBot.Features.Clans/Modules/ClanMotdModule.cs | Adds interaction handlers for Set MOTD button + modal submission. |
| src/RustPlusBot.Features.Clans/Modules/ClanMotdModal.cs | Adds modal definition for MOTD input. |
| src/RustPlusBot.Features.Clans/Messages/ClanInvitesMessageRenderer.cs | Adds renderer for pinned “invites” embed. |
| src/RustPlusBot.Features.Clans/Messages/ClanChangeRenderer.cs | Adds pure renderer from diffed changes to localized feed strings. |
| src/RustPlusBot.Features.Clans/Hosting/ClansHostedService.cs | Adds hosted service to consume clan state change events resiliently. |
| src/RustPlusBot.Features.Clans/ClansServiceCollectionExtensions.cs | Adds AddClans() DI composition (capability provider, renderers, hosted loop). |
| src/RustPlusBot.Features.Clans/ClanComponentIds.cs | Adds stable component IDs for clan interactions. |
| src/RustPlusBot.Features.Chat/Webhooks/IChatWebhookPoster.cs | Generalizes webhook poster contract over chat kind. |
| src/RustPlusBot.Features.Chat/Webhooks/DiscordTeamChatWebhookPoster.cs | Removed team-only poster (replaced by DiscordChatWebhookPoster). |
| src/RustPlusBot.Features.Chat/Webhooks/DiscordChatWebhookPoster.cs | Adds per-kind webhook discovery/creation and client caching. |
| src/RustPlusBot.Features.Chat/Relaying/TeamChatRelay.cs | Removed team-only relay (replaced by ChatRelay). |
| src/RustPlusBot.Features.Chat/Relaying/RelayedChatLine.cs | Adds normalized relay event for team/clan lines. |
| src/RustPlusBot.Features.Chat/Relaying/ChatRelay.cs | Adds generalized relay using kind-based locators + dedup buffer. |
| src/RustPlusBot.Features.Chat/Inbound/InboundOutcome.cs | Updates documentation wording to “chat channel”. |
| src/RustPlusBot.Features.Chat/Inbound/ChatInboundProcessor.cs | Generalizes inbound processing to any chat channel kind. |
| src/RustPlusBot.Features.Chat/Hosting/ChatHostedService.cs | Adds separate team+clan relay loops + clan name recording. |
| src/RustPlusBot.Features.Chat/ChatServiceCollectionExtensions.cs | Updates DI for generalized chat bridge components. |
| src/RustPlusBot.Domain/Clans/ClanState.cs | Adds clan snapshot entity stored per server. |
| src/RustPlusBot.Domain/Clans/ClanPlayerName.cs | Adds SteamId→name cache entity for clan rendering. |
| src/RustPlusBot.Abstractions/Events/ClanStateChangedEvent.cs | Adds event to publish clan probe/snapshot changes. |
| src/RustPlusBot.Abstractions/Events/ClanMessageReceivedEvent.cs | Adds event for received clan chat lines. |
| src/RustPlusBot.Abstractions/Connections/IRustServerQuery.cs | Adds SetClanMotdAsync query seam. |
| src/RustPlusBot.Abstractions/Connections/ClanSnapshot.cs | Adds API-decoupled clan snapshot model. |
| src/RustPlusBot.Abstractions/Connections/ClanProbeResult.cs | Adds probe status model distinguishing NoClan vs Unavailable. |
| src/RustPlusBot.Abstractions/Chat/RelayDedupBuffer.cs | Moves/makes public + adds kind into dedup key. |
| src/RustPlusBot.Abstractions/Chat/ChatChannelKind.cs | Adds team vs clan discriminator enum. |
| RustPlusBot.slnx | Adds clan feature + tests projects to solution. |
| README.md | Documents clan feature behavior and updates module list. |
Files not reviewed (1)
- src/RustPlusBot.Persistence/Migrations/20260721212722_ClanSupport.Designer.cs: Generated file
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…ery clan write GetNamesAsync now filters on GuildId like the sibling stores, matching HasClanAsync/GetAsync convention. RecordNameAsync and SaveAsync keep their lookups keyed on the true primary key (ServerId[+SteamId], not GuildId) to avoid a duplicate-key crash if GuildId is ever stale, but now assign GuildId on every write (insert and update) so any stale value self-heals instead of staying permanently invisible to the newly filtered reads. SaveAsync had the same latent lookup bug as RecordNameAsync had before an earlier fix; a test reproduces the pre-fix InvalidOperationException before applying the fix. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The published beta.4 parsed clan timestamps as Unix seconds while the wire sends milliseconds, so mapping any real clan payload threw, the library downgraded the throw to a ClientMappingFailed response, and the bot silently classified it as Unavailable: no channels, no rows, no log lines. beta.5 carries the parsing fix. Two observability holes made that failure invisible; both closed: - pass the host ILoggerFactory into RustPlus so the library's own warnings (e.g. a selector mapping failure) land in the bot log instead of a NullLogger - log the connect-time clan probe status once per connect in the supervisor Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The #claninfo anchor embeds are no longer pinned: the pin flag, the reconciler's pin step and the gateway pin operation are removed outright. The embeds still sit above the change feed because Discord orders messages by creation time and the reconciler already repairs declaration order. ChannelSpec.Order was declared but never applied, so a capability-gated channel created after the rest of its category (the clan pair appears only once a clan is detected) was appended at the bottom. The reconciler now ends every channel pass with EnsureChannelOrderAsync, which permutes the managed channels' existing position values into spec order — channels outside the list keep their place — and is a pure cache read when the order already matches, so the steady state issues no REST calls. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds support for the Rust clan system. A paired player may or may not be in a clan, so the two clan channels are provisioned only while a clan exists and removed when the player leaves.
Spec:
docs/superpowers/specs/2026-07-21-clan-support-design.mdPlan:
docs/superpowers/plans/2026-07-21-clan-support.mdWhat it does
(guild, server).RustPlusErrorCode.NoClanis the only definitive "no clan" signal; every other failure degrades toUnavailable, which preserves state. A connect-time probe means state is correct after a restart, not only after the next in-game change.#clanchat— bidirectional, with echo dedup, mute gating and command-prefix filtering.#claninfo— three self-refreshing anchor embeds (overview, roster, invites) plus a live feed of clan changes: joins, departures, promotions/demotions, MOTD, invites, rename, logo/colour, score, dissolution.CanSetMotd, re-checked server-side rather than trusting the button.Two design decisions worth reviewing
One chat bridge, not two. Rather than cloning the team-chat bridge,
Features.Chatis generalised over aChatChannelKind { Team, Clan }discriminator and serves both channels: oneChatRelay, oneChatInboundProcessor, one webhook poster, and unifiedIChatSender/IChatChannelLocatorseams replacing the team-specific pair.Features.Clanscontains no chat code. This refactors working, shipped code — its existing tests now run over both kinds.A generic capability gate, not clan-specific branching.
ChannelSpecgained an optionalCapability; the reconciler provisions such a channel only while a registeredIWorkspaceCapabilityProviderreports it available, and deletes it otherwise.WorkspaceRegistrynow refuses to construct if a gated spec has no provider — without that guard, a host composingAddWorkspace()withoutAddClans()would silently delete every guild's clan channels and their history.Live smoke findings (all fixed on this branch)
The first live run produced no channels at all, with nothing in the log. Root cause was in the dependency, not the branch: published RustPlusApi 2.0.0-beta.4 parsed clan timestamps as Unix seconds while the wire sends milliseconds, so mapping any real clan payload threw; the library downgraded the throw to a
ClientMappingFailedresponse and the bot correctly-but-silently treated it asUnavailable. Fixes:ILoggerFactoryis now passed intoRustPlus, so library-side warnings (like that mapping failure) reach the bot log instead of aNullLogger.Two behavioural corrections from the second run:
#claninfoanchor embeds are no longer pinned; the pin mechanism (MessageSpec.Pinned, reconciler pin step, gateway pin operation) is removed outright. The embeds still sit above the feed via creation order plus the existing declaration-order repair.ChannelSpec.Orderwas declared but never applied, so the clan pair — created only once a clan is detected — was appended at the bottom of the category. The reconciler now ends every channel pass withEnsureChannelOrderAsync, which permutes the managed channels' existing position values into spec order (channels outside the list keep their place) and is a pure cache read when already ordered, so the steady state issues no REST calls. Covered by three new tests, mutation-verified.Not implemented (no API exists)
RustPlusApi 2.0.0-beta.5 exposes no clan audit log, no per-member scores, and no kick/invite/promote actions.
CanAccessLogsandCanAccessScoreEventsare readable permission flags with no corresponding fetch. The roster therefore ranks on clan role and online status, which is entirely API-derived.Verification
jb cleanupcode --profile=ReformatAndReorderat a fixed pointEach of the 13 implementation tasks was reviewed independently with mutation testing, then the whole branch was reviewed for cross-cutting issues.
Remaining manual checklist
Channel creation is live-verified. Still worth checking before merge:
ClanState/ProvisionedChannel/ProvisionedMessagerows.Unavailablepath must preserve state. This is the highest-consequence path in the feature.A follow-up list of 11 Minor items is recorded in the branch's progress ledger; none are blocking.
🤖 Generated with Claude Code