Skip to content

Commit fecd30c

Browse files
HandyS11claude
andcommitted
feat(connections): swap module, AddConnections wiring, host registration
- Add ConnectionComponentModule handling workspace:info:swap:* select menus - Add ConnectionServiceCollectionExtensions.AddConnections() DI extension - Wire ConnectionOptions validation and AddConnections() into Program.cs - Add project reference for Connections in Host.csproj - Add ConnectionRegistrationTests verifying IConnectionSupervisor and IConnectionStore resolve Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 861c30a commit fecd30c

5 files changed

Lines changed: 132 additions & 0 deletions

File tree

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using Microsoft.Extensions.DependencyInjection;
2+
using RustPlusBot.Discord;
3+
using RustPlusBot.Features.Connections.Hosting;
4+
using RustPlusBot.Features.Connections.Listening;
5+
using RustPlusBot.Features.Connections.Supervisor;
6+
7+
namespace RustPlusBot.Features.Connections;
8+
9+
/// <summary>DI registration for the live-connection feature.</summary>
10+
public static class ConnectionServiceCollectionExtensions
11+
{
12+
/// <summary>Registers the socket source, supervisor, module seam, and hosted service.</summary>
13+
/// <param name="services">The service collection to add to.</param>
14+
/// <returns>The same service collection, for chaining.</returns>
15+
public static IServiceCollection AddConnections(this IServiceCollection services)
16+
{
17+
ArgumentNullException.ThrowIfNull(services);
18+
19+
services.AddSingleton<IRustSocketSource, RustPlusSocketSource>();
20+
services.AddSingleton<IConnectionSupervisor, ConnectionSupervisor>();
21+
22+
// Contribute this assembly's interaction modules to the Discord layer.
23+
services.AddSingleton(new InteractionModuleAssembly(typeof(ConnectionServiceCollectionExtensions).Assembly));
24+
25+
services.AddHostedService<ConnectionHostedService>();
26+
27+
return services;
28+
}
29+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
using Discord;
2+
using Discord.Interactions;
3+
using Microsoft.Extensions.DependencyInjection;
4+
using RustPlusBot.Features.Connections.Supervisor;
5+
using RustPlusBot.Features.Workspace;
6+
using RustPlusBot.Persistence.Connections;
7+
8+
namespace RustPlusBot.Features.Connections.Modules;
9+
10+
/// <summary>Handles the #info "switch active player" select (ManageGuild).</summary>
11+
/// <param name="scopeFactory">Creates a short-lived DI scope per interaction.</param>
12+
public sealed class ConnectionComponentModule(IServiceScopeFactory scopeFactory)
13+
: InteractionModuleBase<SocketInteractionContext>
14+
{
15+
/// <summary>Promotes the chosen pooled credential to active and restarts the socket on it.</summary>
16+
/// <param name="serverIdRaw">The server id captured from the custom id.</param>
17+
/// <param name="selectedValues">The selected credential id (expects exactly one).</param>
18+
// Custom id: WorkspaceComponentIds.ServerInfoSwapPrefix + "*" = "workspace:info:swap:*"
19+
[ComponentInteraction(WorkspaceComponentIds.ServerInfoSwapPrefix + "*")]
20+
[RequireUserPermission(GuildPermission.ManageGuild)]
21+
public async Task SwapAsync(string serverIdRaw, string[] selectedValues)
22+
{
23+
ArgumentNullException.ThrowIfNull(selectedValues);
24+
if (Context.Guild is null)
25+
{
26+
await RespondAsync("This control must be used in a server.", ephemeral: true).ConfigureAwait(false);
27+
return;
28+
}
29+
30+
if (!Guid.TryParse(serverIdRaw, out var serverId)
31+
|| selectedValues.Length == 0
32+
|| !Guid.TryParse(selectedValues[0], out var credentialId))
33+
{
34+
await RespondAsync("That selection wasn't valid.", ephemeral: true).ConfigureAwait(false);
35+
return;
36+
}
37+
38+
await DeferAsync(ephemeral: true).ConfigureAwait(false);
39+
var scope = scopeFactory.CreateAsyncScope();
40+
await using (scope.ConfigureAwait(false))
41+
{
42+
var store = scope.ServiceProvider.GetRequiredService<IConnectionStore>();
43+
if (!await store.PromoteAsync(Context.Guild.Id, serverId, credentialId).ConfigureAwait(false))
44+
{
45+
await FollowupAsync("That player isn't available to activate.", ephemeral: true).ConfigureAwait(false);
46+
return;
47+
}
48+
49+
var supervisor = scope.ServiceProvider.GetRequiredService<IConnectionSupervisor>();
50+
await supervisor.EnsureConnectionAsync(Context.Guild.Id, serverId).ConfigureAwait(false);
51+
await FollowupAsync("Switching active player…", ephemeral: true).ConfigureAwait(false);
52+
}
53+
}
54+
}

src/RustPlusBot.Host/Program.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using RustPlusBot.Abstractions.Events;
77
using RustPlusBot.Abstractions.Time;
88
using RustPlusBot.Discord;
9+
using RustPlusBot.Features.Connections;
910
using RustPlusBot.Features.Pairing;
1011
using RustPlusBot.Features.Workspace;
1112
using RustPlusBot.Host.Credentials;
@@ -37,6 +38,16 @@
3738
"Pairing:MaxRetryDelay must be at least InitialRetryDelay.")
3839
.ValidateOnStart();
3940
builder.Services.AddPairing();
41+
builder.Services.AddOptions<ConnectionOptions>()
42+
.Bind(builder.Configuration.GetSection("Connections"))
43+
.Validate(static o => o.ConnectTimeout > TimeSpan.Zero, "Connections:ConnectTimeout must be positive.")
44+
.Validate(static o => o.InitialRetryDelay > TimeSpan.Zero, "Connections:InitialRetryDelay must be positive.")
45+
.Validate(static o => o.MaxRetryDelay >= o.InitialRetryDelay,
46+
"Connections:MaxRetryDelay must be at least InitialRetryDelay.")
47+
.Validate(static o => o.HeartbeatInterval > TimeSpan.Zero, "Connections:HeartbeatInterval must be positive.")
48+
.Validate(static o => o.HeartbeatTimeout > TimeSpan.Zero, "Connections:HeartbeatTimeout must be positive.")
49+
.ValidateOnStart();
50+
builder.Services.AddConnections();
4051

