Skip to content

Commit

Permalink
Add #insiders-info interactions (#255)
Browse files Browse the repository at this point in the history
* Add #insiders-info interactions

* Restrict /send-insiders-info-buttons to Trial Moderators

* Add config value for insiderChat

Co-authored-by: Erisa A <erisa@erisa.uk>

* 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 <erisa@erisa.uk>
  • Loading branch information
FloatingMilkshake and Erisa authored Jan 3, 2025
1 parent 6938b9e commit f21ccb9
Show file tree
Hide file tree
Showing 6 changed files with 245 additions and 6 deletions.
12 changes: 10 additions & 2 deletions CommandChecks/HomeServerPerms.cs
Original file line number Diff line number Diff line change
Expand Up @@ -137,14 +137,22 @@ public override async Task<bool> ExecuteCheckAsync(CommandContext ctx, bool help
public class SlashRequireHomeserverPermAttribute : SlashCheckBaseAttribute
{
public ServerPermLevel TargetLvl;
public bool OwnerOverride;

public SlashRequireHomeserverPermAttribute(ServerPermLevel targetlvl)
=> TargetLvl = targetlvl;
public SlashRequireHomeserverPermAttribute(ServerPermLevel targetlvl, bool ownerOverride = false)
{
TargetLvl = targetlvl;
OwnerOverride = ownerOverride;
}

public override async Task<bool> ExecuteChecksAsync(InteractionContext ctx)
{
if (ctx.Guild.Id != Program.cfgjson.ServerID)
return false;

// bot owners can bypass perm checks ONLY if the command allows it.
if (OwnerOverride && Program.cfgjson.BotOwners.Contains(ctx.User.Id))
return true;

var level = await GetPermLevelAsync(ctx.Member);
if (level >= TargetLvl)
Expand Down
4 changes: 2 additions & 2 deletions Commands/InteractionCommands/AnnouncementInteractions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ public async Task AnnounceBuildSlashCommand(InteractionContext ctx,
[Option("lockdown", "Set 0 to not lock. Lock the channel for a certain period of time after announcing the build.")] string lockdownTime = "auto"
)
{
if (Program.cfgjson.InsiderCommandLockedToChannel != 0 && ctx.Channel.Id != Program.cfgjson.InsiderCommandLockedToChannel)
if (Program.cfgjson.InsidersChannel != 0 && ctx.Channel.Id != Program.cfgjson.InsidersChannel)
{
await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} This command only works in <#{Program.cfgjson.InsiderCommandLockedToChannel}>!", ephemeral: true);
await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} This command only works in <#{Program.cfgjson.InsidersChannel}>!", ephemeral: true);
return;
}

Expand Down
39 changes: 39 additions & 0 deletions Commands/InteractionCommands/InsidersInteractions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
namespace Cliptok.Commands.InteractionCommands
{
public class InsidersInteractions : ApplicationCommandModule
{
[SlashCommand("send-insiders-info-buttons", "Sends a message with buttons to get Insider roles for #insiders-info.", false)]
[SlashRequireHomeserverPerm(ServerPermLevel.Admin, ownerOverride: true), SlashCommandPermissions(permissions: DiscordPermission.ModerateMembers)]
public static async Task SendInsidersInfoButtonMessage(InteractionContext ctx)
{
if (Program.cfgjson.InsiderInfoChannel != 0 && ctx.Channel.Id != Program.cfgjson.InsiderInfoChannel)
{
await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} This command only works in <#{Program.cfgjson.InsiderInfoChannel}>!", ephemeral: true);
return;
}

DiscordComponent[] buttons =
[
new DiscordButtonComponent(DiscordButtonStyle.Primary, "insiders-info-roles-menu-callback", "Choose your Insider roles"),
new DiscordButtonComponent(DiscordButtonStyle.Secondary, "insiders-info-chat-btn-callback", "I just want to chat for now")
];

string insidersChannelMention;
if (Program.cfgjson.InsidersChannel == 0)
{
insidersChannelMention = "#insiders";
Program.discord.Logger.LogWarning("#insiders-info message sent with hardcoded #insiders mention! Is insidersChannel set in config.json?");
}
else
{
insidersChannelMention = $"<#{Program.cfgjson.InsidersChannel}>";
}

var builder = new DiscordInteractionResponseBuilder()
.WithContent($"{Program.cfgjson.Emoji.Insider} Choose your Insider roles here! Or, you can choose to chat in {insidersChannelMention} without being notified about new builds.")
.AddComponents(buttons);

await ctx.CreateResponseAsync(builder);
}
}
}
176 changes: 176 additions & 0 deletions Events/InteractionEvents.cs
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,182 @@ await LogChannelHelper.LogDeletedMessagesAsync(
// Respond
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}"));
}
else if (e.Id == "insiders-info-roles-menu-callback")
{
// Shows a menu in #insider-info that allows a user to toggle their Insider roles

// Defer interaction
await e.Interaction.DeferAsync(ephemeral: true);

// Fetch member
var member = await e.Guild.GetMemberAsync(e.User.Id);

// Fetch Insider roles to check whether member already has them
var insiderCanaryRole = await e.Guild.GetRoleAsync(cfgjson.UserRoles.InsiderCanary);
var insiderDevRole = await e.Guild.GetRoleAsync(cfgjson.UserRoles.InsiderDev);
var insiderBetaRole = await e.Guild.GetRoleAsync(cfgjson.UserRoles.InsiderBeta);
var insiderRPRole = await e.Guild.GetRoleAsync(cfgjson.UserRoles.InsiderRP);
var insider10RPRole = await e.Guild.GetRoleAsync(cfgjson.UserRoles.Insider10RP);
var patchTuesdayRole = await e.Guild.GetRoleAsync(cfgjson.UserRoles.PatchTuesday);

// Show menu with current Insider roles, apply new roles based on user selection
var menu = new DiscordSelectComponent("insiders-info-roles-menu-response-callback", "Choose your Insider roles",
new List<DiscordSelectComponentOption>()
{
new("Windows 11 Canary channel", "insiders-info-w11-canary", isDefault: member.Roles.Contains(insiderCanaryRole)),
new("Windows 11 Dev channel", "insiders-info-w11-dev", isDefault: member.Roles.Contains(insiderDevRole)),
new("Windows 11 Beta channel", "insiders-info-w11-beta", isDefault: member.Roles.Contains(insiderBetaRole)),
new("Windows 11 Release Preview channel", "insiders-info-w11-rp", isDefault: member.Roles.Contains(insiderRPRole)),
new("Windows 10 Release Preview channel", "insiders-info-w10-rp", isDefault: member.Roles.Contains(insider10RPRole)),
new("Patch Tuesday", "insiders-info-pt", isDefault: member.Roles.Contains(patchTuesdayRole)),
}, minOptions: 0, maxOptions: 6);

var builder = new DiscordFollowupMessageBuilder()
.WithContent($"{cfgjson.Emoji.Insider} Use the menu below to toggle your Insider roles!")
.AddComponents(menu)
.AsEphemeral(true);

await e.Interaction.CreateFollowupMessageAsync(builder);
}
else if (e.Id == "insiders-info-roles-menu-response-callback")
{
// User has selected new Insider roles w/ menu above
// Compare selection against current roles; add or remove roles as necessary to match selection

// Defer
await e.Interaction.DeferAsync(ephemeral: true);

// Get member
var member = await e.Guild.GetMemberAsync(e.User.Id);

// Map role select options to role IDs
var insiderRoles = new Dictionary<string, ulong>
{
{ "insiders-info-w11-canary", cfgjson.UserRoles.InsiderCanary },
{ "insiders-info-w11-dev", cfgjson.UserRoles.InsiderDev },
{ "insiders-info-w11-beta", cfgjson.UserRoles.InsiderBeta },
{ "insiders-info-w11-rp", cfgjson.UserRoles.InsiderRP },
{ "insiders-info-w10-rp", cfgjson.UserRoles.Insider10RP },
{ "insiders-info-pt", cfgjson.UserRoles.PatchTuesday }
};

// Get a list of the member's current roles that we can add to or remove from
// Then we can apply this in a single request with member.ReplaceRolesAsync to avoid making repeated member update requests
List<DiscordRole> memberRoles = member.Roles.ToList();

var selection = e.Values.Select(x => insiderRoles[x]).ToList();

foreach (var roleId in insiderRoles.Values)
{
var role = await e.Guild.GetRoleAsync(roleId);

if (selection.Contains(roleId))
{
// Member should have the role
if (!memberRoles.Contains(role))
memberRoles.Add(role);
}
else
{
// Member should not have the role
if (memberRoles.Contains(role))
memberRoles.Remove(role);
}
}

await member.ReplaceRolesAsync(memberRoles, "Applying Insider roles chosen in #insiders-info");

await e.Interaction.CreateFollowupMessageAsync(new DiscordFollowupMessageBuilder().WithContent($"{cfgjson.Emoji.Success} Your Insider roles have been updated!").AsEphemeral(true));
}
else if (e.Id == "insiders-info-chat-btn-callback")
{
// Button in #insiders-info that checks whether user has 'insiderChat' role and asks them to confirm granting/revoking it

// Defer
await e.Interaction.DeferAsync(ephemeral: true);

// Get member
var member = await e.Guild.GetMemberAsync(e.User.Id);

// Get insider chat role
var insiderChatRole = await e.Guild.GetRoleAsync(cfgjson.UserRoles.InsiderChat);

// Check whether member already has any insider roles
var insiderRoles = new List<ulong>()
{
cfgjson.UserRoles.InsiderCanary,
cfgjson.UserRoles.InsiderDev,
cfgjson.UserRoles.InsiderBeta,
cfgjson.UserRoles.InsiderRP,
cfgjson.UserRoles.Insider10RP,
cfgjson.UserRoles.PatchTuesday
};
if (member.Roles.Any(x => insiderRoles.Contains(x.Id)))
{
// Member already has an insider role, thus already has access to #insiders
// No need for the chat role too

string insidersMention;
if (cfgjson.InsidersChannel == 0)
insidersMention = "#insiders";
else
insidersMention = $"<#{cfgjson.InsidersChannel}>";

await e.Interaction.CreateFollowupMessageAsync(new DiscordFollowupMessageBuilder()
.WithContent($"You already have Insider roles, so you already have access to chat in {insidersMention}!")
.AsEphemeral(true));

return;
}

if (member.Roles.Contains(insiderChatRole))
{
// Member already has the role
// Ask them if they'd like to remove it
var confirmResponse = new DiscordFollowupMessageBuilder()
.WithContent($"{cfgjson.Emoji.Warning} You already have the {insiderChatRole.Mention} role! Would you like to remove it?")
.AddComponents(new DiscordButtonComponent(DiscordButtonStyle.Danger, "insiders-info-chat-btn-remove-confirm-callback", "Remove"));

await e.Interaction.CreateFollowupMessageAsync(confirmResponse);
}
else
{
// Member does not have the role; show a confirmation message with a button that will give it to them
await e.Interaction.CreateFollowupMessageAsync(new DiscordFollowupMessageBuilder()
.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.")
.AddComponents(new DiscordButtonComponent(DiscordButtonStyle.Secondary, "insiders-info-chat-btn-confirm-callback", "I understand")));
}
}
else if (e.Id == "insiders-info-chat-btn-confirm-callback")
{
// Confirmation for granting insiderChat role, see above

// Defer
await e.Interaction.CreateResponseAsync(DiscordInteractionResponseType.DeferredMessageUpdate);

// Give member insider chat role
var member = await e.Guild.GetMemberAsync(e.User.Id);
var insiderChatRole = await e.Guild.GetRoleAsync(cfgjson.UserRoles.InsiderChat);
await member.GrantRoleAsync(insiderChatRole);

// Respond
await e.Interaction.EditFollowupMessageAsync(e.Message.Id, new DiscordWebhookBuilder().WithContent($"{cfgjson.Emoji.Success} You have been given the {insiderChatRole.Mention} role!"));
}
else if (e.Id == "insiders-info-chat-btn-remove-confirm-callback")
{
// Confirmation for revoking insiderChat role, see above

// Defer
await e.Interaction.CreateResponseAsync(DiscordInteractionResponseType.DeferredMessageUpdate);

// Get member
var member = await e.Guild.GetMemberAsync(e.User.Id);

var insiderChatRole = await e.Guild.GetRoleAsync(cfgjson.UserRoles.InsiderChat);
await member.RevokeRoleAsync(insiderChatRole);

await e.Interaction.EditFollowupMessageAsync(e.Message.Id, new DiscordWebhookBuilder().WithContent($"{cfgjson.Emoji.Success} You have been removed from the {insiderChatRole.Mention} role!"));
}
else
{
await e.Interaction.CreateResponseAsync(DiscordInteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().WithContent("Unknown interaction. I don't know what you are asking me for.").AsEphemeral(true));
Expand Down
16 changes: 15 additions & 1 deletion Structs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -262,12 +262,23 @@ public class ConfigJson

[JsonProperty("forumIntroPosts")]
public List<ulong> ForumIntroPosts { get; private set; } = new();

[JsonProperty("insiderInfoChannel")]
public ulong InsiderInfoChannel { get; private set; }

[JsonProperty("insiderAnnouncementChannel")]
public ulong InsiderAnnouncementChannel { get; private set; } = 0;

private ulong insidersChannel;
[JsonProperty("insidersChannel")]
public ulong InsidersChannel
{
get => insidersChannel == 0 ? InsiderCommandLockedToChannel : insidersChannel;
private set => insidersChannel = value;
}

[JsonProperty("insiderCommandLockedToChannel")]
public ulong InsiderCommandLockedToChannel { get; private set; } = 0;
private ulong InsiderCommandLockedToChannel { get; set; } = 0;

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

[JsonProperty("insider10RP")]
public ulong Insider10RP { get; private set; }

[JsonProperty("insiderChat")]
public ulong InsiderChat { get; private set; }

[JsonProperty("patchTuesday")]
public ulong PatchTuesday { get; private set; }
Expand Down
4 changes: 3 additions & 1 deletion config.json
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@
"insiderBeta": 643712828217360394,
"insiderRP": 288729239921098752,
"insider10RP": 910319453491839069,
"insiderChat": 1323636793668800547,
"patchTuesday": 445773142233710594,
"giveaways": 1169336992455200820
},
Expand Down Expand Up @@ -315,7 +316,8 @@
1065659890036646019
],
"insiderAnnouncementChannel": 1043898319883219004,
"insiderCommandLockedToChannel": 187649467611086849,
"insiderInfoChannel": 1279201622651572317,
"insidersChannel": 187649467611086849,
"dmAutoresponseTimeLimit": 6,
"autoDeleteEmptyThreads": true,
"insiderCanaryThread": 1082394217168523315,
Expand Down

0 comments on commit f21ccb9

Please sign in to comment.