@@ -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