-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathAlarmComponentModule.cs
More file actions
219 lines (195 loc) · 9.43 KB
/
Copy pathAlarmComponentModule.cs
File metadata and controls
219 lines (195 loc) · 9.43 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
using System.Globalization;
using Discord;
using Discord.Interactions;
using Microsoft.Extensions.DependencyInjection;
using RustPlusBot.Features.Alarms.Pairing;
using RustPlusBot.Features.Alarms.Relaying;
using RustPlusBot.Features.Alarms.Rendering;
using RustPlusBot.Persistence.Alarms;
namespace RustPlusBot.Features.Alarms.Modules;
/// <summary>Thin handler for the #alarms pairing prompt + control buttons + rename modal. Any guild member.</summary>
/// <param name="scopeFactory">Creates a short-lived DI scope per interaction.</param>
/// <param name="refresher">Re-renders the alarm embed on demand.</param>
public sealed class AlarmComponentModule(
IServiceScopeFactory scopeFactory,
IAlarmRefresher refresher) : InteractionModuleBase<SocketInteractionContext>
{
private const string InvalidControlMessage = "That control wasn't valid.";
/// <summary>Accepts a pending pairing prompt and starts managing the alarm.</summary>
/// <param name="tail">The "{serverId}:{entityId}" custom-id tail.</param>
[ComponentInteraction(AlarmComponentIds.AcceptPrefix + "*")]
public async Task AcceptAsync(string tail)
{
if (!TryParse(tail, out var serverId, out var entityId) || Context.Guild is null)
{
await RespondAsync(InvalidControlMessage, ephemeral: true).ConfigureAwait(false);
return;
}
await DeferAsync(ephemeral: true).ConfigureAwait(false);
var scope = scopeFactory.CreateAsyncScope();
await using (scope.ConfigureAwait(false))
{
var coordinator = scope.ServiceProvider.GetRequiredService<AlarmPairingCoordinator>();
var accepted = await coordinator
.TryAcceptAsync(Context.Guild.Id, serverId, entityId, Context.User.Id, CancellationToken.None)
.ConfigureAwait(false);
await FollowupAsync(accepted ? "Alarm added." : "That alarm is already managed.", ephemeral: true)
.ConfigureAwait(false);
}
}
/// <summary>Dismisses a pending pairing prompt and removes the transient prompt message.</summary>
/// <param name="tail">The "{serverId}:{entityId}" custom-id tail.</param>
[ComponentInteraction(AlarmComponentIds.DismissPrefix + "*")]
public async Task DismissAsync(string tail)
{
if (!TryParse(tail, out var serverId, out var entityId) || Context.Guild is null)
{
await RespondAsync(InvalidControlMessage, ephemeral: true).ConfigureAwait(false);
return;
}
var scope = scopeFactory.CreateAsyncScope();
await using (scope.ConfigureAwait(false))
{
var coordinator = scope.ServiceProvider.GetRequiredService<AlarmPairingCoordinator>();
coordinator.TryDismiss(Context.Guild.Id, serverId, entityId);
}
// Delete the actual prompt message that hosts this button (the component interaction's source
// message) — not the ephemeral interaction response. Best-effort: a delete failure is non-fatal.
await DeletePromptMessageSafeAsync().ConfigureAwait(false);
await RespondAsync("Dismissed.", ephemeral: true).ConfigureAwait(false);
}
/// <summary>Toggles the @everyone ping setting for this alarm.</summary>
/// <param name="tail">The "{serverId}:{entityId}" custom-id tail.</param>
[ComponentInteraction(AlarmComponentIds.PingTogglePrefix + "*")]
public async Task PingToggleAsync(string tail)
{
if (!TryParse(tail, out var serverId, out var entityId) || Context.Guild is null)
{
await RespondAsync(InvalidControlMessage, ephemeral: true).ConfigureAwait(false);
return;
}
await DeferAsync(ephemeral: true).ConfigureAwait(false);
var scope = scopeFactory.CreateAsyncScope();
await using (scope.ConfigureAwait(false))
{
var store = scope.ServiceProvider.GetRequiredService<IAlarmStore>();
var current = await store.GetAsync(Context.Guild.Id, serverId, entityId, CancellationToken.None)
.ConfigureAwait(false);
if (current is null)
{
await FollowupAsync("That alarm isn't managed.", ephemeral: true).ConfigureAwait(false);
return;
}
await store
.SetPingEveryoneAsync(Context.Guild.Id, serverId, entityId, !current.PingEveryone,
CancellationToken.None)
.ConfigureAwait(false);
}
await refresher.RefreshAsync(Context.Guild.Id, serverId, entityId, unreachable: false, CancellationToken.None)
.ConfigureAwait(false);
await FollowupAsync("Updated.", ephemeral: true).ConfigureAwait(false);
}
/// <summary>Toggles the relay-to-team-chat setting for this alarm.</summary>
/// <param name="tail">The "{serverId}:{entityId}" custom-id tail.</param>
[ComponentInteraction(AlarmComponentIds.RelayTogglePrefix + "*")]
public async Task RelayToggleAsync(string tail)
{
if (!TryParse(tail, out var serverId, out var entityId) || Context.Guild is null)
{
await RespondAsync(InvalidControlMessage, ephemeral: true).ConfigureAwait(false);
return;
}
await DeferAsync(ephemeral: true).ConfigureAwait(false);
var scope = scopeFactory.CreateAsyncScope();
await using (scope.ConfigureAwait(false))
{
var store = scope.ServiceProvider.GetRequiredService<IAlarmStore>();
var current = await store.GetAsync(Context.Guild.Id, serverId, entityId, CancellationToken.None)
.ConfigureAwait(false);
if (current is null)
{
await FollowupAsync("That alarm isn't managed.", ephemeral: true).ConfigureAwait(false);
return;
}
await store
.SetRelayToTeamChatAsync(Context.Guild.Id, serverId, entityId, !current.RelayToTeamChat,
CancellationToken.None)
.ConfigureAwait(false);
}
await refresher.RefreshAsync(Context.Guild.Id, serverId, entityId, unreachable: false, CancellationToken.None)
.ConfigureAwait(false);
await FollowupAsync("Updated.", ephemeral: true).ConfigureAwait(false);
}
/// <summary>Opens the rename modal, carrying the target tail in the modal custom id.</summary>
/// <param name="tail">The "{serverId}:{entityId}" custom-id tail.</param>
[ComponentInteraction(AlarmComponentIds.RenamePrefix + "*")]
public async Task RenamePromptAsync(string tail)
{
if (!TryParse(tail, out _, out _) || Context.Guild is null)
{
await RespondAsync(InvalidControlMessage, ephemeral: true).ConfigureAwait(false);
return;
}
// The modal id carries the same tail so the submit handler can route.
await RespondWithModalAsync<AlarmRenameModal>(AlarmComponentIds.RenameModalPrefix + tail)
.ConfigureAwait(false);
}
/// <summary>Persists the new name, then refreshes the alarm embed.</summary>
/// <param name="tail">The "{serverId}:{entityId}" custom-id tail.</param>
/// <param name="modal">The submitted rename modal.</param>
[ModalInteraction(AlarmComponentIds.RenameModalPrefix + "*")]
public async Task RenameSubmitAsync(string tail, AlarmRenameModal modal)
{
ArgumentNullException.ThrowIfNull(modal);
if (!TryParse(tail, out var serverId, out var entityId) || Context.Guild is null)
{
await RespondAsync(InvalidControlMessage, ephemeral: true).ConfigureAwait(false);
return;
}
var name = string.IsNullOrWhiteSpace(modal.Name)
? "Alarm " + entityId.ToString(CultureInfo.InvariantCulture)
: modal.Name.Trim();
await DeferAsync(ephemeral: true).ConfigureAwait(false);
var scope = scopeFactory.CreateAsyncScope();
await using (scope.ConfigureAwait(false))
{
var store = scope.ServiceProvider.GetRequiredService<IAlarmStore>();
await store.RenameAsync(Context.Guild.Id, serverId, entityId, name, CancellationToken.None)
.ConfigureAwait(false);
}
await refresher.RefreshAsync(Context.Guild.Id, serverId, entityId, unreachable: false, CancellationToken.None)
.ConfigureAwait(false);
await FollowupAsync("Renamed.", ephemeral: true).ConfigureAwait(false);
}
private static bool TryParse(string tail, out Guid serverId, out ulong entityId)
{
serverId = Guid.Empty;
entityId = 0UL;
if (tail is null)
{
return false;
}
var parts = tail.Split(':');
return parts.Length == 2
&& Guid.TryParse(parts[0], out serverId)
&& ulong.TryParse(parts[1], NumberStyles.None, CultureInfo.InvariantCulture, out entityId);
}
private async Task DeletePromptMessageSafeAsync()
{
try
{
// The source message of a component interaction is the prompt that carries the button.
if (Context.Interaction is IComponentInteraction component)
{
await component.Message.DeleteAsync().ConfigureAwait(false);
}
}
#pragma warning disable CA1031 // Best-effort prompt cleanup; a delete failure is non-fatal.
catch (Exception ex)
#pragma warning restore CA1031
{
// Ignore: the prompt is transient and harmless if it lingers.
_ = ex;
}
}
}