Skip to content

Commit e3e7a70

Browse files
committed
feat: add /workspace repair, rebuild, and purge commands
1 parent 7d5a443 commit e3e7a70

1 file changed

Lines changed: 128 additions & 0 deletions

File tree

src/RustPlusBot.Features.Workspace/Modules/WorkspaceAdminModule.cs

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ public sealed class WorkspaceAdminModule(
2323
/// <summary>Custom id for the reset confirmation button.</summary>
2424
public const string ConfirmResetId = "workspace:reset:confirm";
2525

26+
/// <summary>Custom id for the rebuild confirmation button.</summary>
27+
public const string ConfirmRebuildId = "workspace:rebuild:confirm";
28+
29+
/// <summary>Custom id for the purge confirmation button.</summary>
30+
public const string ConfirmPurgeId = "workspace:purge:confirm";
31+
2632
/// <summary>Prompts to delete the entire provisioned workspace (dev-gated).</summary>
2733
[SlashCommand("reset", "Delete ALL of the bot's channels and records in this Discord server (dangerous)")]
2834
public async Task ResetAsync()
@@ -98,6 +104,128 @@ await FollowupAsync($"Registered **{name}** and published ServerRegisteredEvent.
98104
}
99105
}
100106

107+
/// <summary>Recreates any missing categories/channels/messages without deleting data.</summary>
108+
[SlashCommand("repair", "Recreate any missing bot channels without deleting data")]
109+
public async Task RepairAsync()
110+
{
111+
if (Context.Guild is null)
112+
{
113+
await RespondAsync("This command must be used in a server.", ephemeral: true).ConfigureAwait(false);
114+
return;
115+
}
116+
117+
await DeferAsync(ephemeral: true).ConfigureAwait(false);
118+
var scope = scopeFactory.CreateAsyncScope();
119+
try
120+
{
121+
var reconciler = scope.ServiceProvider.GetRequiredService<IWorkspaceReconciler>();
122+
await reconciler.HealGuildAsync(Context.Guild.Id).ConfigureAwait(false);
123+
await FollowupAsync("Workspace repaired.", ephemeral: true).ConfigureAwait(false);
124+
}
125+
finally
126+
{
127+
await scope.DisposeAsync().ConfigureAwait(false);
128+
}
129+
}
130+
131+
/// <summary>Prompts to delete and re-provision the entire workspace (dev-gated).</summary>
132+
[SlashCommand("rebuild", "Delete and re-create all the bot's channels here (dangerous)")]
133+
public async Task RebuildAsync()
134+
{
135+
if (!await EnsureEnabledAsync().ConfigureAwait(false))
136+
{
137+
return;
138+
}
139+
140+
var components = new ComponentBuilder()
141+
.WithButton("Confirm rebuild", ConfirmRebuildId, ButtonStyle.Danger)
142+
.Build();
143+
await RespondAsync(
144+
"This deletes every provisioned channel and re-creates them from scratch. Confirm?",
145+
ephemeral: true, components: components).ConfigureAwait(false);
146+
}
147+
148+
/// <summary>Executes the rebuild after confirmation.</summary>
149+
[ComponentInteraction(ConfirmRebuildId)]
150+
public async Task ConfirmRebuildAsync()
151+
{
152+
if (!await EnsureEnabledAsync().ConfigureAwait(false))
153+
{
154+
return;
155+
}
156+
157+
await DeferAsync(ephemeral: true).ConfigureAwait(false);
158+
var scope = scopeFactory.CreateAsyncScope();
159+
try
160+
{
161+
var teardown = scope.ServiceProvider.GetRequiredService<IWorkspaceTeardownService>();
162+
var reconciler = scope.ServiceProvider.GetRequiredService<IWorkspaceReconciler>();
163+
var servers = scope.ServiceProvider.GetRequiredService<IServerService>();
164+
165+
await teardown.ResetGuildAsync(Context.Guild.Id).ConfigureAwait(false);
166+
167+
var result = await reconciler.ReconcileGlobalAsync(Context.Guild.Id).ConfigureAwait(false);
168+
if (result.Status == ReconcileStatus.MissingPermissions)
169+
{
170+
await FollowupAsync(
171+
$"I'm missing required permissions: {string.Join(", ", result.MissingPermissions)}.",
172+
ephemeral: true).ConfigureAwait(false);
173+
return;
174+
}
175+
176+
foreach (var server in await servers.ListAsync(Context.Guild.Id).ConfigureAwait(false))
177+
{
178+
await reconciler.ReconcileServerAsync(Context.Guild.Id, server.Id).ConfigureAwait(false);
179+
}
180+
181+
await FollowupAsync("Workspace rebuilt.", ephemeral: true).ConfigureAwait(false);
182+
}
183+
finally
184+
{
185+
await scope.DisposeAsync().ConfigureAwait(false);
186+
}
187+
}
188+
189+
/// <summary>Prompts to delete all of this guild's data (dev-gated).</summary>
190+
[SlashCommand("purge", "Delete ALL of this server's bot data (servers, settings, channels) (dangerous)")]
191+
public async Task PurgeAsync()
192+
{
193+
if (!await EnsureEnabledAsync().ConfigureAwait(false))
194+
{
195+
return;
196+
}
197+
198+
var components = new ComponentBuilder()
199+
.WithButton("Confirm purge", ConfirmPurgeId, ButtonStyle.Danger)
200+
.Build();
201+
await RespondAsync(
202+
"This deletes every server, setting, and channel the bot stores for this Discord server. Confirm?",
203+
ephemeral: true, components: components).ConfigureAwait(false);
204+
}
205+
206+
/// <summary>Executes the purge after confirmation.</summary>
207+
[ComponentInteraction(ConfirmPurgeId)]
208+
public async Task ConfirmPurgeAsync()
209+
{
210+
if (!await EnsureEnabledAsync().ConfigureAwait(false))
211+
{
212+
return;
213+
}
214+
215+
await DeferAsync(ephemeral: true).ConfigureAwait(false);
216+
var scope = scopeFactory.CreateAsyncScope();
217+
try
218+
{
219+
var purge = scope.ServiceProvider.GetRequiredService<IGuildPurgeService>();
220+
await purge.PurgeGuildAsync(Context.Guild.Id).ConfigureAwait(false);
221+
await FollowupAsync("Guild data purged.", ephemeral: true).ConfigureAwait(false);
222+
}
223+
finally
224+
{
225+
await scope.DisposeAsync().ConfigureAwait(false);
226+
}
227+
}
228+
101229
private async Task<bool> EnsureEnabledAsync()
102230
{
103231
if (Context.Guild is null)

0 commit comments

Comments
 (0)