Skip to content

Subsystem 3a: team chat bridge (two-way #teamchat ↔ in-game) - #8

Merged
HandyS11 merged 19 commits into
developfrom
feat/chat-bridge
Jun 15, 2026
Merged

Subsystem 3a: team chat bridge (two-way #teamchat ↔ in-game)#8
HandyS11 merged 19 commits into
developfrom
feat/chat-bridge

Conversation

@HandyS11

Copy link
Copy Markdown
Owner

Summary

First slice of subsystem 3 — a two-way #teamchat ↔ in-game team chat bridge, one channel per registered server. Deliberately ships only the relay (the !command framework and Discord-side surfaces are deferred to 3b/3c) to de-risk live-socket bidirectional traffic and the cross-feature event fan-out.

  • Game → Discord: the Connections ConnectionSupervisor subscribes to each live socket's team-chat event and publishes a new TeamMessageReceivedEvent on the in-process bus; a new RustPlusBot.Features.Chat.TeamChatRelay consumes it, drops the bot's own echoes via RelayDedupBuffer, resolves the channel via ITeamChatChannelLocator, and posts each line as the player through a per-player webhook (ITeamChatWebhookPoster).
  • Discord → game: ChatHostedService listens to MessageReceived; TeamChatInboundProcessor filters bot/webhook + empty messages, resolves the #teamchat channel, formats [DisplayName] message, records it for echo-dropping, then relays into team chat via the new ITeamChatSender seam (implemented by the supervisor). Failures get a ❌ reaction.
  • Loop prevention: dedup-only — a shared RelayDedupBuffer records each relayed line and drops only the bot player's matching echo (genuine messages still bridge).
  • Workspace: new per-server #teamchat channel (Interactive profile) provisioned by the reconciler, EN/FR localized; new ITeamChatChannelLocator (TTL-cached, both directions) over a new IWorkspaceStore.GetChannelsByKeyAsync.
  • Gateway: enables the privileged Message Content intent (+ GuildMessages); docs updated for the dev-portal toggle and the Manage Webhooks permission.

New RustPlusBot.Features.Chat project mirrors the established per-feature pattern; cross-feature comms go over the event bus + thin public seams (ITeamChatSender, ITeamChatChannelLocator). No new entities or migrations.

Architecture notes

  • Socket stays encapsulated in Connections; Chat owns all Discord/webhook concerns.
  • Two intentionally-untested integration shims (consistent with the existing socket adapter): RustPlusSocketSource team-chat wiring and DiscordTeamChatWebhookPoster. All surrounding logic is unit-tested.

Test Plan

  • dotnet build RustPlusBot.slnx — 0 warnings / 0 errors (strict analyzers)
  • dotnet test RustPlusBot.slnx — 148/148 passing (Abstractions 5, Chat 14, Workspace 42, Pairing 26, Connections 17, Persistence 44)
  • dotnet jb cleanupcode RustPlusBot.slnx --profile=ReformatAndReorder — clean (no changes)
  • No EF model drift / no new migrations (locator only reads existing ProvisionedChannel rows)
  • Dedup loop-prevention traced end-to-end across both projects (whole-feature review)
  • Manual: enable Message Content intent + grant Manage Webhooks, pair a server, verify both relay directions and that the bot's own relayed lines don't double-post

🤖 Generated with Claude Code

HandyS11 and others added 18 commits June 15, 2026 23:42
…t seam

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Unlike ConnectAsync/GetInfoAsync which use broad catches to return typed
outcomes, SendTeamMessageAsync intentionally lets exceptions propagate so
the upstream supervisor can classify them as a failed send result.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ChatSender

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ose race

Add an early _disposed check in PublishTeamMessageAsync to prevent reading
_shutdown.Token after DisposeAsync has disposed it. Also add explanatory
comments on the shutdown-token choice and the fire-and-forget pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…imeouts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds IWorkspaceStore.GetChannelsByKeyAsync and its EF Core implementation
for a cross-guild reverse lookup of ProvisionedChannel rows by ChannelKey,
needed by the upcoming team-chat channel locator (subsystem 3a Task 6).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
FakeWorkspaceStore was missing the new IWorkspaceStore member added in
the previous commit, causing the Workspace.Tests assembly to fail to
build (CS0535) and silently skip all 38 tests. Adds the in-memory
implementation filtered by ChannelKey over _channels.Values.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… initializers

Add Cache_refreshes_after_ttl_expires test proving the locator serves
stale cache within the 30 s TTL and rebuilds after expiry. Change the
two explicit Dictionary<...> field initializers to target-typed new().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…intent

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…bridge

Add ## Discord application setup (team chat bridge) section to
running-locally.md explaining the required privileged gateway intent and
Manage Webhooks permission. Apply jb ReformatAndReorder to all
chat-bridge source files (these were never jb-formatted before this
finalization pass).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 15, 2026 23:14

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

Implements the first slice of “Subsystem 3a” by adding a two-way bridge between in-game team chat and a per-server Discord #teamchat channel. This introduces a new Chat feature project, wires team-chat receive/send through Connections, and adds Workspace support to provision and locate the #teamchat channel.

Changes:

  • Adds a new RustPlusBot.Features.Chat project that relays game → Discord via per-player webhooks and Discord → game via a hosted MessageReceived listener, with loop prevention using a shared RelayDedupBuffer.
  • Extends Connections to publish TeamMessageReceivedEvent from the live socket and to expose an ITeamChatSender seam for sending messages to team chat.
  • Adds Workspace support for the per-server #teamchat channel and a TTL-cached ITeamChatChannelLocator, plus required Discord gateway intents and updated local-dev docs.

Reviewed changes

Copilot reviewed 42 out of 42 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/RustPlusBot.Persistence.Tests/Workspace/WorkspaceStoreByKeyTests.cs Adds coverage for GetChannelsByKeyAsync persistence query behavior.
tests/RustPlusBot.Features.Workspace.Tests/Specs/ServerWorkspaceSpecProviderTests.cs Verifies the server workspace spec contributes the teamchat channel.
tests/RustPlusBot.Features.Workspace.Tests/Locating/TeamChatChannelLocatorTests.cs Adds coverage for teamchat channel lookup and TTL cache behavior.
tests/RustPlusBot.Features.Workspace.Tests/Fakes/FakeWorkspaceStore.cs Updates fake store to support GetChannelsByKeyAsync.
tests/RustPlusBot.Features.Connections.Tests/TeamChatSenderTests.cs Adds tests for publish-on-receive and send-to-socket routing.
tests/RustPlusBot.Features.Connections.Tests/Fakes/FakeRustSocketSource.cs Extends fake connection to support team message send/receive simulation.
tests/RustPlusBot.Features.Chat.Tests/TeamChatRelayTests.cs Adds unit tests for game → Discord relay and echo dropping.
tests/RustPlusBot.Features.Chat.Tests/TeamChatInboundProcessorTests.cs Adds unit tests for Discord → game filtering/formatting/sending.
tests/RustPlusBot.Features.Chat.Tests/RustPlusBot.Features.Chat.Tests.csproj Introduces the Chat test project.
tests/RustPlusBot.Features.Chat.Tests/RelayDedupBufferTests.cs Adds unit tests for dedup buffer semantics and TTL expiry.
tests/RustPlusBot.Features.Chat.Tests/ChatRegistrationTests.cs Validates DI registration for Chat services (but see stored comment).
tests/RustPlusBot.Abstractions.Tests/Events/TeamMessageReceivedEventTests.cs Adds unit test coverage for the new event type.
src/RustPlusBot.Persistence/Workspace/WorkspaceStore.cs Implements GetChannelsByKeyAsync query.
src/RustPlusBot.Persistence/Workspace/IWorkspaceStore.cs Adds GetChannelsByKeyAsync to the workspace store contract.
src/RustPlusBot.Host/RustPlusBot.Host.csproj References the new Chat feature project from the host.
src/RustPlusBot.Host/Program.cs Registers AddChat() in the host DI setup.
src/RustPlusBot.Features.Workspace/WorkspaceServiceCollectionExtensions.cs Registers ITeamChatChannelLocator as a singleton.
src/RustPlusBot.Features.Workspace/WorkspaceKeys.cs Adds stable key for per-server teamchat channel.
src/RustPlusBot.Features.Workspace/Specs/ServerWorkspaceSpecProvider.cs Provisions per-server #teamchat as an interactive channel.
src/RustPlusBot.Features.Workspace/Locating/TeamChatChannelLocator.cs Adds TTL-cached resolver for per-server teamchat channels.
src/RustPlusBot.Features.Workspace/Locating/ITeamChatChannelLocator.cs Introduces public locator interface for cross-feature use.
src/RustPlusBot.Features.Workspace/Localization/LocalizationCatalog.cs Adds EN/FR localization keys for channel.teamchat.name.
src/RustPlusBot.Features.Connections/Supervisor/ConnectionSupervisor.cs Adds inbound team-chat event publishing and ITeamChatSender implementation.
src/RustPlusBot.Features.Connections/Listening/TeamChatLine.cs Adds representation of raw team chat lines from the socket.
src/RustPlusBot.Features.Connections/Listening/RustPlusSocketSource.cs Wires RustPlusApi team chat receive/send into IRustServerConnection.
src/RustPlusBot.Features.Connections/Listening/ITeamChatSender.cs Introduces the sender seam and TeamChatSendResult.
src/RustPlusBot.Features.Connections/Listening/IRustServerConnection.cs Extends socket interface with team chat send + received event.
src/RustPlusBot.Features.Connections/ConnectionServiceCollectionExtensions.cs Registers ConnectionSupervisor as the singleton backing multiple interfaces.
src/RustPlusBot.Features.Chat/Webhooks/ITeamChatWebhookPoster.cs Adds abstraction for webhook-based posting into Discord teamchat.
src/RustPlusBot.Features.Chat/Webhooks/DiscordTeamChatWebhookPoster.cs Implements webhook discovery/creation and caching (but see stored comment).
src/RustPlusBot.Features.Chat/RustPlusBot.Features.Chat.csproj Introduces the Chat feature project and dependencies.
src/RustPlusBot.Features.Chat/Relaying/TeamChatRelay.cs Implements game → Discord relay with echo drop behavior.
src/RustPlusBot.Features.Chat/Relaying/RelayDedupBuffer.cs Implements short-lived dedup buffer for loop prevention.
src/RustPlusBot.Features.Chat/Inbound/TeamChatInboundProcessor.cs Implements Discord → game processing, formatting, and sending.
src/RustPlusBot.Features.Chat/Inbound/InboundOutcome.cs Adds outcome enum for inbound processing results.
src/RustPlusBot.Features.Chat/Inbound/InboundMessage.cs Adds reduced message DTO for inbound processing.
src/RustPlusBot.Features.Chat/Hosting/ChatHostedService.cs Adds hosted service to consume bus events + listen to Discord messages.
src/RustPlusBot.Features.Chat/ChatServiceCollectionExtensions.cs Adds DI registration for Chat services.
src/RustPlusBot.Discord/DiscordServiceCollectionExtensions.cs Enables GuildMessages + MessageContent gateway intents.
src/RustPlusBot.Abstractions/Events/TeamMessageReceivedEvent.cs Adds new event for publishing received in-game team chat lines.
RustPlusBot.slnx Adds the Chat projects to the solution.
docs/development/running-locally.md Documents Message Content intent + Manage Webhooks requirement.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +42 to +47
// The relay and the inbound processor MUST share the same dedup buffer instance for the
// record/consume echo pairing to work; the registration uses a single AddSingleton.
Assert.Same(
provider.GetRequiredService<RelayDedupBuffer>(),
provider.GetRequiredService<RelayDedupBuffer>());
}
Comment on lines +68 to +73
var hooks = await channel.GetWebhooksAsync().ConfigureAwait(false);
var hook = hooks.FirstOrDefault(h => h.Name == WebhookName)
?? await channel.CreateWebhookAsync(WebhookName).ConfigureAwait(false);
var webhookClient = new DiscordWebhookClient(hook);
return _clients.GetOrAdd(channelId, webhookClient);
}
- DiscordTeamChatWebhookPoster: dispose the redundant DiscordWebhookClient
  when it loses the GetOrAdd cache race (no longer leaks a client/HttpClient).
- ChatRegistrationTests: replace the weak twice-resolve singleton assertion
  with a behavioral test proving the DI-resolved relay and processor share the
  same RelayDedupBuffer (processor records a line; the relay drops its echo).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@HandyS11

Copy link
Copy Markdown
Owner Author

Addressed both Copilot comments in e5005b7:

  1. DiscordTeamChatWebhookPoster race leak — when GetOrAdd loses the race for a channel, the redundant DiscordWebhookClient is now disposed instead of leaked.
  2. ChatRegistrationTests weak assertion — replaced the twice-resolve singleton check with a behavioral test: the DI-resolved TeamChatInboundProcessor records a line and the DI-resolved TeamChatRelay drops its echo, which only passes if they share the same RelayDedupBuffer instance.

Full suite 149/149, build 0/0, jb-clean.

@HandyS11
HandyS11 merged commit 8132240 into develop Jun 15, 2026
3 checks passed
@HandyS11
HandyS11 deleted the feat/chat-bridge branch June 15, 2026 23:34
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