-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathWorkspaceAdminModule.cs
More file actions
245 lines (217 loc) · 9.45 KB
/
Copy pathWorkspaceAdminModule.cs
File metadata and controls
245 lines (217 loc) · 9.45 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
using Discord;
using Discord.Interactions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using RustPlusBot.Abstractions.Events;
using RustPlusBot.Features.Workspace.Reconciler;
using RustPlusBot.Features.Workspace.Teardown;
using RustPlusBot.Persistence.Servers;
namespace RustPlusBot.Features.Workspace.Modules;
/// <summary>Administrative and developer commands for the workspace.</summary>
/// <param name="scopeFactory">Creates a short-lived DI scope per interaction.</param>
/// <param name="options">Workspace options (gates the dangerous commands).</param>
/// <param name="eventBus">Used to publish the stub <see cref="ServerRegisteredEvent"/>.</param>
[Group("workspace", "Workspace administration")]
[RequireUserPermission(GuildPermission.ManageGuild)]
public sealed class WorkspaceAdminModule(
IServiceScopeFactory scopeFactory,
IOptions<WorkspaceOptions> options,
IEventBus eventBus) : InteractionModuleBase<SocketInteractionContext>
{
/// <summary>Custom id for the reset confirmation button.</summary>
public const string ConfirmResetId = "workspace:reset:confirm";
/// <summary>Custom id for the rebuild confirmation button.</summary>
public const string ConfirmRebuildId = "workspace:rebuild:confirm";
/// <summary>Custom id for the purge confirmation button.</summary>
public const string ConfirmPurgeId = "workspace:purge:confirm";
/// <summary>Prompts to delete the entire provisioned workspace (dev-gated).</summary>
[SlashCommand("reset", "Delete ALL of the bot's channels and records in this Discord server (dangerous)")]
public async Task ResetAsync()
{
if (!await EnsureEnabledAsync().ConfigureAwait(false))
{
return;
}
var components = new ComponentBuilder()
.WithButton("Confirm reset", ConfirmResetId, ButtonStyle.Danger)
.Build();
await RespondAsync(
"This deletes every channel and category the bot provisioned here. Confirm?",
ephemeral: true, components: components).ConfigureAwait(false);
}
/// <summary>Executes the workspace reset after confirmation.</summary>
[ComponentInteraction(ConfirmResetId)]
public async Task ConfirmResetAsync()
{
if (!await EnsureEnabledAsync().ConfigureAwait(false))
{
return;
}
await DeferAsync(ephemeral: true).ConfigureAwait(false);
var scope = scopeFactory.CreateAsyncScope();
try
{
var teardown = scope.ServiceProvider.GetRequiredService<IWorkspaceTeardownService>();
await teardown.ResetGuildAsync(Context.Guild.Id).ConfigureAwait(false);
await FollowupAsync("Workspace reset.", ephemeral: true).ConfigureAwait(false);
}
finally
{
await scope.DisposeAsync().ConfigureAwait(false);
}
}
/// <summary>Dev: registers a fake server and publishes a ServerRegisteredEvent to test provisioning.</summary>
/// <param name="name">Server display name.</param>
/// <param name="ip">Server host or ip.</param>
/// <param name="port">Rust+ app port.</param>
[SlashCommand("simulate-server", "Dev: register a fake server to test provisioning")]
public async Task SimulateServerAsync(string name, string ip, int port)
{
if (!await EnsureEnabledAsync().ConfigureAwait(false))
{
return;
}
if (port is < 1 or > 65535)
{
await RespondAsync("Port must be between 1 and 65535.", ephemeral: true).ConfigureAwait(false);
return;
}
await DeferAsync(ephemeral: true).ConfigureAwait(false);
var scope = scopeFactory.CreateAsyncScope();
try
{
var servers = scope.ServiceProvider.GetRequiredService<IServerService>();
var server = await servers.AddAsync(Context.Guild.Id, Context.User.Id, name, ip, port)
.ConfigureAwait(false);
await eventBus.PublishAsync(new ServerRegisteredEvent(Context.Guild.Id, server.Id)).ConfigureAwait(false);
await FollowupAsync($"Registered **{name}** and published ServerRegisteredEvent.", ephemeral: true)
.ConfigureAwait(false);
}
finally
{
await scope.DisposeAsync().ConfigureAwait(false);
}
}
/// <summary>Recreates any missing categories/channels/messages without deleting data.</summary>
[SlashCommand("repair", "Recreate any missing bot channels without deleting data")]
public async Task RepairAsync()
{
if (Context.Guild is null)
{
await RespondAsync("This command must be used in a server.", ephemeral: true).ConfigureAwait(false);
return;
}
await DeferAsync(ephemeral: true).ConfigureAwait(false);
var scope = scopeFactory.CreateAsyncScope();
try
{
var reconciler = scope.ServiceProvider.GetRequiredService<IWorkspaceReconciler>();
await reconciler.HealGuildAsync(Context.Guild.Id).ConfigureAwait(false);
await FollowupAsync("Workspace repaired.", ephemeral: true).ConfigureAwait(false);
}
finally
{
await scope.DisposeAsync().ConfigureAwait(false);
}
}
/// <summary>Prompts to delete and re-provision the entire workspace (dev-gated).</summary>
[SlashCommand("rebuild", "Delete and re-create all the bot's channels here (dangerous)")]
public async Task RebuildAsync()
{
if (!await EnsureEnabledAsync().ConfigureAwait(false))
{
return;
}
var components = new ComponentBuilder()
.WithButton("Confirm rebuild", ConfirmRebuildId, ButtonStyle.Danger)
.Build();
await RespondAsync(
"This deletes every provisioned channel and re-creates them from scratch. Confirm?",
ephemeral: true, components: components).ConfigureAwait(false);
}
/// <summary>Executes the rebuild after confirmation.</summary>
[ComponentInteraction(ConfirmRebuildId)]
public async Task ConfirmRebuildAsync()
{
if (!await EnsureEnabledAsync().ConfigureAwait(false))
{
return;
}
await DeferAsync(ephemeral: true).ConfigureAwait(false);
var scope = scopeFactory.CreateAsyncScope();
try
{
var teardown = scope.ServiceProvider.GetRequiredService<IWorkspaceTeardownService>();
var reconciler = scope.ServiceProvider.GetRequiredService<IWorkspaceReconciler>();
var servers = scope.ServiceProvider.GetRequiredService<IServerService>();
await teardown.ResetGuildAsync(Context.Guild.Id).ConfigureAwait(false);
var result = await reconciler.ReconcileGlobalAsync(Context.Guild.Id).ConfigureAwait(false);
if (result.Status == ReconcileStatus.MissingPermissions)
{
await FollowupAsync(
$"I'm missing required permissions: {string.Join(", ", result.MissingPermissions)}.",
ephemeral: true).ConfigureAwait(false);
return;
}
foreach (var server in await servers.ListAsync(Context.Guild.Id).ConfigureAwait(false))
{
await reconciler.ReconcileServerAsync(Context.Guild.Id, server.Id).ConfigureAwait(false);
}
await FollowupAsync("Workspace rebuilt.", ephemeral: true).ConfigureAwait(false);
}
finally
{
await scope.DisposeAsync().ConfigureAwait(false);
}
}
/// <summary>Prompts to delete all of this guild's data (dev-gated).</summary>
[SlashCommand("purge", "Delete ALL of this server's bot data (servers, settings, channels) (dangerous)")]
public async Task PurgeAsync()
{
if (!await EnsureEnabledAsync().ConfigureAwait(false))
{
return;
}
var components = new ComponentBuilder()
.WithButton("Confirm purge", ConfirmPurgeId, ButtonStyle.Danger)
.Build();
await RespondAsync(
"This deletes every server, setting, and channel the bot stores for this Discord server. Confirm?",
ephemeral: true, components: components).ConfigureAwait(false);
}
/// <summary>Executes the purge after confirmation.</summary>
[ComponentInteraction(ConfirmPurgeId)]
public async Task ConfirmPurgeAsync()
{
if (!await EnsureEnabledAsync().ConfigureAwait(false))
{
return;
}
await DeferAsync(ephemeral: true).ConfigureAwait(false);
var scope = scopeFactory.CreateAsyncScope();
try
{
var purge = scope.ServiceProvider.GetRequiredService<IGuildPurgeService>();
await purge.PurgeGuildAsync(Context.Guild.Id).ConfigureAwait(false);
await FollowupAsync("Guild data purged.", ephemeral: true).ConfigureAwait(false);
}
finally
{
await scope.DisposeAsync().ConfigureAwait(false);
}
}
private async Task<bool> EnsureEnabledAsync()
{
if (Context.Guild is null)
{
await RespondAsync("This command must be used in a server.", ephemeral: true).ConfigureAwait(false);
return false;
}
if (!options.Value.EnableDangerCommands)
{
await RespondAsync("Developer commands are disabled.", ephemeral: true).ConfigureAwait(false);
return false;
}
return true;
}
}