Skip to content

Commit f21ccb9

Browse files
Add #insiders-info interactions (#255)
* Add #insiders-info interactions * Restrict /send-insiders-info-buttons to Trial Moderators * Add config value for insiderChat Co-authored-by: Erisa A <[email protected]> * Add OwnerOverride to SlashRequireHomeserverPerm & /send-insiders-info-buttons * Check whether insidersChannel is set in config before using it * Check whether user already has insider roles when giving insiderChat * config: Make insiderCommandLockedToChannel an alias of insidersChannel Also changes usages to InsidersChannel; config.json accepts both, but only InsidersChannel can be used in code --------- Co-authored-by: Erisa A <[email protected]>
1 parent 6938b9e commit f21ccb9

File tree

6 files changed

+245
-6
lines changed

6 files changed

+245
-6
lines changed

CommandChecks/HomeServerPerms.cs

+10-2
Original file line numberDiff line numberDiff line change
@@ -137,14 +137,22 @@ public override async Task<bool> ExecuteCheckAsync(CommandContext ctx, bool help
137137
public class SlashRequireHomeserverPermAttribute : SlashCheckBaseAttribute
138138
{
139139
public ServerPermLevel TargetLvl;
140+
public bool OwnerOverride;
140141

141-
public SlashRequireHomeserverPermAttribute(ServerPermLevel targetlvl)
142-
=> TargetLvl = targetlvl;
142+
public SlashRequireHomeserverPermAttribute(ServerPermLevel targetlvl, bool ownerOverride = false)
143+
{
144+
TargetLvl = targetlvl;
145+
OwnerOverride = ownerOverride;
146+
}
143147

144148
public override async Task<bool> ExecuteChecksAsync(InteractionContext ctx)
145149
{
146150
if (ctx.Guild.Id != Program.cfgjson.ServerID)
147151
return false;
152+
153+
// bot owners can bypass perm checks ONLY if the command allows it.
154+
if (OwnerOverride && Program.cfgjson.BotOwners.Contains(ctx.User.Id))
155+
return true;
148156

149157
var level = await GetPermLevelAsync(ctx.Member);
150158
if (level >= TargetLvl)

Commands/InteractionCommands/AnnouncementInteractions.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,9 @@ public async Task AnnounceBuildSlashCommand(InteractionContext ctx,
3434
[Option("lockdown", "Set 0 to not lock. Lock the channel for a certain period of time after announcing the build.")] string lockdownTime = "auto"
3535
)
3636
{
37-
if (Program.cfgjson.InsiderCommandLockedToChannel != 0 && ctx.Channel.Id != Program.cfgjson.InsiderCommandLockedToChannel)
37+
if (Program.cfgjson.InsidersChannel != 0 && ctx.Channel.Id != Program.cfgjson.InsidersChannel)
3838
{
39-
await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} This command only works in <#{Program.cfgjson.InsiderCommandLockedToChannel}>!", ephemeral: true);
39+
await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} This command only works in <#{Program.cfgjson.InsidersChannel}>!", ephemeral: true);
4040
return;
4141
}
4242

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
namespace Cliptok.Commands.InteractionCommands
2+
{
3+
public class InsidersInteractions : ApplicationCommandModule
4+
{
5+
[SlashCommand("send-insiders-info-buttons", "Sends a message with buttons to get Insider roles for #insiders-info.", false)]
6+
[SlashRequireHomeserverPerm(ServerPermLevel.Admin, ownerOverride: true), SlashCommandPermissions(permissions: DiscordPermission.ModerateMembers)]
7+
public static async Task SendInsidersInfoButtonMessage(InteractionContext ctx)
8+
{
9+
if (Program.cfgjson.InsiderInfoChannel != 0 && ctx.Channel.Id != Program.cfgjson.InsiderInfoChannel)
10+
{
11+
await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} This command only works in <#{Program.cfgjson.InsiderInfoChannel}>!", ephemeral: true);
12+
return;
13+
}
14+
15+
DiscordComponent[] buttons =
16+
[
17+
new DiscordButtonComponent(DiscordButtonStyle.Primary, "insiders-info-roles-menu-callback", "Choose your Insider roles"),
18+
new DiscordButtonComponent(DiscordButtonStyle.Secondary, "insiders-info-chat-btn-callback", "I just want to chat for now")
19+
];
20+
21+
string insidersChannelMention;
22+
if (Program.cfgjson.InsidersChannel == 0)
23+
{
24+
insidersChannelMention = "#insiders";
25+
Program.discord.Logger.LogWarning("#insiders-info message sent with hardcoded #insiders mention! Is insidersChannel set in config.json?");
26+
}
27+
else
28+
{
29+
insidersChannelMention = $"<#{Program.cfgjson.InsidersChannel}>";
30+
}
31+
32+
var builder = new DiscordInteractionResponseBuilder()
33+
.WithContent($"{Program.cfgjson.Emoji.Insider} Choose your Insider roles here! Or, you can choose to chat in {insidersChannelMention} without being notified about new builds.")
34+
.AddComponents(buttons);
35+
36+
await ctx.CreateResponseAsync(builder);
37+
}
38+
}
39+
}

Events/InteractionEvents.cs

+176
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,182 @@ await LogChannelHelper.LogDeletedMessagesAsync(
200200
// Respond
201201
await e.Message.ModifyAsync(new DiscordMessageBuilder().WithContent($"{cfgjson.Emoji.Success} Override successfully added. <@{newOverwrite.Id}> already had an override in <#{pendingOverride.ChannelId}>, so here are their new permissions:\n**Allowed:** {newOverwrite.Allowed}\n**Denied:** {newOverwrite.Denied}"));
202202
}
203+
else if (e.Id == "insiders-info-roles-menu-callback")
204+
{
205+
// Shows a menu in #insider-info that allows a user to toggle their Insider roles
206+
207+
// Defer interaction
208+
await e.Interaction.DeferAsync(ephemeral: true);
209+
210+
// Fetch member
211+
var member = await e.Guild.GetMemberAsync(e.User.Id);
212+
213+
// Fetch Insider roles to check whether member already has them
214+
var insiderCanaryRole = await e.Guild.GetRoleAsync(cfgjson.UserRoles.InsiderCanary);
215+
var insiderDevRole = await e.Guild.GetRoleAsync(cfgjson.UserRoles.InsiderDev);
216+
var insiderBetaRole = await e.Guild.GetRoleAsync(cfgjson.UserRoles.InsiderBeta);
217+
var insiderRPRole = await e.Guild.GetRoleAsync(cfgjson.UserRoles.InsiderRP);
218+
var insider10RPRole = await e.Guild.GetRoleAsync(cfgjson.UserRoles.Insider10RP);
219+
var patchTuesdayRole = await e.Guild.GetRoleAsync(cfgjson.UserRoles.PatchTuesday);
220+
221+
// Show menu with current Insider roles, apply new roles based on user selection
222+
var menu = new DiscordSelectComponent("insiders-info-roles-menu-response-callback", "Choose your Insider roles",
223+
new List<DiscordSelectComponentOption>()
224+
{
225+
new("Windows 11 Canary channel", "insiders-info-w11-canary", isDefault: member.Roles.Contains(insiderCanaryRole)),
226+
new("Windows 11 Dev channel", "insiders-info-w11-dev", isDefault: member.Roles.Contains(insiderDevRole)),
227+
new("Windows 11 Beta channel", "insiders-info-w11-beta", isDefault: member.Roles.Contains(insiderBetaRole)),
228+
new("Windows 11 Release Preview channel", "insiders-info-w11-rp", isDefault: member.Roles.Contains(insiderRPRole)),
229+
new("Windows 10 Release Preview channel", "insiders-info-w10-rp", isDefault: member.Roles.Contains(insider10RPRole)),
230+
new("Patch Tuesday", "insiders-info-pt", isDefault: member.Roles.Contains(patchTuesdayRole)),
231+
}, minOptions: 0, maxOptions: 6);
232+
233+
var builder = new DiscordFollowupMessageBuilder()
234+
.WithContent($"{cfgjson.Emoji.Insider} Use the menu below to toggle your Insider roles!")
235+
.AddComponents(menu)
236+
.AsEphemeral(true);
237+
238+
await e.Interaction.CreateFollowupMessageAsync(builder);
239+
}
240+
else if (e.Id == "insiders-info-roles-menu-response-callback")
241+
{
242+
// User has selected new Insider roles w/ menu above
243+
// Compare selection against current roles; add or remove roles as necessary to match selection
244+
245+
// Defer
246+
await e.Interaction.DeferAsync(ephemeral: true);
247+
248+
// Get member
249+
var member = await e.Guild.GetMemberAsync(e.User.Id);
250+
251+
// Map role select options to role IDs
252+
var insiderRoles = new Dictionary<string, ulong>
253+
{
254+
{ "insiders-info-w11-canary", cfgjson.UserRoles.InsiderCanary },
255+
{ "insiders-info-w11-dev", cfgjson.UserRoles.InsiderDev },
256+
{ "insiders-info-w11-beta", cfgjson.UserRoles.InsiderBeta },
257+
{ "insiders-info-w11-rp", cfgjson.UserRoles.InsiderRP },
258+
{ "insiders-info-w10-rp", cfgjson.UserRoles.Insider10RP },
259+
{ "insiders-info-pt", cfgjson.UserRoles.PatchTuesday }
260+
};
261+
262+
// Get a list of the member's current roles that we can add to or remove from
263+
// Then we can apply this in a single request with member.ReplaceRolesAsync to avoid making repeated member update requests
264+
List<DiscordRole> memberRoles = member.Roles.ToList();
265+
266+
var selection = e.Values.Select(x => insiderRoles[x]).ToList();
267+
268+
foreach (var roleId in insiderRoles.Values)
269+
{
270+
var role = await e.Guild.GetRoleAsync(roleId);
271+
272+
if (selection.Contains(roleId))
273+
{
274+
// Member should have the role
275+
if (!memberRoles.Contains(role))
276+
memberRoles.Add(role);
277+
}
278+
else
279+
{
280+
// Member should not have the role
281+
if (memberRoles.Contains(role))
282+
memberRoles.Remove(role);
283+
}
284+
}
285+
286+
await member.ReplaceRolesAsync(memberRoles, "Applying Insider roles chosen in #insiders-info");
287+
288+
await e.Interaction.CreateFollowupMessageAsync(new DiscordFollowupMessageBuilder().WithContent($"{cfgjson.Emoji.Success} Your Insider roles have been updated!").AsEphemeral(true));
289+
}
290+
else if (e.Id == "insiders-info-chat-btn-callback")
291+
{
292+
// Button in #insiders-info that checks whether user has 'insiderChat' role and asks them to confirm granting/revoking it
293+
294+
// Defer
295+
await e.Interaction.DeferAsync(ephemeral: true);
296+
297+
// Get member
298+
var member = await e.Guild.GetMemberAsync(e.User.Id);
299+
300+
// Get insider chat role
301+
var insiderChatRole = await e.Guild.GetRoleAsync(cfgjson.UserRoles.InsiderChat);
302+
303+
// Check whether member already has any insider roles
304+
var insiderRoles = new List<ulong>()
305+
{
306+
cfgjson.UserRoles.InsiderCanary,
307+
cfgjson.UserRoles.InsiderDev,
308+
cfgjson.UserRoles.InsiderBeta,
309+
cfgjson.UserRoles.InsiderRP,
310+
cfgjson.UserRoles.Insider10RP,
311+
cfgjson.UserRoles.PatchTuesday
312+
};
313+
if (member.Roles.Any(x => insiderRoles.Contains(x.Id)))
314+
{
315+
// Member already has an insider role, thus already has access to #insiders
316+
// No need for the chat role too
317+
318+
string insidersMention;
319+
if (cfgjson.InsidersChannel == 0)
320+
insidersMention = "#insiders";
321+
else
322+
insidersMention = $"<#{cfgjson.InsidersChannel}>";
323+
324+
await e.Interaction.CreateFollowupMessageAsync(new DiscordFollowupMessageBuilder()
325+
.WithContent($"You already have Insider roles, so you already have access to chat in {insidersMention}!")
326+
.AsEphemeral(true));
327+
328+
return;
329+
}
330+
331+
if (member.Roles.Contains(insiderChatRole))
332+
{
333+
// Member already has the role
334+
// Ask them if they'd like to remove it
335+
var confirmResponse = new DiscordFollowupMessageBuilder()
336+
.WithContent($"{cfgjson.Emoji.Warning} You already have the {insiderChatRole.Mention} role! Would you like to remove it?")
337+
.AddComponents(new DiscordButtonComponent(DiscordButtonStyle.Danger, "insiders-info-chat-btn-remove-confirm-callback", "Remove"));
338+
339+
await e.Interaction.CreateFollowupMessageAsync(confirmResponse);
340+
}
341+
else
342+
{
343+
// Member does not have the role; show a confirmation message with a button that will give it to them
344+
await e.Interaction.CreateFollowupMessageAsync(new DiscordFollowupMessageBuilder()
345+
.WithContent($"{cfgjson.Emoji.Warning} Please note that <#{cfgjson.InsidersChannel}> is **not for tech support**! If you need tech support, please ask in the appropriate channels instead. Press the button to acknowledge this and get the {insiderChatRole.Mention} role.")
346+
.AddComponents(new DiscordButtonComponent(DiscordButtonStyle.Secondary, "insiders-info-chat-btn-confirm-callback", "I understand")));
347+
}
348+
}
349+
else if (e.Id == "insiders-info-chat-btn-confirm-callback")
350+
{
351+
// Confirmation for granting insiderChat role, see above
352+
353+
// Defer
354+
await e.Interaction.CreateResponseAsync(DiscordInteractionResponseType.DeferredMessageUpdate);
355+
356+
// Give member insider chat role
357+
var member = await e.Guild.GetMemberAsync(e.User.Id);
358+
var insiderChatRole = await e.Guild.GetRoleAsync(cfgjson.UserRoles.InsiderChat);
359+
await member.GrantRoleAsync(insiderChatRole);
360+
361+
// Respond
362+
await e.Interaction.EditFollowupMessageAsync(e.Message.Id, new DiscordWebhookBuilder().WithContent($"{cfgjson.Emoji.Success} You have been given the {insiderChatRole.Mention} role!"));
363+
}
364+
else if (e.Id == "insiders-info-chat-btn-remove-confirm-callback")
365+
{
366+
// Confirmation for revoking insiderChat role, see above
367+
368+
// Defer
369+
await e.Interaction.CreateResponseAsync(DiscordInteractionResponseType.DeferredMessageUpdate);
370+
371+
// Get member
372+
var member = await e.Guild.GetMemberAsync(e.User.Id);
373+
374+
var insiderChatRole = await e.Guild.GetRoleAsync(cfgjson.UserRoles.InsiderChat);
375+
await member.RevokeRoleAsync(insiderChatRole);
376+
377+
await e.Interaction.EditFollowupMessageAsync(e.Message.Id, new DiscordWebhookBuilder().WithContent($"{cfgjson.Emoji.Success} You have been removed from the {insiderChatRole.Mention} role!"));
378+
}
203379
else
204380
{
205381
await e.Interaction.CreateResponseAsync(DiscordInteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().WithContent("Unknown interaction. I don't know what you are asking me for.").AsEphemeral(true));

Structs.cs

+15-1
Original file line numberDiff line numberDiff line change
@@ -262,12 +262,23 @@ public class ConfigJson
262262

263263
[JsonProperty("forumIntroPosts")]
264264
public List<ulong> ForumIntroPosts { get; private set; } = new();
265+
266+
[JsonProperty("insiderInfoChannel")]
267+
public ulong InsiderInfoChannel { get; private set; }
265268

266269
[JsonProperty("insiderAnnouncementChannel")]
267270
public ulong InsiderAnnouncementChannel { get; private set; } = 0;
271+
272+
private ulong insidersChannel;
273+
[JsonProperty("insidersChannel")]
274+
public ulong InsidersChannel
275+
{
276+
get => insidersChannel == 0 ? InsiderCommandLockedToChannel : insidersChannel;
277+
private set => insidersChannel = value;
278+
}
268279

269280
[JsonProperty("insiderCommandLockedToChannel")]
270-
public ulong InsiderCommandLockedToChannel { get; private set; } = 0;
281+
private ulong InsiderCommandLockedToChannel { get; set; } = 0;
271282

272283
[JsonProperty("dmAutoresponseTimeLimit")]
273284
public int DmAutoresponseTimeLimit { get; private set; } = 0;
@@ -489,6 +500,9 @@ public class UserRoleConfig
489500

490501
[JsonProperty("insider10RP")]
491502
public ulong Insider10RP { get; private set; }
503+
504+
[JsonProperty("insiderChat")]
505+
public ulong InsiderChat { get; private set; }
492506

493507
[JsonProperty("patchTuesday")]
494508
public ulong PatchTuesday { get; private set; }

config.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@
228228
"insiderBeta": 643712828217360394,
229229
"insiderRP": 288729239921098752,
230230
"insider10RP": 910319453491839069,
231+
"insiderChat": 1323636793668800547,
231232
"patchTuesday": 445773142233710594,
232233
"giveaways": 1169336992455200820
233234
},
@@ -315,7 +316,8 @@
315316
1065659890036646019
316317
],
317318
"insiderAnnouncementChannel": 1043898319883219004,
318-
"insiderCommandLockedToChannel": 187649467611086849,
319+
"insiderInfoChannel": 1279201622651572317,
320+
"insidersChannel": 187649467611086849,
319321
"dmAutoresponseTimeLimit": 6,
320322
"autoDeleteEmptyThreads": true,
321323
"insiderCanaryThread": 1082394217168523315,

0 commit comments

Comments
 (0)