diff --git a/CompatBot/Commands/AutoCompleteProviders/ContentFilterAutoCompleteProvider.cs b/CompatBot/Commands/AutoCompleteProviders/ContentFilterAutoCompleteProvider.cs new file mode 100644 index 00000000..2a96a85c --- /dev/null +++ b/CompatBot/Commands/AutoCompleteProviders/ContentFilterAutoCompleteProvider.cs @@ -0,0 +1,43 @@ +using CompatBot.Database; +using CompatBot.Database.Providers; +using Microsoft.EntityFrameworkCore; + +namespace CompatBot.Commands.AutoCompleteProviders; + +public class ContentFilterAutoCompleteProvider: IAutoCompleteProvider +{ + public async ValueTask> AutoCompleteAsync(AutoCompleteContext context) + { + if (!ModProvider.IsMod(context.User.Id)) + return [new($"{Config.Reactions.Denied} You are not authorized to use this command.", -1)]; + + await using var db = new BotDb(); + IEnumerable<(int id, string trigger)> result; + if (context.UserInput is not {Length: >0} prefix) + result = db.Piracystring + .OrderByDescending(e=>e.Id) + .Take(25) + .AsNoTracking() + .AsEnumerable() + .Select(i => (id: i.Id, trigger:i.String)); + else + { + prefix = prefix.ToLowerInvariant(); + var prefixMatches = db.Piracystring + .Where(i => i.Id.ToString().StartsWith(prefix) || i.String.StartsWith(prefix)) + .Take(25); + var substringMatches= db.Piracystring + .Where(i => i.Id.ToString().Contains(prefix) || i.String.Contains(prefix)) + .Take(25); + result = prefixMatches + .Concat(substringMatches) + .Distinct() + .OrderBy(i => i.Id) + .Take(25) + .AsNoTracking() + .AsEnumerable() + .Select(i => (id: i.Id, trigger: i.String)); + } + return result.Select(i => new DiscordAutoCompleteChoice($"{i.id}: {i.trigger}", i.id)).ToList(); + } +} \ No newline at end of file diff --git a/CompatBot/Commands/AutoCompleteProviders/ExplainAutoCompleteProvider.cs b/CompatBot/Commands/AutoCompleteProviders/ExplainAutoCompleteProvider.cs new file mode 100644 index 00000000..76087083 --- /dev/null +++ b/CompatBot/Commands/AutoCompleteProviders/ExplainAutoCompleteProvider.cs @@ -0,0 +1,54 @@ +using CompatBot.Database; +using Microsoft.EntityFrameworkCore; + +namespace CompatBot.Commands.AutoCompleteProviders; + +public class ExplainAutoCompleteProvider: IAutoCompleteProvider +{ + public async ValueTask> AutoCompleteAsync(AutoCompleteContext context) + { + await using var db = new BotDb(); + IEnumerable result; + if (context.UserInput is not {Length: >0} prefix) + //todo: use tracking stats to get popular entries instead? + result = await db.Explanation + .OrderBy(e=>e.Keyword) + .Take(25) + .Select(e => e.Keyword) + .AsNoTracking() + .ToListAsync() + .ConfigureAwait(false); + else + { + prefix = prefix.ToLowerInvariant(); + var prefixMatches = db.Explanation + .Where(e => e.Keyword.StartsWith(prefix)) + .OrderBy(e => e.Keyword) + .Take(25) + .Select(e => e.Keyword) + .AsNoTracking() + .AsEnumerable(); + var substringMatches= db.Explanation + .Where(e => e.Keyword.Contains(prefix)) + .OrderBy(e => e.Keyword) + .Take(25) + .Select(e => e.Keyword) + .AsNoTracking() + .AsEnumerable(); + var fuzzyMatches = db.Explanation + .Select(e => e.Keyword) + .AsNoTracking() + .AsEnumerable() + .Select(term => new{coeff=term.GetFuzzyCoefficientCached(prefix), term=term}) + .OrderByDescending(pair => pair.coeff) + .Take(25) + .Select(pair => pair.term); + result = prefixMatches + .Concat(substringMatches) + .Concat(fuzzyMatches) + .Distinct() + .Take(25); + } + return result.Select(term => new DiscordAutoCompleteChoice(term, term)).ToList(); + } +} \ No newline at end of file diff --git a/CompatBot/Commands/AutoCompleteProviders/FilterActionAutoCompleteProvider.cs b/CompatBot/Commands/AutoCompleteProviders/FilterActionAutoCompleteProvider.cs new file mode 100644 index 00000000..f52909f8 --- /dev/null +++ b/CompatBot/Commands/AutoCompleteProviders/FilterActionAutoCompleteProvider.cs @@ -0,0 +1,47 @@ +using CompatBot.Database; +using CompatBot.Utils.Extensions; +using TResult = System.Collections.Generic.IEnumerable; + +namespace CompatBot.Commands.AutoCompleteProviders; + +public class FilterActionAutoCompleteProvider: IAutoCompleteProvider +{ + static FilterActionAutoCompleteProvider() + { + var validValues = FilterActionExtensions.ActionFlagValues; + minValue = (int)validValues[0]; + maxValue = (int)validValues.Aggregate((a, b) => a | b); + choiceList = new DiscordAutoCompleteChoice[maxValue+1]; + choiceList[0] = new("Default", 0); + choiceList[maxValue] = new("All", maxValue); + for (var i = minValue; i < maxValue; i++) + choiceList[i] = new($"{((FilterAction)i).ToFlagsString()}: {((FilterAction)i).ToString()}", i); + } + + private static readonly int minValue; + private static readonly int maxValue; + private static readonly DiscordAutoCompleteChoice[] choiceList; + private static readonly char[] Delimiters = { ',', ' ' }; + + public ValueTask AutoCompleteAsync(AutoCompleteContext context) + => ValueTask.FromResult(GetChoices(Parse(context.UserInput))); + + private static int Parse(string? input) + { + if (input is not {Length: >0 and <7}) + return 0; + return (int)input.ToFilterAction(); + } + + private static TResult GetChoices(int start) + { + List result = [choiceList[start]]; + for(var i=minValue; i<=maxValue; i <<= 1) + { + var nextVal = start | i; + if (nextVal != start) + result.Add(choiceList[nextVal]); + } + return result.Take(25); + } +} \ No newline at end of file diff --git a/CompatBot/Commands/ChoiceProviders/CompatListStatusChoiceProvider.cs b/CompatBot/Commands/ChoiceProviders/CompatListStatusChoiceProvider.cs new file mode 100644 index 00000000..3e09c76a --- /dev/null +++ b/CompatBot/Commands/ChoiceProviders/CompatListStatusChoiceProvider.cs @@ -0,0 +1,18 @@ +namespace CompatBot.Commands.ChoiceProviders; + +public class CompatListStatusChoiceProvider : IChoiceProvider +{ + private static readonly IReadOnlyList compatListStatus = + [ + new("playable", "playable"), + new("ingame or better", "ingame"), + new("intro or better", "intro"), + new("loadable or better", "loadable"), + new("only ingame", "ingameOnly"), + new("only intro", "introOnly"), + new("only loadable", "loadableOnly"), + ]; + + public ValueTask> ProvideAsync(CommandParameter parameter) + => ValueTask.FromResult>(compatListStatus); +} diff --git a/CompatBot/Commands/ChoiceProviders/FilterActionChoiceProvider.cs b/CompatBot/Commands/ChoiceProviders/FilterActionChoiceProvider.cs new file mode 100644 index 00000000..51c1e34e --- /dev/null +++ b/CompatBot/Commands/ChoiceProviders/FilterActionChoiceProvider.cs @@ -0,0 +1,20 @@ +using CompatBot.Database; + +namespace CompatBot.Commands.ChoiceProviders; + +public class FilterActionChoiceProvider : IChoiceProvider +{ + private static readonly IReadOnlyList actionType = + [ + new("Default", 0), + new("Remove content", (int)FilterAction.RemoveContent), + new("Warn", (int)FilterAction.IssueWarning), + new("Show explanation", (int)FilterAction.ShowExplain), + new("Send message", (int)FilterAction.SendMessage), + new("No mod log", (int)FilterAction.MuteModQueue), + new("Kick user", (int)FilterAction.Kick), + ]; + + public ValueTask> ProvideAsync(CommandParameter parameter) + => ValueTask.FromResult>(actionType); +} \ No newline at end of file diff --git a/CompatBot/Commands/ChoiceProviders/FilterContextChoiceProvider.cs b/CompatBot/Commands/ChoiceProviders/FilterContextChoiceProvider.cs new file mode 100644 index 00000000..74154f97 --- /dev/null +++ b/CompatBot/Commands/ChoiceProviders/FilterContextChoiceProvider.cs @@ -0,0 +1,17 @@ +using CompatBot.Database; + +namespace CompatBot.Commands.ChoiceProviders; + +public class FilterContextChoiceProvider : IChoiceProvider +{ + private static readonly IReadOnlyList contextType = + [ + new("Default", 0), + new("Chat", (int)FilterContext.Chat), + new("Logs", (int)FilterContext.Log), + new("Both", (int)(FilterContext.Chat | FilterContext.Log)), + ]; + + public ValueTask> ProvideAsync(CommandParameter parameter) + => ValueTask.FromResult>(contextType); +} \ No newline at end of file diff --git a/CompatBot/Commands/ChoiceProviders/ScoreTypeChoiceProvider.cs b/CompatBot/Commands/ChoiceProviders/ScoreTypeChoiceProvider.cs new file mode 100644 index 00000000..bab1e8eb --- /dev/null +++ b/CompatBot/Commands/ChoiceProviders/ScoreTypeChoiceProvider.cs @@ -0,0 +1,14 @@ +namespace CompatBot.Commands.ChoiceProviders; + +public class ScoreTypeChoiceProvider : IChoiceProvider +{ + private static readonly IReadOnlyList scoreType = + [ + new("combined", "both"), + new("critic score", "critic"), + new("user score", "user"), + ]; + + public ValueTask> ProvideAsync(CommandParameter parameter) + => ValueTask.FromResult>(scoreType); +} \ No newline at end of file diff --git a/CompatBot/Commands/CompatList.Top.cs b/CompatBot/Commands/CompatList.Top.cs index e600861b..7a63fe03 100644 --- a/CompatBot/Commands/CompatList.Top.cs +++ b/CompatBot/Commands/CompatList.Top.cs @@ -1,6 +1,6 @@ using CompatApiClient.Utils; +using CompatBot.Commands.ChoiceProviders; using CompatBot.Database; -using DSharpPlus.Commands.Processors.SlashCommands.ArgumentModifiers; using Microsoft.EntityFrameworkCore; namespace CompatBot.Commands; @@ -69,35 +69,5 @@ internal static partial class CompatList else await ctx.RespondAsync("Failed to generate list", ephemeral).ConfigureAwait(false); } - - public class CompatListStatusChoiceProvider : IChoiceProvider - { - private static readonly IReadOnlyList compatListStatus = - [ - new("playable", "playable"), - new("ingame or better", "ingame"), - new("intro or better", "intro"), - new("loadable or better", "loadable"), - new("only ingame", "ingameOnly"), - new("only intro", "introOnly"), - new("only loadable", "loadableOnly"), - ]; - - public ValueTask> ProvideAsync(CommandParameter parameter) - => ValueTask.FromResult>(compatListStatus); - } - - public class ScoreTypeChoiceProvider : IChoiceProvider - { - private static readonly IReadOnlyList scoreType = - [ - new("combined", "both"), - new("critic score", "critic"), - new("user score", "user"), - ]; - - public ValueTask> ProvideAsync(CommandParameter parameter) - => ValueTask.FromResult>(scoreType); - } } } \ No newline at end of file diff --git a/CompatBot/Commands/ContentFilters.cs b/CompatBot/Commands/ContentFilters.cs index 260ba5b8..60dfb535 100644 --- a/CompatBot/Commands/ContentFilters.cs +++ b/CompatBot/Commands/ContentFilters.cs @@ -5,15 +5,20 @@ using System.Text.RegularExpressions; using System.Xml.Linq; using CompatApiClient.Compression; using CompatApiClient.Utils; +using CompatBot.Commands.AutoCompleteProviders; +using CompatBot.Commands.ChoiceProviders; using CompatBot.Database; using CompatBot.Database.Providers; using CompatBot.Utils.Extensions; +using DSharpPlus.Commands.Processors.SlashCommands.ArgumentModifiers; +using DSharpPlus.Interactivity; using Microsoft.EntityFrameworkCore; +using org.mariuszgromada.math.mxparser.parsertokens; using Exception = System.Exception; namespace CompatBot.Commands; -[Command("filters"), TextAlias("piracy", "filter"), RequiresBotSudoerRole, RequiresDm] +[Command("filter"), RequiresBotSudoerRole] [Description("Used to manage content filters. **Works only in DM**")] internal sealed partial class ContentFilters { @@ -25,10 +30,12 @@ internal sealed partial class ContentFilters [GeneratedRegex(@" (\(.+\)\s*\(.+\)|\(\w+(,\s*\w+)+\))\.iso$")] private static partial Regex ExtraIsoInfoPattern(); - [Command("list")] - [Description("Lists all filters")] - public async Task List(CommandContext ctx) + [Command("dump")] + [Description("Saves all filters as a text file attachment")] + public static async ValueTask List(SlashCommandContext ctx) { + var ephemeral = !ctx.Channel.IsPrivate; + await ctx.DeferResponseAsync(ephemeral).ConfigureAwait(false); var table = new AsciiTable( new AsciiColumn("ID", alignToRight: true), new AsciiColumn("Trigger"), @@ -50,7 +57,7 @@ internal sealed partial class ContentFilters foreach (var t in nonUniqueTriggers) { var duplicateFilters = filters.Where(ps => ps.String.Equals(t, StringComparison.InvariantCultureIgnoreCase)).ToList(); - foreach (FilterContext fctx in Enum.GetValues(typeof(FilterContext))) + foreach (FilterContext fctx in FilterActionExtensions.ActionFlagValues) { if (duplicateFilters.Count(f => (f.Context & fctx) == fctx) > 1) { @@ -84,58 +91,107 @@ internal sealed partial class ContentFilters await using (var writer = new StreamWriter(output, leaveOpen: true)) await writer.WriteAsync(result.ToString()).ConfigureAwait(false); output.Seek(0, SeekOrigin.Begin); - await ctx.Channel.SendMessageAsync(new DiscordMessageBuilder().AddFile("filters.txt", output)).ConfigureAwait(false); + var builder = new DiscordInteractionResponseBuilder().AddFile("filters.txt", output); + if (ephemeral) + builder = builder.AsEphemeral(); + await ctx.RespondAsync(builder).ConfigureAwait(false); } - /* - [Command("add"), TextAlias("create")] + [Command("add")] [Description("Adds a new content filter")] - public async Task Add(CommandContext ctx, [RemainingText, Description("A plain string to match")] string? trigger) + public static async ValueTask Add( + SlashCommandContext ctx, + [Description("A plain string to match"), MinMaxLength(minLength: 3)] + string trigger, + //[Description("Context where filter is active (default is Chat and Logs)"), VariadicArgument(2, 0)]IReadOnlyList context, // todo: use this when variadic bugs are fixed + [Description("Context where filter is active (default is Chat and Logs)"), SlashChoiceProvider] + int context = 0, + //[Description("Actions performed by the filter (default is Remove, and Warn with Message)"), VariadicArgument(6, 0)]IReadOnlyList action, + [Description("Actions performed by the filter (default is Remove, and Warn with Message)"), SlashAutoCompleteProvider] + int action = 0, + [Description("Validation regex (use https://regex101.com to test)")] + string? validation = null, + [Description("Custom message to send if `M`essage action was enabled (default is the piracy warning)")] + string? message = null, + [Description("Explanation to send if `E`xplain action was enabled"), SlashAutoCompleteProvider] + string? explanation = null + ) { - trigger ??= ""; + var ephemeral = !ctx.Channel.IsPrivate; + if (validation is { Length: > 0 }) + { + try + { + _ = Regex.IsMatch( + trigger, + validation, + RegexOptions.Multiline | RegexOptions.IgnoreCase, + TimeSpan.FromMilliseconds(100) + ); + } + catch (Exception e) + { + await ctx.RespondAsync($"❌ Invalid regex expression: {e.Message}", ephemeral: ephemeral).ConfigureAwait(false); + return; + } + } + + explanation = explanation?.ToLowerInvariant(); await using var db = new BotDb(); - Piracystring? filter; + if (explanation is {Length: >0} && !await db.Explanation.AnyAsync(e => e.Keyword == explanation).ConfigureAwait(false)) + { + await ctx.RespondAsync($"❌ Unknown explanation term: {explanation}", ephemeral: ephemeral).ConfigureAwait(false); + return; + } + var isNewFilter = true; - if (string.IsNullOrEmpty(trigger)) - filter = new() {String = trigger}; + var filter = await db.Piracystring.FirstOrDefaultAsync(ps => ps.String == trigger && ps.Disabled).ConfigureAwait(false); + if (filter is null) + filter = new() { String = trigger }; else { - filter = await db.Piracystring.FirstOrDefaultAsync(ps => ps.String == trigger && ps.Disabled).ConfigureAwait(false); - if (filter == null) - filter = new() {String = trigger}; - else - { - filter.Disabled = false; - isNewFilter = false; - } + filter.Disabled = false; + isNewFilter = false; } if (isNewFilter) { filter.Context = FilterContext.Chat | FilterContext.Log; filter.Actions = FilterAction.RemoveContent | FilterAction.IssueWarning | FilterAction.SendMessage; } - - var (success, msg) = await EditFilterPropertiesAsync(ctx, db, filter).ConfigureAwait(false); - if (success) + filter.ValidatingRegex = validation; + if (context is not 0) + filter.Context = (FilterContext)context; + if (action is not 0) + filter.Actions = (FilterAction)action; + if (message is {Length: >0}) + filter.CustomMessage = message; + if (explanation is { Length: > 0 }) + filter.ExplainTerm = explanation; + if (filter.Actions.HasFlag(FilterAction.ShowExplain) + && filter.ExplainTerm is not { Length: > 0 }) { - if (isNewFilter) - await db.Piracystring.AddAsync(filter).ConfigureAwait(false); - await db.SaveChangesAsync().ConfigureAwait(false); - await msg.UpdateOrCreateMessageAsync(ctx.Channel, embed: FormatFilter(filter).WithTitle("Created a new content filter #" + filter.Id)).ConfigureAwait(false); - var member = ctx.Member ?? await ctx.Client.GetMemberAsync(ctx.User).ConfigureAwait(false); - var reportMsg = $"{member?.GetMentionWithNickname()} added a new content filter: `{filter.String.Sanitize()}`"; - if (!string.IsNullOrEmpty(filter.ValidatingRegex)) - reportMsg += $"\nValidation: `{filter.ValidatingRegex}`"; - await ctx.Client.ReportAsync("🆕 Content filter created", reportMsg, null, ReportSeverity.Low).ConfigureAwait(false); - ContentFilter.RebuildMatcher(); + await ctx.RespondAsync("❌ Explain action flag was enabled, but no valid explanation term was provided.", ephemeral: ephemeral).ConfigureAwait(false); + return; } - else - await msg.UpdateOrCreateMessageAsync(ctx.Channel, "Content filter creation aborted").ConfigureAwait(false); + + if (isNewFilter) + await db.Piracystring.AddAsync(filter).ConfigureAwait(false); + await db.SaveChangesAsync().ConfigureAwait(false); + var resultEmbed = FormatFilter(filter).WithTitle("Created a new content filter #" + filter.Id); + await ctx.RespondAsync(resultEmbed, ephemeral: ephemeral).ConfigureAwait(false); + + var member = ctx.Member ?? await ctx.Client.GetMemberAsync(ctx.User).ConfigureAwait(false); + var reportMsg = $"{member?.GetMentionWithNickname()} added a new content filter: `{filter.String.Sanitize()}`"; + if (!string.IsNullOrEmpty(filter.ValidatingRegex)) + reportMsg += $"\nValidation: `{filter.ValidatingRegex}`"; + await ctx.Client.ReportAsync("🆕 Content filter created", reportMsg, null, ReportSeverity.Low).ConfigureAwait(false); + ContentFilter.RebuildMatcher(); } + /* [Command("import"), RequiresBotSudoerRole] [Description("Import suspicious strings for a certain dump collection from attached dat file (zip is fine)")] - public async Task Import(CommandContext ctx) + public static async ValueTask Import(CommandContext ctx) { if (ctx.Message.Attachments.Count == 0) { @@ -221,80 +277,130 @@ internal sealed partial class ContentFilters ImportLock.Release(); } } + */ - [Command("edit"), TextAlias("fix", "update", "change")] + [Command("update")] [Description("Modifies the specified content filter")] - public async Task Edit(CommandContext ctx, [Description("Filter ID")] int id) + public async Task Edit(SlashCommandContext ctx, + [Description("Filter ID"), SlashAutoCompleteProvider] + int id, + [Description("A plain string to match"), MinMaxLength(minLength: 3)] + string? trigger = null, + //[Description("Context where filter is active (default is Chat and Logs)"), VariadicArgument(2, 0)]IReadOnlyList context, // todo: use this when variadic bugs are fixed + [Description("Context where filter is active (default is Chat and Logs)"), SlashChoiceProvider] + int context = 0, + //[Description("Actions performed by the filter (default is Remove, and Warn with Message)"), VariadicArgument(6, 0)]IReadOnlyList action, + [Description("Actions performed by the filter (default is Remove, and Warn with Message)"), SlashAutoCompleteProvider] + int action = 0, + [Description("Validation regex (use https://regex101.com to test)")] + string? validation = null, + [Description("Custom message to send if `M`essage action was enabled (default is the piracy warning)")] + string? message = null, + [Description("Explanation to send if `E`xplain action was enabled"), SlashAutoCompleteProvider] + string? explanation = null + ) { + var ephemeral = !ctx.Channel.IsPrivate; + if (validation is { Length: > 0 }) + { + try + { + _ = Regex.IsMatch( + "test", + validation, + RegexOptions.Multiline | RegexOptions.IgnoreCase, + TimeSpan.FromMilliseconds(100) + ); + } + catch (Exception e) + { + await ctx.RespondAsync($"❌ Invalid regex expression: {e.Message}", ephemeral: ephemeral).ConfigureAwait(false); + return; + } + } + + await using var db = new BotDb(); + explanation = explanation?.ToLowerInvariant(); + if (explanation is {Length: >0} && !await db.Explanation.AnyAsync(e => e.Keyword == explanation).ConfigureAwait(false)) + { + await ctx.RespondAsync($"❌ Unknown explanation term: {explanation}", ephemeral: ephemeral).ConfigureAwait(false); + return; + } + + var filter = await db.Piracystring.FirstOrDefaultAsync(ps => ps.Id == id).ConfigureAwait(false); + if (filter is null) + { + await ctx.RespondAsync($"❌ Unknown filter ID: {id}", ephemeral: ephemeral).ConfigureAwait(false); + return; + } + + filter.Disabled = false; + if (trigger is { Length: > 0 }) + filter.String = trigger; + if (validation is { Length: >0 }) + filter.ValidatingRegex = validation; + if (context is not 0) + filter.Context = (FilterContext)context; + if (action is not 0) + filter.Actions = (FilterAction)action; + if (message is {Length: >0}) + filter.CustomMessage = message; + if (explanation is { Length: > 0 }) + filter.ExplainTerm = explanation; + if (filter.Actions.HasFlag(FilterAction.ShowExplain) + && filter.ExplainTerm is not { Length: > 0 }) + { + await ctx.RespondAsync("❌ Explain action flag was enabled, but no valid explanation term was provided.", ephemeral: ephemeral).ConfigureAwait(false); + return; + } + + await db.SaveChangesAsync().ConfigureAwait(false); + var resultEmbed = FormatFilter(filter).WithTitle("Created a new content filter #" + filter.Id); + await ctx.RespondAsync(resultEmbed, ephemeral: ephemeral).ConfigureAwait(false); + + var member = ctx.Member ?? await ctx.Client.GetMemberAsync(ctx.User).ConfigureAwait(false); + var reportMsg = $"{member?.GetMentionWithNickname()} updated content filter #{filter.Id}: `{filter.String.Sanitize()}`"; + if (!string.IsNullOrEmpty(filter.ValidatingRegex)) + reportMsg += $"\nValidation: `{filter.ValidatingRegex}`"; + await ctx.Client.ReportAsync("🆙 Content filter created", reportMsg, null, ReportSeverity.Low).ConfigureAwait(false); + ContentFilter.RebuildMatcher(); + } + + [Command("view")] + [Description("Show the details of the specified content filter")] + public static async ValueTask ViewById( + SlashCommandContext ctx, + [Description("Filter ID"), SlashAutoCompleteProvider] int id + ) + { + var ephemeral = !ctx.Channel.IsPrivate; await using var db = new BotDb(); var filter = await db.Piracystring.FirstOrDefaultAsync(ps => ps.Id == id && !ps.Disabled).ConfigureAwait(false); if (filter is null) { - await ctx.Channel.SendMessageAsync("Specified filter does not exist").ConfigureAwait(false); + await ctx.RespondAsync("❌ Specified filter does not exist", ephemeral: ephemeral).ConfigureAwait(false); return; } - await EditFilterCmd(ctx, db, filter).ConfigureAwait(false); + var messageBuilder = new DiscordInteractionResponseBuilder().AddEmbed(FormatFilter(filter)); + if (ephemeral) + messageBuilder = messageBuilder.AsEphemeral(); + await ctx.RespondAsync(messageBuilder).ConfigureAwait(false); } - [Command("edit")] - public async Task Edit(CommandContext ctx, [Description("Trigger to edit"), RemainingText] string trigger) - { - await using var db = new BotDb(); - var filter = await db.Piracystring.FirstOrDefaultAsync(ps => ps.String == trigger && !ps.Disabled).ConfigureAwait(false); - if (filter is null) - { - await ctx.Channel.SendMessageAsync("Specified filter does not exist").ConfigureAwait(false); - return; - } - - await EditFilterCmd(ctx, db, filter).ConfigureAwait(false); - } - */ - - [Command("view"), TextAlias("show")] - [Description("Shows the details of the specified content filter")] - public sealed class View - { - [Command("id")] - public async Task ViewById(CommandContext ctx, [Description("Filter ID")] int id) - { - await using var db = new BotDb(); - var filter = await db.Piracystring.FirstOrDefaultAsync(ps => ps.Id == id && !ps.Disabled).ConfigureAwait(false); - if (filter is null) - { - await ctx.Channel.SendMessageAsync("Specified filter does not exist").ConfigureAwait(false); - return; - } - - await ctx.Channel.SendMessageAsync(new DiscordMessageBuilder().AddEmbed(FormatFilter(filter))).ConfigureAwait(false); - } - - [Command("trigger")] - public async Task ViewByTrigger(CommandContext ctx, [Description("Trigger to view"), RemainingText] string trigger) - { - await using var db = new BotDb(); - var filter = await db.Piracystring.FirstOrDefaultAsync(ps => ps.String == trigger && !ps.Disabled).ConfigureAwait(false); - if (filter is null) - { - await ctx.Channel.SendMessageAsync("Specified filter does not exist").ConfigureAwait(false); - return; - } - - await ctx.Channel.SendMessageAsync(new DiscordMessageBuilder().AddEmbed(FormatFilter(filter))).ConfigureAwait(false); - } - } - - /* - [Command("remove"), TextAlias("delete", "del")] + [Command("remove")] [Description("Removes a content filter trigger")] - public async Task Remove(CommandContext ctx, [Description("Filter IDs to remove, separated with spaces")] params int[] ids) + public static async ValueTask Remove( + SlashCommandContext ctx, + [Description("Filter to remove"), SlashAutoCompleteProvider]int id + ) { + var ephemeral = !ctx.Channel.IsPrivate; int removedFilters; var removedTriggers = new StringBuilder(); await using (var db = new BotDb()) { - foreach (var f in db.Piracystring.Where(ps => ids.Contains(ps.Id) && !ps.Disabled)) + foreach (var f in db.Piracystring.Where(ps => ps.Id == id && !ps.Disabled)) { f.Disabled = true; removedTriggers.Append($"\n`{f.String.Sanitize()}`"); @@ -302,11 +408,12 @@ internal sealed partial class ContentFilters removedFilters = await db.SaveChangesAsync(Config.Cts.Token).ConfigureAwait(false); } - if (removedFilters < ids.Length) - await ctx.Channel.SendMessageAsync("Some ids couldn't be removed.").ConfigureAwait(false); + if (removedFilters is 0) + await ctx.RespondAsync("Nothing was removed.", ephemeral: ephemeral).ConfigureAwait(false); else { - await ctx.ReactWithAsync(Config.Reactions.Success, $"Trigger{StringUtils.GetSuffix(ids.Length)} successfully removed!").ConfigureAwait(false); + await ctx.RespondAsync($"✅ Content filter was successfully removed", ephemeral: ephemeral).ConfigureAwait(false); + var member = ctx.Member ?? await ctx.Client.GetMemberAsync(ctx.User).ConfigureAwait(false); var s = removedFilters == 1 ? "" : "s"; var filterList = removedTriggers.ToString(); @@ -317,594 +424,6 @@ internal sealed partial class ContentFilters ContentFilter.RebuildMatcher(); } - [Command("remove")] - public async Task Remove(CommandContext ctx, [Description("Trigger to remove"), RemainingText] string trigger) - { - if (string.IsNullOrWhiteSpace(trigger)) - { - await ctx.ReactWithAsync(Config.Reactions.Failure, "No trigger was specified").ConfigureAwait(false); - return; - } - - await using (var db = new BotDb()) - { - var f = await db.Piracystring.FirstOrDefaultAsync(ps => ps.String.Equals(trigger, StringComparison.InvariantCultureIgnoreCase) && !ps.Disabled).ConfigureAwait(false); - if (f is null) - { - await ctx.ReactWithAsync(Config.Reactions.Failure, "Specified filter does not exist").ConfigureAwait(false); - return; - } - - f.Disabled = true; - await db.SaveChangesAsync(Config.Cts.Token).ConfigureAwait(false); - } - - await ctx.ReactWithAsync(Config.Reactions.Success, "Trigger was removed").ConfigureAwait(false); - var member = ctx.Member ?? await ctx.Client.GetMemberAsync(ctx.User).ConfigureAwait(false); - await ctx.Client.ReportAsync("📴 Content filter removed", $"{member?.GetMentionWithNickname()} removed 1 content filter: `{trigger.Sanitize()}`", null, ReportSeverity.Medium).ConfigureAwait(false); - ContentFilter.RebuildMatcher(); - } - - private static async Task EditFilterCmd(CommandContext ctx, BotDb db, Piracystring filter) - { - var (success, msg) = await EditFilterPropertiesAsync(ctx, db, filter).ConfigureAwait(false); - if (success) - { - await db.SaveChangesAsync().ConfigureAwait(false); - await msg.UpdateOrCreateMessageAsync(ctx.Channel, embed: FormatFilter(filter).WithTitle("Updated content filter")).ConfigureAwait(false); - var member = ctx.Member ?? await ctx.Client.GetMemberAsync(ctx.User).ConfigureAwait(false); - var reportMsg = $"{member?.GetMentionWithNickname()} changed content filter #{filter.Id} (`{filter.Actions.ToFlagsString()}`): `{filter.String.Sanitize()}`"; - if (!string.IsNullOrEmpty(filter.ValidatingRegex)) - reportMsg += $"\nValidation: `{filter.ValidatingRegex}`"; - await ctx.Client.ReportAsync("🆙 Content filter updated", reportMsg, null, ReportSeverity.Low).ConfigureAwait(false); - ContentFilter.RebuildMatcher(); - } - else - await msg.UpdateOrCreateMessageAsync(ctx.Channel, "Content filter update aborted").ConfigureAwait(false); - } - - private static async Task<(bool success, DiscordMessage? message)> EditFilterPropertiesAsync(CommandContext ctx, BotDb db, Piracystring filter) - { - try - { - return await EditFilterPropertiesInternalAsync(ctx, db, filter).ConfigureAwait(false); - } - catch (Exception e) - { - Config.Log.Error(e, "Failed to edit content filter"); - return (false, null); - } - } - private static async Task<(bool success, DiscordMessage? message)> EditFilterPropertiesInternalAsync(CommandContext ctx, BotDb db, Piracystring filter) - { - var interact = ctx.Client.GetInteractivity(); - var abort = new DiscordButtonComponent(ButtonStyle.Danger, "filter:edit:abort", "Cancel", emoji: new(DiscordEmoji.FromUnicode("✖"))); - var lastPage = new DiscordButtonComponent(ButtonStyle.Secondary, "filter:edit:last", "To Last Field", emoji: new(DiscordEmoji.FromUnicode("⏭"))); - var firstPage = new DiscordButtonComponent(ButtonStyle.Secondary, "filter:edit:first", "To First Field", emoji: new(DiscordEmoji.FromUnicode("⏮"))); - var previousPage = new DiscordButtonComponent(ButtonStyle.Secondary, "filter:edit:previous", "Previous", emoji: new(DiscordEmoji.FromUnicode("◀"))); - var nextPage = new DiscordButtonComponent(ButtonStyle.Primary, "filter:edit:next", "Next", emoji: new(DiscordEmoji.FromUnicode("▶"))); - var trash = new DiscordButtonComponent(ButtonStyle.Secondary, "filter:edit:trash", "Clear", emoji: new(DiscordEmoji.FromUnicode("🗑"))); - var saveEdit = new DiscordButtonComponent(ButtonStyle.Success, "filter:edit:save", "Save", emoji: new(DiscordEmoji.FromUnicode("💾"))); - - var contextChat = new DiscordButtonComponent(ButtonStyle.Secondary, "filter:edit:context:chat", "Chat"); - var contextLog = new DiscordButtonComponent(ButtonStyle.Secondary, "filter:edit:context:log", "Log"); - var actionR = new DiscordButtonComponent(ButtonStyle.Secondary, "filter:edit:action:r", "R"); - var actionW = new DiscordButtonComponent(ButtonStyle.Secondary, "filter:edit:action:w", "W"); - var actionM = new DiscordButtonComponent(ButtonStyle.Secondary, "filter:edit:action:m", "M"); - var actionE = new DiscordButtonComponent(ButtonStyle.Secondary, "filter:edit:action:e", "E"); - var actionU = new DiscordButtonComponent(ButtonStyle.Secondary, "filter:edit:action:u", "U"); - var actionK = new DiscordButtonComponent(ButtonStyle.Secondary, "filter:edit:action:k", "K"); - - var minus = new DiscordComponentEmoji(DiscordEmoji.FromUnicode("➖")); - var plus = new DiscordComponentEmoji(DiscordEmoji.FromUnicode("➕")); - - DiscordMessage? msg = null; - string? errorMsg = null; - DiscordMessage? txt; - ComponentInteractionCreateEventArgs? btn; - - step1: - // step 1: define trigger string - var embed = FormatFilter(filter, errorMsg, 1) - .WithDescription(""" - Any simple string that is used to flag potential content for a check using Validation regex. - **Must** be sufficiently long to reduce the number of checks. - """); - saveEdit.SetEnabled(filter.IsComplete()); - var messageBuilder = new DiscordMessageBuilder() - .WithContent("Please specify a new **trigger**") - .AddEmbed(embed) - .AddComponents(lastPage, nextPage, saveEdit, abort); - errorMsg = null; - msg = await msg.UpdateOrCreateMessageAsync(ctx.Channel, messageBuilder).ConfigureAwait(false); - (txt, btn) = await interact.WaitForMessageOrButtonAsync(msg, ctx.User, InteractTimeout).ConfigureAwait(false); - if (btn != null) - { - if (btn.Id == abort.CustomId) - return (false, msg); - - if (btn.Id == saveEdit.CustomId) - return (true, msg); - - if (btn.Id == lastPage.CustomId) - { - if (filter.Actions.HasFlag(FilterAction.ShowExplain)) - goto step6; - - if (filter.Actions.HasFlag(FilterAction.SendMessage)) - goto step5; - - goto step4; - } - } - else if (txt?.Content != null) - { - if (txt.Content.Length < Config.MinimumPiracyTriggerLength) - { - errorMsg = "Trigger is too short"; - goto step1; - } - - filter.String = txt.Content; - } - else - return (false, msg); - - step2: - // step 2: context of the filter where it is applicable - embed = FormatFilter(filter, errorMsg, 2) - .WithDescription($""" - Context of the filter indicates where it is applicable. - **`C`** = **`{FilterContext.Chat}`** will apply it in filtering discord messages. - **`L`** = **`{FilterContext.Log}`** will apply it during log parsing. - Reactions will toggle the context, text message will set the specified flags. - """); - saveEdit.SetEnabled(filter.IsComplete()); - contextChat.SetEmoji(filter.Context.HasFlag(FilterContext.Chat) ? minus : plus); - contextLog.SetEmoji(filter.Context.HasFlag(FilterContext.Log) ? minus : plus); - messageBuilder = new DiscordMessageBuilder() - .WithContent("Please specify filter **context(s)**") - .AddEmbed(embed) - .AddComponents(previousPage, nextPage, saveEdit, abort) - .AddComponents(contextChat, contextLog); - errorMsg = null; - msg = await msg.UpdateOrCreateMessageAsync(ctx.Channel, messageBuilder).ConfigureAwait(false); - (txt, btn) = await interact.WaitForMessageOrButtonAsync(msg, ctx.User, InteractTimeout).ConfigureAwait(false); - if (btn != null) - { - if (btn.Id == abort.CustomId) - return (false, msg); - - if (btn.Id == saveEdit.CustomId) - return (true, msg); - - if (btn.Id == previousPage.CustomId) - goto step1; - - if (btn.Id == contextChat.CustomId) - { - filter.Context ^= FilterContext.Chat; - goto step2; - } - - if (btn.Id == contextLog.CustomId) - { - filter.Context ^= FilterContext.Log; - goto step2; - } - } - else if (txt != null) - { - var flagsTxt = txt.Content.Split(Separators, StringSplitOptions.RemoveEmptyEntries); - FilterContext newCtx = 0; - foreach (var f in flagsTxt) - { - switch (f.ToUpperInvariant()) - { - case "C": - case "CHAT": - newCtx |= FilterContext.Chat; - break; - case "L": - case "LOG": - case "LOGS": - newCtx |= FilterContext.Log; - break; - case "ABORT": - return (false, msg); - case "-": - case "SKIP": - case "NEXT": - break; - default: - errorMsg = $"Unknown context `{f}`."; - goto step2; - } - } - filter.Context = newCtx; - } - else - return (false, msg); - - step3: - // step 3: actions that should be performed on match - embed = FormatFilter(filter, errorMsg, 3) - .WithDescription($""" - Actions that will be executed on positive match. - **`R`** = **`{FilterAction.RemoveContent}`** will remove the message / log. - **`W`** = **`{FilterAction.IssueWarning}`** will issue a warning to the user. - **`M`** = **`{FilterAction.SendMessage}`** send _a_ message with an explanation of why it was removed. - **`E`** = **`{FilterAction.ShowExplain}`** show `explain` for the specified term (**not implemented**). - **`U`** = **`{FilterAction.MuteModQueue}`** mute mod queue reporting for this action. - **`K`** = **`{FilterAction.Kick}`** kick user from server. - Buttons will toggle the action, text message will set the specified flags. - """); - actionR.SetEmoji(filter.Actions.HasFlag(FilterAction.RemoveContent) ? minus : plus); - actionW.SetEmoji(filter.Actions.HasFlag(FilterAction.IssueWarning) ? minus : plus); - actionM.SetEmoji(filter.Actions.HasFlag(FilterAction.SendMessage) ? minus : plus); - actionE.SetEmoji(filter.Actions.HasFlag(FilterAction.ShowExplain) ? minus : plus); - actionU.SetEmoji(filter.Actions.HasFlag(FilterAction.MuteModQueue) ? minus : plus); - actionK.SetEmoji(filter.Actions.HasFlag(FilterAction.Kick) ? minus : plus); - saveEdit.SetEnabled(filter.IsComplete()); - messageBuilder = new DiscordMessageBuilder() - .WithContent("Please specify filter **action(s)**") - .AddEmbed(embed) - .AddComponents(previousPage, nextPage, saveEdit, abort) - .AddComponents(actionR, actionW, actionM, actionE, actionU) - .AddComponents(actionK); - errorMsg = null; - msg = await msg.UpdateOrCreateMessageAsync(ctx.Channel, messageBuilder).ConfigureAwait(false); - (txt, btn) = await interact.WaitForMessageOrButtonAsync(msg, ctx.User, InteractTimeout).ConfigureAwait(false); - if (btn != null) - { - if (btn.Id == abort.CustomId) - return (false, msg); - - if (btn.Id == saveEdit.CustomId) - return (true, msg); - - if (btn.Id == previousPage.CustomId) - goto step2; - - if (btn.Id == actionR.CustomId) - { - filter.Actions ^= FilterAction.RemoveContent; - goto step3; - } - - if (btn.Id == actionW.CustomId) - { - filter.Actions ^= FilterAction.IssueWarning; - goto step3; - } - - if (btn.Id == actionM.CustomId) - { - filter.Actions ^= FilterAction.SendMessage; - goto step3; - } - - if (btn.Id == actionE.CustomId) - { - filter.Actions ^= FilterAction.ShowExplain; - goto step3; - } - - if (btn.Id == actionU.CustomId) - { - filter.Actions ^= FilterAction.MuteModQueue; - goto step3; - } - - if (btn.Id == actionK.CustomId) - { - filter.Actions ^= FilterAction.Kick; - goto step3; - } - } - else if (txt != null) - { - var flagsTxt = txt.Content.ToUpperInvariant().Split(Separators, StringSplitOptions.RemoveEmptyEntries); - if (flagsTxt.Length == 1 - && flagsTxt[0].Length <= Enum.GetValues(typeof(FilterAction)).Length) - flagsTxt = flagsTxt[0].Select(c => c.ToString()).ToArray(); - FilterAction newActions = 0; - foreach (var f in flagsTxt) - { - switch (f) - { - case "R": - case "REMOVE": - case "REMOVEMESSAGE": - newActions |= FilterAction.RemoveContent; - break; - case "W": - case "WARN": - case "WARNING": - case "ISSUEWARNING": - newActions |= FilterAction.IssueWarning; - break; - case "M": - case "MSG": - case "MESSAGE": - case "SENDMESSAGE": - newActions |= FilterAction.SendMessage; - break; - case "E": - case "X": - case "EXPLAIN": - case "SHOWEXPLAIN": - case "SENDEXPLAIN": - newActions |= FilterAction.ShowExplain; - break; - case "U": - case "MMQ": - case "MUTE": - case "MUTEMODQUEUE": - newActions |= FilterAction.MuteModQueue; - break; - case "K": - case "KICK": - newActions |= FilterAction.Kick; - break; - case "ABORT": - return (false, msg); - case "-": - case "SKIP": - case "NEXT": - break; - default: - errorMsg = $"Unknown action `{f.ToLowerInvariant()}`."; - goto step2; - } - } - filter.Actions = newActions; - } - else - return (false, msg); - - step4: - // step 4: validation regex to filter out false positives of the plaintext triggers - embed = FormatFilter(filter, errorMsg, 4) - .WithDescription(""" - Validation [regex](https://docs.microsoft.com/en-us/dotnet/standard/base-types/regular-expression-language-quick-reference) to optionally perform more strict trigger check. - **Please [test](https://regex101.com/) your regex**. Following flags are enabled: Multiline, IgnoreCase. - Additional validation can help reduce false positives of a plaintext trigger match. - """); - var next = (filter.Actions & (FilterAction.SendMessage | FilterAction.ShowExplain)) == 0 ? firstPage : nextPage; - trash.SetDisabled(string.IsNullOrEmpty(filter.ValidatingRegex)); - saveEdit.SetEnabled(filter.IsComplete()); - messageBuilder = new DiscordMessageBuilder() - .WithContent("Please specify filter **validation regex**") - .AddEmbed(embed) - .AddComponents(previousPage, next, trash, saveEdit, abort); - errorMsg = null; - msg = await msg.UpdateOrCreateMessageAsync(ctx.Channel, messageBuilder).ConfigureAwait(false); - (txt, btn) = await interact.WaitForMessageOrButtonAsync(msg, ctx.User, InteractTimeout).ConfigureAwait(false); - if (btn != null) - { - if (btn.Id == abort.CustomId) - return (false, msg); - - if (btn.Id == saveEdit.CustomId) - return (true, msg); - - if (btn.Id == previousPage.CustomId) - goto step3; - - if (btn.Id == firstPage.CustomId) - goto step1; - - if (btn.Id == trash.CustomId) - filter.ValidatingRegex = null; - } - else if (txt != null) - { - if (string.IsNullOrWhiteSpace(txt.Content) || txt.Content == "-" || txt.Content == ".*") - filter.ValidatingRegex = null; - else - { - try - { - _ = Regex.IsMatch( - filter.String ?? "test", - txt.Content, - RegexOptions.Multiline | RegexOptions.IgnoreCase, - TimeSpan.FromMilliseconds(100) - ); - } - catch (Exception e) - { - errorMsg = "Invalid regex expression: " + e.Message; - goto step4; - } - - filter.ValidatingRegex = txt.Content; - } - } - else - return (false, msg); - - if (filter.Actions.HasFlag(FilterAction.SendMessage)) - goto step5; - else if (filter.Actions.HasFlag(FilterAction.ShowExplain)) - goto step6; - else - goto stepConfirm; - - step5: - // step 5: optional custom message for the user - embed = FormatFilter(filter, errorMsg, 5) - .WithDescription( - "Optional custom message sent to the user.\n" + - "If left empty, default piracy warning message will be used." - ); - next = (filter.Actions.HasFlag(FilterAction.ShowExplain) ? nextPage : firstPage); - saveEdit.SetEnabled(filter.IsComplete()); - messageBuilder = new DiscordMessageBuilder() - .WithContent("Please specify filter **validation regex**") - .AddEmbed(embed) - .AddComponents(previousPage, next, saveEdit, abort); - errorMsg = null; - msg = await msg.UpdateOrCreateMessageAsync(ctx.Channel, messageBuilder).ConfigureAwait(false); - (txt, btn) = await interact.WaitForMessageOrButtonAsync(msg, ctx.User, InteractTimeout).ConfigureAwait(false); - if (btn != null) - { - if (btn.Id == abort.CustomId) - return (false, msg); - - if (btn.Id == saveEdit.CustomId) - return (true, msg); - - if (btn.Id == previousPage.CustomId) - goto step4; - - if (btn.Id == firstPage.CustomId) - goto step1; - } - else if (txt != null) - { - if (string.IsNullOrWhiteSpace(txt.Content) || txt.Content == "-") - filter.CustomMessage = null; - else - filter.CustomMessage = txt.Content; - } - else - return (false, msg); - - if (filter.Actions.HasFlag(FilterAction.ShowExplain)) - goto step6; - else - goto stepConfirm; - - step6: - // step 6: show explanation for the term - embed = FormatFilter(filter, errorMsg, 6) - .WithDescription(""" - Explanation term that is used to show an explanation. - **__Currently not implemented__**. - """); - saveEdit.SetEnabled(filter.IsComplete()); - messageBuilder = new DiscordMessageBuilder() - .WithContent("Please specify filter **explanation term**") - .AddEmbed(embed) - .AddComponents(previousPage, firstPage, saveEdit, abort); - errorMsg = null; - msg = await msg.UpdateOrCreateMessageAsync(ctx.Channel, messageBuilder).ConfigureAwait(false); - (txt, btn) = await interact.WaitForMessageOrButtonAsync(msg, ctx.User, InteractTimeout).ConfigureAwait(false); - if (btn != null) - { - if (btn.Id == abort.CustomId) - return (false, msg); - - if (btn.Id == saveEdit.CustomId) - return (true, msg); - - if (btn.Id == previousPage.CustomId) - { - if (filter.Actions.HasFlag(FilterAction.SendMessage)) - goto step5; - else - goto step4; - } - - if (btn.Id == firstPage.CustomId) - goto step1; - } - else if (txt != null) - { - if (string.IsNullOrWhiteSpace(txt.Content) || txt.Content == "-") - filter.ExplainTerm = null; - else - { - var term = txt.Content.ToLowerInvariant(); - var existingTerm = await db.Explanation.FirstOrDefaultAsync(exp => exp.Keyword == term).ConfigureAwait(false); - if (existingTerm == null) - { - errorMsg = $"Term `{txt.Content.ToLowerInvariant().Sanitize()}` is not defined."; - goto step6; - } - - filter.ExplainTerm = txt.Content; - } - } - else - return (false, msg); - - stepConfirm: - // last step: confirm - if (errorMsg == null && !filter.IsComplete()) - errorMsg = "Some required properties are not defined"; - saveEdit.SetEnabled(filter.IsComplete()); - messageBuilder = new DiscordMessageBuilder() - .WithContent("Does this look good? (y/n)") - .AddEmbed(FormatFilter(filter, errorMsg)) - .AddComponents(previousPage, firstPage, saveEdit, abort); - errorMsg = null; - msg = await msg.UpdateOrCreateMessageAsync(ctx.Channel, messageBuilder).ConfigureAwait(false); - (txt, btn) = await interact.WaitForMessageOrButtonAsync(msg, ctx.User, InteractTimeout).ConfigureAwait(false); - if (btn != null) - { - if (btn.Id == abort.CustomId) - return (false, msg); - - if (btn.Id == saveEdit.CustomId) - return (true, msg); - - if (btn.Id == previousPage.CustomId) - { - if (filter.Actions.HasFlag(FilterAction.ShowExplain)) - goto step6; - - if (filter.Actions.HasFlag(FilterAction.SendMessage)) - goto step5; - - goto step4; - } - - if (btn.Id == firstPage.CustomId) - goto step1; - } - else if (!string.IsNullOrEmpty(txt?.Content)) - { - if (!filter.IsComplete()) - goto step5; - - switch (txt.Content.ToLowerInvariant()) - { - case "yes": - case "y": - case "✅": - case "☑": - case "✔": - case "👌": - case "👍": - return (true, msg); - case "no": - case "n": - case "❎": - case "❌": - case "👎": - return (false, msg); - default: - errorMsg = "I don't know what you mean, so I'll just abort"; - if (filter.Actions.HasFlag(FilterAction.ShowExplain)) - goto step6; - - if (filter.Actions.HasFlag(FilterAction.SendMessage)) - goto step5; - - goto step4; - } - } - else - { - return (false, msg); - } - return (false, msg); - } - */ - private static DiscordEmbedBuilder FormatFilter(Piracystring filter, string? error = null, int highlight = -1) { var field = 1; diff --git a/CompatBot/Commands/Hardware.cs b/CompatBot/Commands/Hardware.cs index 7897cbb3..0cd6405f 100644 --- a/CompatBot/Commands/Hardware.cs +++ b/CompatBot/Commands/Hardware.cs @@ -85,7 +85,7 @@ internal sealed class Hardware .ToListAsync() .ConfigureAwait(false); var featureStats = new Dictionary(); - foreach (CpuFeatures feature in Enum.GetValues(typeof(CpuFeatures))) + foreach (CpuFeatures feature in Enum.GetValues()) { if (feature == CpuFeatures.None) continue; diff --git a/CompatBot/Database/BotDb.cs b/CompatBot/Database/BotDb.cs index f744f876..03a68164 100644 --- a/CompatBot/Database/BotDb.cs +++ b/CompatBot/Database/BotDb.cs @@ -93,6 +93,7 @@ public class SuspiciousString [Flags] public enum FilterContext: byte { + //None = 0b_0000_0000, do NOT add this Chat = 0b_0000_0001, Log = 0b_0000_0010, } diff --git a/CompatBot/Database/Providers/ContentFilter.cs b/CompatBot/Database/Providers/ContentFilter.cs index 84ca7d11..14e90ab8 100644 --- a/CompatBot/Database/Providers/ContentFilter.cs +++ b/CompatBot/Database/Providers/ContentFilter.cs @@ -2,6 +2,7 @@ using CompatApiClient.Utils; using CompatBot.Commands; using CompatBot.EventHandlers; +using CompatBot.Utils.Extensions; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Memory; using NReco.Text; @@ -59,7 +60,7 @@ internal static class ContentFilter { var newFilters = new Dictionary?>(); using var db = new BotDb(); - foreach (FilterContext ctx in Enum.GetValues(typeof(FilterContext))) + foreach (FilterContext ctx in Enum.GetValues()) { var triggerList = db.Piracystring.Where(ps => ps.Disabled == false && ps.Context.HasFlag(ctx)).AsNoTracking() .AsEnumerable() @@ -259,7 +260,7 @@ internal static class ContentFilter } var actionList = ""; - foreach (FilterAction fa in Enum.GetValues(typeof(FilterAction))) + foreach (FilterAction fa in FilterActionExtensions.ActionFlagValues) { if (trigger.Actions.HasFlag(fa) && !ignoreFlags.HasFlag(fa)) actionList += (completedActions.Contains(fa) ? "✅" : "❌") + " " + fa + ' '; diff --git a/CompatBot/Program.cs b/CompatBot/Program.cs index c244b476..6eb16c26 100644 --- a/CompatBot/Program.cs +++ b/CompatBot/Program.cs @@ -10,6 +10,7 @@ using CompatBot.Database.Providers; using CompatBot.EventHandlers; using CompatBot.Utils.Extensions; using DSharpPlus.Commands.Processors.SlashCommands; +using DSharpPlus.Commands.Processors.SlashCommands.RemoteRecordRetentionPolicies; using DSharpPlus.Commands.Processors.TextCommands; using DSharpPlus.Commands.Processors.TextCommands.Parsing; using Microsoft.EntityFrameworkCore; @@ -149,6 +150,9 @@ internal static class Program { //NamingPolicy = new CamelCaseNamingPolicy(), RegisterCommands = true, +#if DEBUG + //UnconditionallyOverwriteCommands = true, +#endif }); textCommandProcessor.AddConverter(); extension.AddProcessor(textCommandProcessor); @@ -168,7 +172,7 @@ internal static class Program RegisterDefaultCommandProcessors = false, //UseDefaultCommandErrorHandler = false, #if DEBUG - DebugGuildId = Config.BotGuildId, + //DebugGuildId = Config.BotGuildId, // this forces app commands to be guild-limited, which doesn't work well #endif }) .UseInteractivity() diff --git a/CompatBot/Properties/GlobalUsings.cs b/CompatBot/Properties/GlobalUsings.cs index a180331e..d4b597a0 100644 --- a/CompatBot/Properties/GlobalUsings.cs +++ b/CompatBot/Properties/GlobalUsings.cs @@ -18,3 +18,4 @@ global using DSharpPlus.Commands.Trees.Metadata; global using DSharpPlus.Entities; global using DSharpPlus.EventArgs; global using DSharpPlus.Interactivity.Extensions; +global using DSharpPlus.Commands.Processors.SlashCommands.ArgumentModifiers; diff --git a/CompatBot/Utils/DiscordChannelExtensions.cs b/CompatBot/Utils/Extensions/DiscordChannelExtensions.cs similarity index 100% rename from CompatBot/Utils/DiscordChannelExtensions.cs rename to CompatBot/Utils/Extensions/DiscordChannelExtensions.cs diff --git a/CompatBot/Utils/Extensions/FilterActionExtensions.cs b/CompatBot/Utils/Extensions/FilterActionExtensions.cs index 0fd85ffe..e3f46fa0 100644 --- a/CompatBot/Utils/Extensions/FilterActionExtensions.cs +++ b/CompatBot/Utils/Extensions/FilterActionExtensions.cs @@ -4,7 +4,8 @@ namespace CompatBot.Utils.Extensions; internal static class FilterActionExtensions { - private static readonly Dictionary ActionFlags = new() + internal static readonly FilterAction[] ActionFlagValues = Enum.GetValues(); + private static readonly Dictionary ActionFlagToChar = new() { [FilterAction.RemoveContent] = 'r', [FilterAction.IssueWarning] = 'w', @@ -14,20 +15,33 @@ internal static class FilterActionExtensions [FilterAction.Kick] = 'k', }; - public static string ToFlagsString(this FilterAction flags) + private static readonly Dictionary CharToActionFlag = new() { - var result = Enum.GetValues(typeof(FilterAction)) - .Cast() - .Select(fa => flags.HasFlag(fa) ? ActionFlags[fa] : '-') - .ToArray(); - return new(result); - } + ['r'] = FilterAction.RemoveContent, + ['w'] = FilterAction.IssueWarning, + ['m'] = FilterAction.SendMessage, + ['e'] = FilterAction.ShowExplain, + ['u'] = FilterAction.MuteModQueue, + ['k'] = FilterAction.Kick, + }; + + public static string ToFlagsString(this FilterAction flags) + => new( + ActionFlagValues + .Select(fa => flags.HasFlag(fa) ? ActionFlagToChar[fa] : '-') + .ToArray() + ); + + public static FilterAction ToFilterAction(this string flags) + => flags.ToCharArray() + .Select(c => CharToActionFlag.TryGetValue(c, out var f)? f: 0) + .Aggregate((a, b) => a | b); public static string GetLegend(string wrapChar = "`") { var result = new StringBuilder("Actions flag legend:"); - foreach (FilterAction fa in Enum.GetValues(typeof(FilterAction))) - result.Append($"\n{wrapChar}{ActionFlags[fa]}{wrapChar} = {fa}"); + foreach (FilterAction fa in ActionFlagValues) + result.Append($"\n{wrapChar}{ActionFlagToChar[fa]}{wrapChar} = {fa}"); return result.ToString(); } } \ No newline at end of file diff --git a/CompatBot/Utils/Extensions/ServiceProviderExteinsions.cs b/CompatBot/Utils/Extensions/ServiceProviderExteinsions.cs new file mode 100644 index 00000000..d665da85 --- /dev/null +++ b/CompatBot/Utils/Extensions/ServiceProviderExteinsions.cs @@ -0,0 +1,7 @@ +namespace CompatBot.Utils; + +public static class ServiceProviderExteinsions +{ + public static T? GetService(this IServiceProvider provider) + => (T?)provider.GetService(typeof(T)); +} \ No newline at end of file