4152
var host = builder.Build();
4253

src/RustPlusBot.Host/RustPlusBot.Host.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,6 @@
2020
<ProjectReference Include="..\RustPlusBot.Abstractions\RustPlusBot.Abstractions.csproj" />
2121
<ProjectReference Include="..\RustPlusBot.Features.Workspace\RustPlusBot.Features.Workspace.csproj" />
2222
<ProjectReference Include="..\RustPlusBot.Features.Pairing\RustPlusBot.Features.Pairing.csproj" />
23+
<ProjectReference Include="..\RustPlusBot.Features.Connections\RustPlusBot.Features.Connections.csproj" />
2324
</ItemGroup>
2425
</Project>
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
using Discord.WebSocket;
2+
using Microsoft.Extensions.DependencyInjection;
3+
using NSubstitute;
4+
using RustPlusBot.Abstractions.Credentials;
5+
using RustPlusBot.Abstractions.Events;
6+
using RustPlusBot.Abstractions.Time;
7+
using RustPlusBot.Discord.Notifications;
8+
using RustPlusBot.Features.Connections;
9+
using RustPlusBot.Features.Connections.Supervisor;
10+
using RustPlusBot.Persistence;
11+
using RustPlusBot.Persistence.Connections;
12+
13+
namespace RustPlusBot.Features.Connections.Tests;
14+
15+
public sealed class ConnectionRegistrationTests
16+
{
17+
[Fact]
18+
public async Task Services_Resolve()
19+
{
20+
var services = new ServiceCollection();
21+
services.AddSingleton(new DiscordSocketClient());
22+
services.AddSingleton<IClock, SystemClock>();
23+
services.AddSingleton<IEventBus, InMemoryEventBus>();
24+
services.AddSingleton(Substitute.For<ICredentialProtector>());
25+
services.AddSingleton(Substitute.For<IUserDmSender>());
26+
services.AddLogging();
27+
services.AddBotPersistence("DataSource=:memory:");
28+
services.AddOptions<ConnectionOptions>();
29+
services.AddConnections();
30+
31+
await using var provider = services.BuildServiceProvider(new ServiceProviderOptions { ValidateScopes = true });
32+
33+
Assert.NotNull(provider.GetRequiredService<IConnectionSupervisor>());
34+
await using var scope = provider.CreateAsyncScope();
35+
Assert.NotNull(scope.ServiceProvider.GetRequiredService<IConnectionStore>());
36+
}
37+
}

0 commit comments

Comments
 (0)