Skip to content

Add clan support: detection, conditional channels, chat bridge and info embeds - #60

Merged
HandyS11 merged 39 commits into
developfrom
feat/clan-support
Jul 22, 2026
Merged

Add clan support: detection, conditional channels, chat bridge and info embeds#60
HandyS11 merged 39 commits into
developfrom
feat/clan-support

Conversation

@HandyS11

@HandyS11 HandyS11 commented Jul 22, 2026

Copy link
Copy Markdown
Owner

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.md
Plan: docs/superpowers/plans/2026-07-21-clan-support.md

What it does

  • Detection per (guild, server). RustPlusErrorCode.NoClan is the only definitive "no clan" signal; every other failure degrades to Unavailable, 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.
  • Set MOTD button — writes back to the game, gated on the paired player's in-game clan role having CanSetMotd, re-checked server-side rather than trusting the button.
  • English and French throughout (48 new keys).

Two design decisions worth reviewing

One chat bridge, not two. Rather than cloning the team-chat bridge, Features.Chat is generalised over a ChatChannelKind { Team, Clan } discriminator and serves both channels: one ChatRelay, one ChatInboundProcessor, one webhook poster, and unified IChatSender / IChatChannelLocator seams replacing the team-specific pair. Features.Clans contains no chat code. This refactors working, shipped code — its existing tests now run over both kinds.

A generic capability gate, not clan-specific branching. ChannelSpec gained an optional Capability; the reconciler provisions such a channel only while a registered IWorkspaceCapabilityProvider reports it available, and deletes it otherwise. WorkspaceRegistry now refuses to construct if a gated spec has no provider — without that guard, a host composing AddWorkspace() without AddClans() 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 ClientMappingFailed response and the bot correctly-but-silently treated it as Unavailable. Fixes:

  • RustPlusApi bumped to 2.0.0-beta.5, which carries the timestamp fix.
  • Library diagnostics surfaced: the host ILoggerFactory is now passed into RustPlus, so library-side warnings (like that mapping failure) reach the bot log instead of a NullLogger.
  • Probe visibility: the supervisor logs the connect-time clan probe status once per connect.

Two behavioural corrections from the second run:

  • No pinning: the #claninfo anchor 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.
  • Channel ordering enforced: ChannelSpec.Order was 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 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 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. CanAccessLogs and CanAccessScoreEvents are readable permission flags with no corresponding fetch. The roster therefore ranks on clan role and online status, which is entirely API-derived.

Verification

  • Build: 41 projects, 0 warnings
  • Tests: 1230 passed / 1 skipped across 19 assemblies
  • No EF model/migration drift
  • jb cleanupcode --profile=ReformatAndReorder at a fixed point
  • Live smoke: clan creation provisions both channels (clan detection confirmed working end-to-end against a real server)

Each 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:

  1. A message round-trips in each direction exactly once (no echo loop, no double-post).
  2. Leaving the clan deletes both channels and leaves no orphaned ClanState / ProvisionedChannel / ProvisionedMessage rows.
  3. Killing the Rust+ socket mid-session does not delete the channels — the Unavailable path must preserve state. This is the highest-consequence path in the feature.
  4. Switching the guild language to French re-renders both channel names and all embeds.

A follow-up list of 11 Minor items is recorded in the branch's progress ledger; none are blocking.

🤖 Generated with Claude Code

HandyS11 and others added 30 commits July 21, 2026 22:00
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>
HandyS11 and others added 6 commits July 22, 2026 12:18
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>
Copilot AI review requested due to automatic review settings July 22, 2026 11:56

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

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 ChatChannelKind and 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.

Comment thread src/RustPlusBot.Persistence/Clans/ClanStore.cs
Comment thread src/RustPlusBot.Persistence/Clans/ClanStore.cs
Comment thread src/RustPlusBot.Persistence/Clans/ClanStore.cs
HandyS11 and others added 3 commits July 22, 2026 14:17
…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>
@HandyS11
HandyS11 merged commit 2f3c4d4 into develop Jul 22, 2026
3 checks passed
@HandyS11
HandyS11 deleted the feat/clan-support branch July 22, 2026 19:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants