Skip to content
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
35257c9
chore(workspace): scaffold Features.Workspace project and tests
HandyS11 Jun 14, 2026
7e3d25c
feat(domain): add ProvisionedCategory/Channel/Message entities
HandyS11 Jun 14, 2026
934f79b
refactor: remove /server, /bind, and ChannelBinding (superseded by wo…
HandyS11 Jun 14, 2026
ea32dba
feat(persistence): provisioning schema + WorkspaceProvisioning migration
HandyS11 Jun 14, 2026
df33bdf
feat(persistence): IWorkspaceStore + WorkspaceStore upsert/teardown q…
HandyS11 Jun 14, 2026
1b4e873
feat(workspace): i18n localization seam with EN/FR catalog
HandyS11 Jun 14, 2026
d4b9f33
feat(workspace): spec registry (channel/message specs, providers, ren…
HandyS11 Jun 14, 2026
26391f4
feat(workspace): IWorkspaceGateway seam + in-memory test fake
HandyS11 Jun 14, 2026
40c0296
feat(workspace): per-guild ProvisioningLock
HandyS11 Jun 14, 2026
8ebf37f
feat(workspace): WorkspaceReconciler converge engine + fresh provision
HandyS11 Jun 14, 2026
dbf27ae
refactor(workspace): address review — Skipped status + UpdatedAt on e…
HandyS11 Jun 14, 2026
6e16b5a
test(workspace): reconciler is idempotent on re-run
HandyS11 Jun 14, 2026
21eb77d
test(workspace): reconciler adopts same-named channel on drift
HandyS11 Jun 14, 2026
8e45a1c
test(workspace): reconciler self-heals a deleted channel
HandyS11 Jun 14, 2026
015006e
test(workspace): message repost-vs-edit and orphan-spec retention
HandyS11 Jun 14, 2026
decf7f1
test(workspace): concurrent reconciles converge once
HandyS11 Jun 14, 2026
377d286
test(workspace): per-server category provisioning
HandyS11 Jun 14, 2026
64490fc
feat(workspace): channel/message keys, renderers, and spec providers
HandyS11 Jun 14, 2026
f973511
feat(workspace): teardown service (RemoveServer / ResetGuild)
HandyS11 Jun 14, 2026
bbb3d9f
feat: ServerRegisteredEvent, WorkspaceOptions, and interaction-module…
HandyS11 Jun 14, 2026
3907616
feat(workspace): Discord.Net implementation of IWorkspaceGateway
HandyS11 Jun 14, 2026
0ee0773
feat(workspace): AddWorkspace DI registration + composition test
HandyS11 Jun 14, 2026
b619b9c
feat(workspace): /setup command module
HandyS11 Jun 14, 2026
ae7b74d
feat(workspace): settings language selector + /workspace reset & simu…
HandyS11 Jun 14, 2026
2e04a80
feat(workspace): HealGuild self-heal + hosted service (startup, Chann…
HandyS11 Jun 14, 2026
76a1ffe
feat(host): wire AddWorkspace, config flag, and update running-locall…
HandyS11 Jun 14, 2026
35e90a5
refactor(workspace): address final review — heal all provisioned scop…
HandyS11 Jun 14, 2026
77d603b
style(workspace): apply dotnet format (pre-push hook)
HandyS11 Jun 14, 2026
4c25b01
fix(workspace): address PR review — validate culture/port, clarify re…
HandyS11 Jun 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions RustPlusBot.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
<Project Path="src/RustPlusBot.Discord/RustPlusBot.Discord.csproj" />
<Project Path="src/RustPlusBot.Domain/RustPlusBot.Domain.csproj" />
<Project Path="src/RustPlusBot.Host/RustPlusBot.Host.csproj" />
<Project Path="src/RustPlusBot.Features.Workspace/RustPlusBot.Features.Workspace.csproj" />
<Project Path="src/RustPlusBot.Persistence/RustPlusBot.Persistence.csproj" />
</Folder>
<Folder Name="/tests/">
<Project Path="tests/RustPlusBot.Abstractions.Tests/RustPlusBot.Abstractions.Tests.csproj" />
<Project Path="tests/RustPlusBot.Features.Workspace.Tests/RustPlusBot.Features.Workspace.Tests.csproj" />
<Project Path="tests/RustPlusBot.Persistence.Tests/RustPlusBot.Persistence.Tests.csproj" />
</Folder>
</Solution>
19 changes: 13 additions & 6 deletions docs/development/running-locally.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,18 @@

On first run the host applies the EF Core migration and creates `rustplusbot.db` in the working directory.

5. In the guild, use the slash commands:

- `/server add name:<name> ip:<host> port:<port>`
- `/server list`
- `/server remove id:<id>`
- `/bind feature:<Chat|Events|Devices|Cameras> channel:<#channel>`
5. In the guild, use the slash commands per the provisioning flow below.

A missing or empty `Discord:Token` makes the host fail fast at startup with a clear `OptionsValidationException`.

## Provisioning the workspace

1. Invite the bot with the **Manage Channels**, **Manage Roles**, **Manage Messages**, and
**Embed Links** permissions.
2. In your test guild, run `/setup`. The bot creates a **RustPlusBot** category with
`#information`, `#setup`, and `#settings` channels and posts its anchored messages.
3. Re-running `/setup` is safe — it reconciles and repairs without creating duplicates.
4. Change the language from the selector in `#settings`.
5. In Development (`Workspace:EnableDangerCommands = true`), use
`/workspace simulate-server name:<n> ip:<host> port:<port>` to create a server category, and
`/workspace reset` to delete the whole workspace.
6 changes: 6 additions & 0 deletions src/RustPlusBot.Abstractions/Events/ServerRegisteredEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace RustPlusBot.Abstractions.Events;

/// <summary>Published when a Rust server is registered to a guild (stub trigger in 1a; FCM pairing in 1b).</summary>
/// <param name="GuildId">The owning guild snowflake.</param>
/// <param name="ServerId">The registered server's id.</param>
public sealed record ServerRegisteredEvent(ulong GuildId, Guid ServerId);
7 changes: 7 additions & 0 deletions src/RustPlusBot.Discord/DiscordBotService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ namespace RustPlusBot.Discord;
/// <param name="client">The Discord socket client.</param>
/// <param name="interactions">The interaction service that dispatches slash commands.</param>
/// <param name="services">The root service provider used to construct interaction modules.</param>
/// <param name="moduleAssemblies">Additional assemblies to scan for interaction modules contributed by feature projects.</param>
/// <param name="options">The Discord options carrying the bot token.</param>
/// <param name="logger">The logger.</param>
[SuppressMessage("Performance", "CA1873:Avoid potentially expensive logging",
Expand All @@ -25,6 +26,7 @@ public sealed class DiscordBotService(
DiscordSocketClient client,
InteractionService interactions,
IServiceProvider services,
IEnumerable<InteractionModuleAssembly> moduleAssemblies,
IOptions<DiscordOptions> options,
ILogger<DiscordBotService> logger) : IHostedService
{
Expand All @@ -41,6 +43,11 @@ public async Task StartAsync(CancellationToken cancellationToken)
client.JoinedGuild += OnJoinedGuildAsync;

await interactions.AddModulesAsync(Assembly.GetExecutingAssembly(), services).ConfigureAwait(false);
foreach (var moduleAssembly in moduleAssemblies)
{
await interactions.AddModulesAsync(moduleAssembly.Assembly, services).ConfigureAwait(false);
}

await client.LoginAsync(TokenType.Bot, _options.Token).ConfigureAwait(false);
await client.StartAsync().ConfigureAwait(false);
}
Expand Down
7 changes: 7 additions & 0 deletions src/RustPlusBot.Discord/InteractionModuleAssembly.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
using System.Reflection;

namespace RustPlusBot.Discord;

/// <summary>Registers an assembly whose Discord interaction modules should be loaded at startup.</summary>
/// <param name="Assembly">The assembly to scan for interaction modules.</param>
public sealed record InteractionModuleAssembly(Assembly Assembly);
42 changes: 0 additions & 42 deletions src/RustPlusBot.Discord/Modules/BindModule.cs

This file was deleted.

119 changes: 0 additions & 119 deletions src/RustPlusBot.Discord/Modules/ServerModule.cs

This file was deleted.

17 changes: 0 additions & 17 deletions src/RustPlusBot.Domain/Guilds/BoundFeature.cs

This file was deleted.

17 changes: 0 additions & 17 deletions src/RustPlusBot.Domain/Guilds/ChannelBinding.cs

This file was deleted.

20 changes: 20 additions & 0 deletions src/RustPlusBot.Domain/Workspace/ProvisionedCategory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
namespace RustPlusBot.Domain.Workspace;

/// <summary>A Discord category the bot has provisioned. One per scope (global or per-server).</summary>
public sealed class ProvisionedCategory
{
/// <summary>Surrogate primary key.</summary>
public Guid Id { get; set; } = Guid.NewGuid();

/// <summary>The owning Discord guild snowflake.</summary>
public ulong GuildId { get; set; }

/// <summary>The server this category belongs to, or <c>null</c> for the global category.</summary>
public Guid? RustServerId { get; set; }

/// <summary>The provisioned Discord category snowflake.</summary>
public ulong DiscordCategoryId { get; set; }

/// <summary>When the record was first created (UTC).</summary>
public DateTimeOffset CreatedAt { get; set; }
}
23 changes: 23 additions & 0 deletions src/RustPlusBot.Domain/Workspace/ProvisionedChannel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace RustPlusBot.Domain.Workspace;

/// <summary>A Discord text channel the bot has provisioned, identified by its stable spec key.</summary>
public sealed class ProvisionedChannel
{
/// <summary>Surrogate primary key.</summary>
public Guid Id { get; set; } = Guid.NewGuid();

/// <summary>The owning Discord guild snowflake.</summary>
public ulong GuildId { get; set; }

/// <summary>The server this channel belongs to, or <c>null</c> for a global channel.</summary>
public Guid? RustServerId { get; set; }

/// <summary>The stable spec key (e.g. "information", "setup", "settings", "info").</summary>
public string ChannelKey { get; set; } = string.Empty;

/// <summary>The provisioned Discord channel snowflake.</summary>
public ulong DiscordChannelId { get; set; }

/// <summary>When the record was first created (UTC).</summary>
public DateTimeOffset CreatedAt { get; set; }
}
29 changes: 29 additions & 0 deletions src/RustPlusBot.Domain/Workspace/ProvisionedMessage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
namespace RustPlusBot.Domain.Workspace;

/// <summary>An anchored bot message, edited in place rather than re-posted.</summary>
public sealed class ProvisionedMessage
{
/// <summary>Surrogate primary key.</summary>
public Guid Id { get; set; } = Guid.NewGuid();

/// <summary>The owning Discord guild snowflake.</summary>
public ulong GuildId { get; set; }

/// <summary>The server this message belongs to, or <c>null</c> for a global message.</summary>
public Guid? RustServerId { get; set; }

/// <summary>The stable spec key (e.g. "information.main", "settings.main", "server.info").</summary>
public string MessageKey { get; set; } = string.Empty;

/// <summary>The channel the message lives in.</summary>
public ulong DiscordChannelId { get; set; }

/// <summary>The anchored message snowflake.</summary>
public ulong DiscordMessageId { get; set; }

/// <summary>When the record was first created (UTC).</summary>
public DateTimeOffset CreatedAt { get; set; }

/// <summary>When the message was last edited in place (UTC).</summary>
public DateTimeOffset UpdatedAt { get; set; }
}
3 changes: 3 additions & 0 deletions src/RustPlusBot.Features.Workspace/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("RustPlusBot.Features.Workspace.Tests")]
Loading
Loading