Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge Slash Commands #167

Draft
wants to merge 4 commits into
base: development
Choose a base branch
from
Draft
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Implement slash warn checking allowing ephemeral/private check in guilds
Blackcatmaxy committed Oct 5, 2023

Verified

This commit was signed with the committer’s verified signature.
Blackcatmaxy Daniel Bereza
commit 888f46211d1c27a81fbf20dab8dfba8b648216ae
125 changes: 125 additions & 0 deletions src/BotCatMaxy/Components/CommandHandling/CommandModule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
using System;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using Discord;
using Discord.Interactions;
using Discord.WebSocket;
using Humanizer;
#nullable enable
namespace BotCatMaxy.Components.CommandHandling;

public class CommandModule : InteractionModuleBase<IInteractionContext>
{
// Insanity that this has to be made
// Purposefully not allowing ephemeral because those are not real messages
private async Task<IUserMessage> RespondWithMessageAsync(string message, Embed embed = null, MessageComponent component = null)
{
await RespondAsync(message, embed: embed, components: component);
return await Context.Interaction.GetOriginalResponseAsync();
}

protected async Task<IUserMessage> DeferWithMessageAsync()
{
await DeferAsync();
return await Context.Interaction.GetOriginalResponseAsync();
}

/// <summary>
/// Confirms a user isn't already under a specific TempAct and displays a confirmation to overwrite if they are.
/// </summary>
/// <param name="actions">The list of TempActions to be searched.</param>
/// <param name="newAction">The TempAction to be compared with the old action.</param>
/// <param name="user">If an <see cref="IUser"/> is included show on embed.</param>
/// <typeparam name="TAct">The type of act which should be able to be inferred and just serve for type safety.</typeparam>
/// <returns>A Tuple so so that the result can be used and action can be removed.</returns>
/*public async Task<(CommandResult result, TAct action)?> ConfirmNoTempAct<TAct>(IReadOnlyList<TAct>? actions, TAct newAction, UserRef user) where TAct : TempAction
{
if (actions?.Count is null or 0 || actions.Any(action => action.UserId == user.ID) == false)
return null;
TAct? oldAction = null;
foreach (var action in actions)
{
if (action.UserId == user.ID)
oldAction = action;
}
if (oldAction == null)
return null;
string oldInfo =
$"{oldAction.Type} for {oldAction.Length.LimitedHumanize()} for `{oldAction.Reason}` started {MentionTime(oldAction.Start, 'R')} and ending {MentionTime(oldAction.EndTime, 'R')}.";
// var page = new PageBuilder()
// .WithColor(Color.Blue)
// .AddField($"Current Action:", oldInfo)
// .AddField("Overwrite With:", $"{newAction.Type} starting {MentionTime(newAction.Start, 'R')} and ending {MentionTime(newAction.EndTime, 'R')}.")
// .WithTitle($"Are you sure you want to overwrite existing {newAction.Type}?");
//
//
// if (user.User != null)
// page.WithAuthor(user.User);
//
// var selection = new ButtonSelectionBuilder<string>()
// .AddUser(Context.User)
// .WithSelectionPage(page)
// .AddOption(new ButtonOption<string>("Confirm", ButtonStyle.Danger))
// .AddOption(new ButtonOption<string>("Cancel", ButtonStyle.Secondary))
// .WithActionOnCancellation(ActionOnStop.DisableInput)
// .WithActionOnSuccess(ActionOnStop.DisableInput)
// .WithActionOnTimeout(ActionOnStop.DisableInput)
// .WithStringConverter(x => x.Option)
// .Build();
//
// var result = await Interactivity.SendSelectionAsync(selection, Context.Channel, TimeSpan.FromMinutes(1));
//
// if (result.IsSuccess && result.Value!.Option == "Confirm")
// return (null, oldAction);
// return (CommandResult.FromError("Command has been canceled."), null);
}*/

/// <summary>
/// Asks the user what server they want to be talking about if they are not currently in a server.
/// </summary>
/// <returns>An <see cref="IGuild"/> or <c>null</c> if canceled.</returns>
protected async Task<IGuild?> QueryMutualGuild()
{
if (Context.Guild != null)
return Context.Guild;

var mutualGuilds = (await Context.User.GetMutualGuildsAsync(Context.Client)).ToImmutableArray();
var menu = new SelectMenuBuilder()
.WithPlaceholder("Select server in dropdown")
// required menu
.WithCustomId("guild-selection");
for (var i = 0; i < mutualGuilds.Length; i++)
{
string name = mutualGuilds[i].Name.Truncate(30, "...");
string id = mutualGuilds[i].Id.ToString();
menu.AddOption($"{name}", id, $"{name} guild");
//Could pad ID right, but honestly is it worth it?
}

var component = new ComponentBuilder()
.WithSelectMenu(menu);
var message = await RespondWithMessageAsync("Select server to view", component: component.Build());
var interaction = await InteractionUtility.WaitForMessageComponentAsync(Context.Client as BaseSocketClient, message, TimeSpan.FromMinutes(5));
var response = (interaction as SocketMessageComponent)?.Data?.Values?.First();
if (response != null && ulong.TryParse(response, out ulong parsedId))
{
await interaction.DeferAsync(ephemeral: true);
return await Context.Client.GetGuildAsync(parsedId);
}

return null;
}

/// <summary>
/// Format a <see cref="DateTimeOffset"/> for Discord client rendering.
/// </summary>
/// <param name="offset">The <see cref="DateTimeOffset"/> to be formatted.</param>
/// <param name="styleChar">The <a href="https://discord.com/developers/docs/reference#message-formatting-formats">character to set the style</a> on the client </param>
public string MentionTime(DateTimeOffset offset, char? styleChar = null)
{
string style = (styleChar != null) ? $":{styleChar}" : "";
return $"<t:{offset.ToUnixTimeSeconds()}{style}>";
}
}
13 changes: 5 additions & 8 deletions src/BotCatMaxy/Components/CommandHandling/SlashCommandHandler.cs
Original file line number Diff line number Diff line change
@@ -40,7 +40,12 @@ public async Task InitializeAsync(CancellationToken cancellationToken)
{
socketClient.Ready += async () =>
{
#if DEBUG
await _interaction.RegisterCommandsToGuildAsync(285529027383525376);
#else
await interactionService.RegisterCommandsGloballyAsync();
#endif
LogSeverity.Info.Log("CMDs", "Registered commands");
};
// Hook the MessageReceived event into our command handler
socketClient.InteractionCreated += async (x) =>
@@ -52,14 +57,6 @@ public async Task InitializeAsync(CancellationToken cancellationToken)

_interaction.SlashCommandExecuted += CommandExecuted;

//Adds custom type readers
// _interaction.addty
// _interaction.AddTypeReader(typeof(Emoji), new EmojiTypeReader());
// _interaction.AddTypeReader(typeof(UserRef), new UserRefTypeReader());
// _interaction.AddTypeReader(typeof(IUser), new BetterUserTypeReader());
// _interaction.AddTypeReader(typeof(TimeSpan), new TimeSpanTypeReader(), true);
// _interaction.AddTypeReader(typeof(CommandInfo[]), new CommandTypeReader());

// See Dependency Injection guide for more information.
await _interaction.AddModulesAsync(assembly: Assembly.GetAssembly(typeof(Program)),
services: _services);
77 changes: 24 additions & 53 deletions src/BotCatMaxy/Components/Moderation/SlashModerationCommands.cs
Original file line number Diff line number Diff line change
@@ -17,24 +17,21 @@
using IResult = Discord.Interactions.IResult;
using RunMode = Discord.Commands.RunMode;
using RuntimeResult = Discord.Interactions.RuntimeResult;
#nullable enable

namespace BotCatMaxy
{
[Name("Moderation")]
public class SlashModerationCommands : InteractionModuleBase
public class SlashModerationCommands : CommandModule
{
public async Task<RuntimeResult> ExecuteWarnAsync(IUser user, float size, string reason)
{
var warns = user.Id.LoadInfractions(Context.Guild);
string modifier = (size != 1) ? $"(size of `{size}x`) " : "";
// await RespondAsync($"{user.Mention} is being given their `{warns.Count.Suffix()}` warning {modifier}because of `{reason}`.");
await DeferAsync();
var message = await Context.Interaction.GetOriginalResponseAsync();
var message = await DeferWithMessageAsync();
IUserMessage logMessage = await DiscordLogging.LogWarn(Context.Guild, Context.User, user.Id, reason, message.GetJumpUrl());
WarnResult result = await user.Id.Warn(size, reason, Context.Channel as ITextChannel, user, logMessage?.GetJumpUrl());

if (result.success)
{
string modifier = (size != 1) ? $"(size of `{size}x`) " : "";
return CommandResult.FromSuccess($"{user.Mention} has been given their `{result.warnsAmount.Suffix()}` warning {modifier}because of `{reason}`.");
}

@@ -47,67 +44,44 @@ public async Task<RuntimeResult> ExecuteWarnAsync(IUser user, float size, string
}
return CommandResult.FromError(result.description.Truncate(1500));
}

// [SlashCommand("warn", "warn a user")]
// [Discord.Commands.Summary("Warn a user with a reason.")]
// [CanWarn()]
// public Task<RuntimeResult> WarnUserAsync([RequireHierarchy] IUser user, [Remainder] string reason)
// => ExecuteWarnAsync(user, 1, reason);
//

[SlashCommand("warn", "warn a user")]
[Discord.Commands.Summary("Warn a user with a specific size, along with a reason.")]
[Discord.Interactions.CanWarn()]
public Task WarnUserWithSizeAsync([Discord.Interactions.RequireHierarchy] IUser user, string reason, float size = 1)
=> ExecuteWarnAsync(user, size, reason);

#nullable enable
[SlashCommand("dmwarns", "warn a user")]
[Discord.Commands.Summary("Views a user's infractions.")]
[Discord.Commands.RequireContext(ContextType.DM, ErrorMessage = "This command now only works in the bot's DMs")]
[Alias("dminfractions", "dmwarnings", "warns", "infractions", "warnings")]
public async Task<RuntimeResult> DMUserWarnsAsync(IUser? user = null, int amount = 50)
[SlashCommand("displaywarns", "show a user's warns")]
[Discord.Interactions.CanWarn]
public async Task<RuntimeResult> DisplayUserWarnsAsync(IUser? user = null, int amount = 5)
{
if (amount < 1)
return CommandResult.FromError("Why would you want to see that many infractions?");

// var guild = await QueryMutualGuild();
// if (guild == null)
return CommandResult.FromError("You have timed out or canceled.");
//
// user ??= new IUser(Context.Message.Author);
// List<Infraction> infractions = user.LoadInfractions(guild, false);
// if (infractions?.Count is null or 0)
// {
// var message = $"{user.Mention()} has no infractions";
// if (user.User == null)
// message += " or doesn't exist";
// return CommandResult.FromSuccess(message);
// }
//
// user = user with { GuildUser = await guild.GetUserAsync(user.ID) };
// var embed = infractions.GetEmbed(user, guild, amount: amount);
// return CommandResult.FromSuccess($"Here are {user.Mention()}'s {((amount < infractions.Count) ? $"last {amount} out of " : "")}{"infraction".ToQuantity(infractions.Count)}",
// embed: embed);
user ??= Context.User;
List<Infraction> infractions = user.Id.LoadInfractions(Context.Guild, false);
if (infractions?.Count is null or 0)
return CommandResult.FromSuccess($"{user.Mention} has no infractions.");

var embed = infractions.GetEmbed(user, Context.Guild, amount: amount);
return CommandResult.FromSuccess($"Here are {user.Mention}'s {((amount < infractions.Count) ? $"last {amount} out of " : "")}{"infraction".ToQuantity(infractions.Count)}",
embed: embed);
}
#nullable disable

[SlashCommand("warns", "view a user's warns")]
[Discord.Interactions.CanWarn]
[Alias("infractions", "warnings")]
public async Task CheckUserWarnsAsync(IUser user = null, int amount = 5)
public async Task CheckUserWarnsAsync(IUser? user = null, int amount = 5)
{
// user ??= new IUser(Context.User as IGuildUser);
List<Infraction> infractions = user.Id.LoadInfractions(Context.Guild, false);
user ??= Context.User;
var guild = await QueryMutualGuild();
List<Infraction> infractions = user.Id.LoadInfractions(guild, false);
if (infractions?.Count is null or 0)
{
await RespondAsync($"{user.Username} has no infractions");
await FollowupAsync($"{user.Username} has no infractions", ephemeral: true);
return;
}
await RespondAsync(embed: infractions.GetEmbed(user, Context.Guild, amount: amount, showLinks: true));
await FollowupAsync(embed: infractions.GetEmbed(user, guild, amount: amount, showLinks: true), ephemeral: true);
}

[SlashCommand("removewarn", "remove a user's warn")]
[Alias("warnremove", "removewarning")]
[HasAdmin()]
public async Task<RuntimeResult> RemoveWarnAsync([Discord.Interactions.RequireHierarchy] IUser user, int index)
{
@@ -131,10 +105,9 @@ public async Task<RuntimeResult> RemoveWarnAsync([Discord.Interactions.RequireHi

[SlashCommand("kickwarn", "kick a user and warn them with an optional reason")]
[Discord.Commands.Summary("Kicks a user, and warns them with an optional reason.")]
[Alias("warnkick", "warnandkick", "kickandwarn")]
[Discord.Commands.RequireContext(ContextType.Guild)]
[Discord.Commands.RequireUserPermission(GuildPermission.KickMembers)]
public async Task KickAndWarn([Discord.Interactions.RequireHierarchy] SocketGuildUser user, [Remainder] string reason = "Unspecified")
public async Task KickAndWarn([Discord.Interactions.RequireHierarchy] SocketGuildUser user, string reason = "Unspecified")
{
var invocation = await Context.Interaction.GetOriginalResponseAsync();
await user.Warn(1, reason, Context.Channel as ITextChannel, "Discord");
@@ -195,9 +168,7 @@ public async Task KickAndWarn([Discord.Interactions.RequireHierarchy] SocketGuil
// }

[SlashCommand("delete", "clear a specific number of messages")]
[Discord.Commands.Summary("Clear a specific number of messages between 0-300.")]
[Alias("clean", "clear", "deletemany", "purge")]
[Discord.Commands.RequireUserPermission(ChannelPermission.ManageMessages)]
[Discord.Interactions.RequireUserPermission(ChannelPermission.ManageMessages)]
public async Task DeleteMany(uint number, IUser user = null)
{
if (number == 0 || number > 300)