diff --git a/Clients/CompatApiClient/CompatApiClient.csproj b/Clients/CompatApiClient/CompatApiClient.csproj
index a5405ea7..594e085a 100644
--- a/Clients/CompatApiClient/CompatApiClient.csproj
+++ b/Clients/CompatApiClient/CompatApiClient.csproj
@@ -15,7 +15,7 @@
-
+
diff --git a/CompatBot/Commands/Antipiracy.cs b/CompatBot/Commands/Antipiracy.cs
index 9d6f0885..1292a09b 100644
--- a/CompatBot/Commands/Antipiracy.cs
+++ b/CompatBot/Commands/Antipiracy.cs
@@ -1,76 +1,684 @@
-using System.Collections.Generic;
+using System;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
using System.Threading.Tasks;
using CompatApiClient.Utils;
using CompatBot.Commands.Attributes;
using CompatBot.Database;
-using CompatBot.Database.Providers;
using CompatBot.Utils;
+using CompatBot.Utils.Extensions;
+using DiscUtils.Streams;
using DSharpPlus.CommandsNext;
using DSharpPlus.CommandsNext.Attributes;
+using DSharpPlus.Entities;
+using DSharpPlus.EventArgs;
+using DSharpPlus.Interactivity;
using Microsoft.EntityFrameworkCore;
+using Exception = System.Exception;
namespace CompatBot.Commands
{
- [Group("piracy"), RequiresBotSudoerRole, RequiresDm]
- [Description("Used to manage piracy filters **in DM**")]
+ [Group("filters"), Aliases("piracy", "filter"), RequiresBotSudoerRole, RequiresDm]
+ [Description("Used to manage content filters. **Works only in DM**")]
internal sealed class Antipiracy: BaseCommandModuleCustom
{
+ private static readonly TimeSpan InteractTimeout = TimeSpan.FromMinutes(5);
+ private static readonly char[] Separators = {' ', ',', ';', '|'};
+
[Command("list"), Aliases("show")]
[Description("Lists all filters")]
public async Task List(CommandContext ctx)
{
var table = new AsciiTable(
new AsciiColumn("ID", alignToRight: true),
- new AsciiColumn("Trigger")
+ new AsciiColumn("Trigger"),
+ new AsciiColumn("Validation"),
+ new AsciiColumn("Context"),
+ new AsciiColumn("Actions"),
+ new AsciiColumn("Custom message")
);
using (var db = new BotDb())
- foreach (var item in await db.Piracystring.ToListAsync().ConfigureAwait(false))
- table.Add(item.Id.ToString(), item.String);
+ foreach (var item in await db.Piracystring.Where(ps => !ps.Disabled).OrderBy(ps => ps.String).ToListAsync().ConfigureAwait(false))
+ {
+ table.Add(
+ item.Id.ToString(),
+ item.String.Sanitize(),
+ item.ValidatingRegex,
+ item.Context.ToString(),
+ item.Actions.ToFlagsString(),
+ string.IsNullOrEmpty(item.CustomMessage) ? "" : "✅"
+ );
+ }
await ctx.SendAutosplitMessageAsync(table.ToString()).ConfigureAwait(false);
+ await ctx.RespondAsync(FilterActionExtensions.GetLegend()).ConfigureAwait(false);
}
- [Command("add")]
- [Description("Adds a new piracy filter trigger")]
+ [Command("add"), Aliases("create")]
+ [Description("Adds a new content filter")]
public async Task Add(CommandContext ctx, [RemainingText, Description("A plain string to match")] string trigger)
{
- var wasSuccessful = await PiracyStringProvider.AddAsync(trigger).ConfigureAwait(false);
- if (wasSuccessful)
+ using (var db = new BotDb())
{
- await ctx.ReactWithAsync(Config.Reactions.Success, "New trigger successfully saved!").ConfigureAwait(false);
- var member = ctx.Member ?? ctx.Client.GetMember(ctx.User);
- await ctx.Client.ReportAsync("🤬 Piracy filter added", $"{member.GetMentionWithNickname()} added a new piracy filter:\n```{trigger.Sanitize()}```", null, ReportSeverity.Low).ConfigureAwait(false);
+ Piracystring filter;
+ if (string.IsNullOrEmpty(trigger))
+ filter = new Piracystring();
+ else
+ {
+ filter = await db.Piracystring.FirstOrDefaultAsync(ps => ps.String == trigger).ConfigureAwait(false);
+ if (filter == null)
+ filter = new Piracystring();
+ else
+ filter.Disabled = false;
+ }
+ var isNewFilter = filter.Id == default;
+ if (isNewFilter)
+ {
+ filter.Context = FilterContext.Chat | FilterContext.Log;
+ filter.Actions = FilterAction.RemoveMessage | FilterAction.IssueWarning | FilterAction.SendMessage;
+ }
+
+ var (success, msg) = await EditFilterPropertiesAsync(ctx, db, filter).ConfigureAwait(false);
+ if (success)
+ {
+ 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")).ConfigureAwait(false);
+ var member = ctx.Member ?? ctx.Client.GetMember(ctx.User);
+ await ctx.Client.ReportAsync("🆕 Content filter created", $"{member.GetMentionWithNickname()} added a new content filter: `{filter.String.Sanitize()}`", null, ReportSeverity.Low).ConfigureAwait(false);
+ }
+ else
+ await msg.UpdateOrCreateMessageAsync(ctx.Channel, "Content filter creation aborted").ConfigureAwait(false);
+ }
+ }
+
+ [Command("edit"), Aliases("fix", "update", "change")]
+ [Description("Modifies the specified content filter")]
+ public async Task Edit(CommandContext ctx, [Description("Filter ID")] int id)
+ {
+ using (var db = new BotDb())
+ {
+ var filter = await db.Piracystring.FirstOrDefaultAsync(ps => ps.Id == id && !ps.Disabled).ConfigureAwait(false);
+ if (filter == null)
+ {
+ await ctx.RespondAsync("Specified filter does not exist").ConfigureAwait(false);
+ return;
+ }
+
+ await EditFilterCmd(ctx, db, filter).ConfigureAwait(false);
+ }
+ }
+
+ [Command("edit")]
+ public async Task Edit(CommandContext ctx, [Description("Trigger to edit"), RemainingText] string trigger)
+ {
+ using (var db = new BotDb())
+ {
+ var filter = await db.Piracystring.FirstOrDefaultAsync(ps => ps.String.Equals(trigger, StringComparison.InvariantCultureIgnoreCase) && !ps.Disabled).ConfigureAwait(false);
+ if (filter == null)
+ {
+ await ctx.RespondAsync("Specified filter does not exist").ConfigureAwait(false);
+ return;
+ }
+
+ await EditFilterCmd(ctx, db, filter).ConfigureAwait(false);
}
- else
- await ctx.ReactWithAsync(Config.Reactions.Failure, "Trigger already defined.").ConfigureAwait(false);
- if (wasSuccessful)
- await List(ctx).ConfigureAwait(false);
}
[Command("remove"), Aliases("delete", "del")]
[Description("Removes a piracy filter trigger")]
public async Task Remove(CommandContext ctx, [Description("Filter IDs to remove, separated with spaces")] params int[] ids)
{
- var failedIds = new List();
- var removedFilters = new List();
- foreach (var id in ids)
+ int removedFilters;
+ var removedTriggers = new StringBuilder();
+ using (var db = new BotDb())
{
- var trigger = await PiracyStringProvider.GetTriggerAsync(id).ConfigureAwait(false);
- if (await PiracyStringProvider.RemoveAsync(id).ConfigureAwait(false))
- removedFilters.Add(trigger.Sanitize());
- else
- failedIds.Add(id);
+ foreach (var f in db.Piracystring.Where(ps => ids.Contains(ps.Id)))
+ {
+ f.Disabled = true;
+ removedTriggers.Append($"\n`{f.String.Sanitize()}`");
+ }
+ removedFilters = await db.SaveChangesAsync(Config.Cts.Token).ConfigureAwait(false);
}
- if (failedIds.Count > 0)
- await ctx.RespondAsync("Some ids couldn't be removed: " + string.Join(", ", failedIds)).ConfigureAwait(false);
+ if (removedFilters < ids.Length)
+ await ctx.RespondAsync("Some ids couldn't be removed.").ConfigureAwait(false);
else
{
await ctx.ReactWithAsync(Config.Reactions.Success, $"Trigger{StringUtils.GetSuffix(ids.Length)} successfully removed!").ConfigureAwait(false);
var member = ctx.Member ?? ctx.Client.GetMember(ctx.User);
- var s = removedFilters.Count == 1 ? "" : "s";
- await ctx.Client.ReportAsync($"🤬 Piracy filter{s} removed", $"{member.GetMentionWithNickname()} removed {removedFilters.Count} piracy filter{s}:\n```\n{string.Join('\n', removedFilters)}\n```", null, ReportSeverity.Medium).ConfigureAwait(false);
+ var s = removedFilters == 1 ? "" : "s";
+ var filterList = removedTriggers.ToString();
+ if (removedFilters == 1)
+ filterList = filterList.TrimStart();
+ await ctx.Client.ReportAsync($"📴 Piracy filter{s} removed", $"{member.GetMentionWithNickname()} removed {removedFilters} piracy filter{s}: {filterList}".Trim(EmbedPager.MaxDescriptionLength), null, ReportSeverity.Medium).ConfigureAwait(false);
}
- await List(ctx).ConfigureAwait(false);
+ }
+
+ [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;
+ }
+
+ using (var db = new BotDb())
+ {
+ var f = await db.Piracystring.FirstOrDefaultAsync(ps => ps.String.Equals(trigger, StringComparison.InvariantCultureIgnoreCase) && !ps.Disabled).ConfigureAwait(false);
+ if (f == 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 ?? ctx.Client.GetMember(ctx.User);
+ await ctx.Client.ReportAsync("📴 Piracy filter removed", $"{member.GetMentionWithNickname()} removed 1 piracy filter: `{trigger.Sanitize()}`", null, ReportSeverity.Medium).ConfigureAwait(false);
+ }
+
+ private 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 ?? ctx.Client.GetMember(ctx.User);
+ await ctx.Client.ReportAsync("🆙 Content filter updated", $"{member.GetMentionWithNickname()} changed content filter: `{filter.String.Sanitize()}`", null, ReportSeverity.Low)
+ .ConfigureAwait(false);
+ }
+ else
+ await msg.UpdateOrCreateMessageAsync(ctx.Channel, "Content filter update aborted").ConfigureAwait(false);
+ }
+
+ private async Task<(bool success, DiscordMessage message)> EditFilterPropertiesAsync(CommandContext ctx, BotDb db, Piracystring filter)
+ {
+ var interact = ctx.Client.GetInteractivity();
+ var abort = DiscordEmoji.FromUnicode("🛑");
+ var lastPage = DiscordEmoji.FromUnicode("↪");
+ var firstPage = DiscordEmoji.FromUnicode("↩");
+ var previousPage = DiscordEmoji.FromUnicode("⏪");
+ var nextPage = DiscordEmoji.FromUnicode("⏩");
+ var trash = DiscordEmoji.FromUnicode("🗑");
+ var saveEdit = DiscordEmoji.FromUnicode("💾");
+
+ var letterC = DiscordEmoji.FromUnicode("🇨");
+ var letterL = DiscordEmoji.FromUnicode("🇱");
+ var letterR = DiscordEmoji.FromUnicode("🇷");
+ var letterW = DiscordEmoji.FromUnicode("🇼");
+ var letterM = DiscordEmoji.FromUnicode("🇲");
+ var letterE = DiscordEmoji.FromUnicode("🇪");
+
+ DiscordMessage msg = null;
+ string errorMsg = null;
+ DiscordMessage txt;
+ MessageReactionAddEventArgs emoji;
+
+ 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.\n" +
+ "**Must** be sufficiently long to reduce the number of checks."
+ );
+ msg = await msg.UpdateOrCreateMessageAsync(ctx.Channel, "Please specify a new **trigger**", embed: embed).ConfigureAwait(false);
+ errorMsg = null;
+ (msg, txt, emoji) = await interact.WaitForMessageOrReactionAsync(msg, ctx.User, InteractTimeout, abort, lastPage, nextPage, (filter.IsComplete() ? saveEdit : null)).ConfigureAwait(false);
+ if (emoji != null)
+ {
+ if (emoji.Emoji == abort)
+ return (false, msg);
+
+ if (emoji.Emoji == saveEdit)
+ return (true, msg);
+
+ if (emoji.Emoji == lastPage)
+ {
+ if (filter.Actions.HasFlag(FilterAction.ShowExplain))
+ goto step6;
+
+ if (filter.Actions.HasFlag(FilterAction.SendMessage))
+ goto step5;
+
+ goto step4;
+ }
+ }
+ else if (txt?.Content != null)
+ {
+ var existing = await db.Piracystring.FirstOrDefaultAsync(ps => ps.String.Equals(txt.Content, StringComparison.InvariantCultureIgnoreCase)).ConfigureAwait(false);
+ if (existing != null)
+ {
+ if (existing.Disabled)
+ db.Piracystring.Remove(existing);
+ else
+ {
+ errorMsg = $"Trigger `{txt.Content.Sanitize()}` already exists";
+ goto step1;
+ }
+ }
+
+ 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.\n" +
+ $"**`C`** = **`{FilterContext.Chat}`** will apply it in filtering discord messages.\n" +
+ $"**`L`** = **`{FilterContext.Log}`** will apply it during log parsing.\n" +
+ "Reactions will toggle the context, text message will set the specified flags."
+ );
+ msg = await msg.UpdateOrCreateMessageAsync(ctx.Channel, "Please specify filter **context(s)**", embed: embed).ConfigureAwait(false);
+ errorMsg = null;
+ (msg, txt, emoji) = await interact.WaitForMessageOrReactionAsync(msg, ctx.User, InteractTimeout, abort, previousPage, nextPage, letterC, letterL, (filter.IsComplete() ? saveEdit : null)).ConfigureAwait(false);
+ if (emoji != null)
+ {
+ if (emoji.Emoji == abort)
+ return (false, msg);
+
+ if (emoji.Emoji == saveEdit)
+ return (true, msg);
+
+ if (emoji.Emoji == previousPage)
+ goto step1;
+
+ if (emoji.Emoji == letterC)
+ {
+ filter.Context ^= FilterContext.Chat;
+ goto step2;
+ }
+
+ if (emoji.Emoji == letterL)
+ {
+ 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.\n" +
+ $"**`R`** = **`{FilterAction.RemoveMessage}`** will remove the message / log.\n" +
+ $"**`W`** = **`{FilterAction.IssueWarning}`** will issue a warning to the user.\n" +
+ $"**`M`** = **`{FilterAction.SendMessage}`** send _a_ message with an explanation of why it was removed.\n" +
+ $"**`E`** = **`{FilterAction.ShowExplain}`** show `explain` for the specified term (**not implemented**).\n" +
+ "Reactions will toggle the action, text message will set the specified flags."
+ );
+ msg = await msg.UpdateOrCreateMessageAsync(ctx.Channel, "Please specify filter **action(s)**", embed: embed).ConfigureAwait(false);
+ errorMsg = null;
+ (msg, txt, emoji) = await interact.WaitForMessageOrReactionAsync(msg, ctx.User, InteractTimeout, abort, previousPage, nextPage, letterR, letterW, letterM, letterE, (filter.IsComplete() ? saveEdit : null)).ConfigureAwait(false);
+ if (emoji != null)
+ {
+ if (emoji.Emoji == abort)
+ return (false, msg);
+
+ if (emoji.Emoji == saveEdit)
+ return (true, msg);
+
+ if (emoji.Emoji == previousPage)
+ goto step2;
+
+ if (emoji.Emoji == letterR)
+ {
+ filter.Actions ^= FilterAction.RemoveMessage;
+ goto step3;
+ }
+
+ if (emoji.Emoji == letterW)
+ {
+ filter.Actions ^= FilterAction.IssueWarning;
+ goto step3;
+ }
+
+ if (emoji.Emoji == letterM)
+ {
+ filter.Actions ^= FilterAction.SendMessage;
+ goto step3;
+ }
+
+ if (emoji.Emoji == letterE)
+ {
+ filter.Actions ^= FilterAction.ShowExplain;
+ 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.RemoveMessage;
+ 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 "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.\n" +
+ "**Please [test](https://regex101.com/) your regex**. Following flags are enabled: Multiline, IgnoreCase.\n" +
+ "Additional validation can help reduce false positives of a plaintext trigger match."
+ );
+ msg = await msg.UpdateOrCreateMessageAsync(ctx.Channel, "Please specify filter **validation regex**", embed: embed).ConfigureAwait(false);
+ errorMsg = null;
+ var next = (filter.Actions & (FilterAction.SendMessage | FilterAction.ShowExplain)) == 0 ? firstPage : nextPage;
+ (msg, txt, emoji) = await interact.WaitForMessageOrReactionAsync(msg, ctx.User, InteractTimeout, abort, previousPage, next, trash, (filter.IsComplete() ? saveEdit : null)).ConfigureAwait(false);
+ if (emoji != null)
+ {
+ if (emoji.Emoji == abort)
+ return (false, msg);
+
+ if (emoji.Emoji == saveEdit)
+ return (true, msg);
+
+ if (emoji.Emoji == previousPage)
+ goto step3;
+
+ if (emoji.Emoji == firstPage)
+ goto step1;
+
+ if (emoji.Emoji == trash)
+ filter.ValidatingRegex = null;
+ }
+ else if (txt != null)
+ {
+ if (string.IsNullOrWhiteSpace(txt.Content) || txt.Content == "-" || txt.Content == ".*")
+ filter.ValidatingRegex = null;
+ else
+ {
+ try
+ {
+ Regex.IsMatch("test", txt.Content, RegexOptions.Multiline | RegexOptions.IgnoreCase);
+ }
+ 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."
+ );
+ msg = await msg.UpdateOrCreateMessageAsync(ctx.Channel, "Please specify filter **validation regex**", embed: embed).ConfigureAwait(false);
+ errorMsg = null;
+ next = (filter.Actions.HasFlag(FilterAction.ShowExplain) ? nextPage : firstPage);
+ (msg, txt, emoji) = await interact.WaitForMessageOrReactionAsync(msg, ctx.User, InteractTimeout, abort, previousPage, next, (filter.IsComplete() ? saveEdit : null)).ConfigureAwait(false);
+ if (emoji != null)
+ {
+ if (emoji.Emoji == abort)
+ return (false, msg);
+
+ if (emoji.Emoji == saveEdit)
+ return (true, msg);
+
+ if (emoji.Emoji == previousPage)
+ goto step4;
+
+ if (emoji.Emoji == firstPage)
+ 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.\n" +
+ "**__Currently not implemented__**."
+ );
+ msg = await msg.UpdateOrCreateMessageAsync(ctx.Channel, "Please specify filter **explanation term**", embed: embed).ConfigureAwait(false);
+ errorMsg = null;
+ (msg, txt, emoji) = await interact.WaitForMessageOrReactionAsync(msg, ctx.User, InteractTimeout, abort, previousPage, firstPage, (filter.IsComplete() ? saveEdit : null)).ConfigureAwait(false);
+ if (emoji != null)
+ {
+ if (emoji.Emoji == abort)
+ return (false, msg);
+
+ if (emoji.Emoji == saveEdit)
+ return (true, msg);
+
+ if (emoji.Emoji == previousPage)
+ {
+ if (filter.Actions.HasFlag(FilterAction.SendMessage))
+ goto step5;
+ else
+ goto step4;
+ }
+
+ if (emoji.Emoji == firstPage)
+ goto step1;
+ }
+ else if (txt != null)
+ {
+ if (string.IsNullOrWhiteSpace(txt.Content) || txt.Content == "-")
+ filter.ExplainTerm = null;
+ else
+ {
+ var existingTerm = await db.Explanation.FirstOrDefaultAsync(exp => exp.Keyword == txt.Content.ToLowerInvariant()).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";
+ embed = FormatFilter(filter, errorMsg);
+ msg = await msg.UpdateOrCreateMessageAsync(ctx.Channel, "Does this look good? (y/n)", embed: embed.Build()).ConfigureAwait(false);
+ errorMsg = null;
+ (msg, txt, emoji) = await interact.WaitForMessageOrReactionAsync(msg, ctx.User, InteractTimeout, abort, previousPage, firstPage, (filter.IsComplete() ? saveEdit : null)).ConfigureAwait(false);
+ if (emoji != null)
+ {
+ if (emoji.Emoji == abort)
+ return (false, msg);
+
+ if (emoji.Emoji == saveEdit)
+ return (true, msg);
+
+ if (emoji.Emoji == previousPage)
+ {
+ if (filter.Actions.HasFlag(FilterAction.ShowExplain))
+ goto step6;
+
+ if (filter.Actions.HasFlag(FilterAction.SendMessage))
+ goto step5;
+
+ goto step4;
+ }
+
+ if (emoji.Emoji == firstPage)
+ 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;
+ var result = new DiscordEmbedBuilder
+ {
+ Title = "Filter preview",
+ Color = string.IsNullOrEmpty(error) ? Config.Colors.Help : Config.Colors.Maintenance,
+ };
+ if (!string.IsNullOrEmpty(error))
+ result.AddField("Entry error", error);
+
+ result.AddFieldEx((string.IsNullOrEmpty(filter.String) ? "⚠ " : "") + "Trigger", filter.String, highlight == field++, true)
+ .AddFieldEx("Context", filter.Context.ToString(), highlight == field++, true)
+ .AddFieldEx("Actions", filter.Actions.ToFlagsString(), highlight == field++, true)
+ .AddFieldEx("Validation", filter.ValidatingRegex, highlight == field++, true);
+ if (filter.Actions.HasFlag(FilterAction.SendMessage))
+ result.AddFieldEx("Message", filter.CustomMessage, highlight == field, true);
+ field++;
+ if (filter.Actions.HasFlag(FilterAction.ShowExplain))
+ result.AddFieldEx((string.IsNullOrEmpty(filter.ExplainTerm) ? "⚠ " : "") + "Explain", filter.ExplainTerm, highlight == field, true);
+ return result;
}
}
}
diff --git a/CompatBot/Commands/Attributes/RequiresDm.cs b/CompatBot/Commands/Attributes/RequiresDm.cs
index 76270887..8680df3c 100644
--- a/CompatBot/Commands/Attributes/RequiresDm.cs
+++ b/CompatBot/Commands/Attributes/RequiresDm.cs
@@ -1,4 +1,6 @@
using System;
+using System.IO;
+using System.Net.Http;
using System.Threading.Tasks;
using DSharpPlus.CommandsNext;
using DSharpPlus.CommandsNext.Attributes;
@@ -8,12 +10,20 @@ namespace CompatBot.Commands.Attributes
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = false)]
internal class RequiresDm: CheckBaseAttribute
{
+ private const string Source = "https://cdn.discordapp.com/attachments/417347469521715210/534798232858001418/24qx11.jpg";
+ private static readonly Lazy Poster = new Lazy(() =>
+ {
+ using (var client = HttpClientFactory.Create())
+ return client.GetByteArrayAsync(Source).ConfigureAwait(true).GetAwaiter().GetResult();
+ });
+
public override async Task ExecuteCheckAsync(CommandContext ctx, bool help)
{
if (ctx.Channel.IsPrivate || help)
return true;
- await ctx.RespondAsync($"{ctx.Message.Author.Mention} https://cdn.discordapp.com/attachments/417347469521715210/534798232858001418/24qx11.jpg").ConfigureAwait(false);
+ using (var stream = new MemoryStream(Poster.Value))
+ await ctx.RespondWithFileAsync("senpai_plz.jpg", stream).ConfigureAwait(false);
return false;
}
}
diff --git a/CompatBot/Commands/CompatList.cs b/CompatBot/Commands/CompatList.cs
index 02e8aa62..0ee7583b 100644
--- a/CompatBot/Commands/CompatList.cs
+++ b/CompatBot/Commands/CompatList.cs
@@ -60,7 +60,7 @@ namespace CompatBot.Commands
if (!await DiscordInviteFilter.CheckMessageForInvitesAsync(ctx.Client, ctx.Message).ConfigureAwait(false))
return;
- if (!await AntipiracyMonitor.IsClean(ctx.Client, ctx.Message).ConfigureAwait(false))
+ if (!await ContentFilter.IsClean(ctx.Client, ctx.Message).ConfigureAwait(false))
return;
var productCodes = ProductCodeLookup.GetProductIds(ctx.Message.Content);
@@ -89,7 +89,7 @@ Example usage:
!top 10 playable
!top 10 new ingame
!top 10 old loadable bluray")]
- public async Task Top(CommandContext ctx, [Description("To see all filters do !filters")] params string[] filters)
+ public async Task Top(CommandContext ctx, [Description("You can use game status or media (psn/blu-ray)")] params string[] filters)
{
var requestBuilder = RequestBuilder.Start();
var age = "new";
@@ -126,18 +126,6 @@ Example usage:
await DoRequestAndRespond(ctx, requestBuilder).ConfigureAwait(false);
}
- [Command("filters"), Hidden]
- [Description("Provides information about available filters for the !top command")]
- public async Task Filters(CommandContext ctx)
- {
- var embed = new DiscordEmbedBuilder {Description = "List of recognized tokens in each filter category", Color = Config.Colors.Help}
- .AddField("Statuses", DicToDesc(ApiConfig.Statuses))
- .AddField("Release types", DicToDesc(ApiConfig.ReleaseTypes))
- .Build();
- var ch = await ctx.GetChannelForSpamAsync().ConfigureAwait(false);
- await ch.SendMessageAsync(embed: embed).ConfigureAwait(false);
- }
-
[Group("latest"), TriggersTyping]
[Description("Provides links to the latest RPCS3 build")]
[Cooldown(1, 30, CooldownBucketType.Channel)]
diff --git a/CompatBot/Commands/Explain.cs b/CompatBot/Commands/Explain.cs
index 2380b0a4..089cd907 100644
--- a/CompatBot/Commands/Explain.cs
+++ b/CompatBot/Commands/Explain.cs
@@ -62,7 +62,7 @@ namespace CompatBot.Commands
if (!await DiscordInviteFilter.CheckMessageForInvitesAsync(ctx.Client, ctx.Message).ConfigureAwait(false))
return;
- if (!await AntipiracyMonitor.IsClean(ctx.Client, ctx.Message).ConfigureAwait(false))
+ if (!await ContentFilter.IsClean(ctx.Client, ctx.Message).ConfigureAwait(false))
return;
term = term.ToLowerInvariant();
diff --git a/CompatBot/CompatBot.csproj b/CompatBot/CompatBot.csproj
index eb6af075..067cf2a6 100644
--- a/CompatBot/CompatBot.csproj
+++ b/CompatBot/CompatBot.csproj
@@ -22,9 +22,9 @@
-
-
-
+
+
+
@@ -37,9 +37,10 @@
-
+
-
+
+
diff --git a/CompatBot/Config.cs b/CompatBot/Config.cs
index 06c23ee6..22d49f06 100644
--- a/CompatBot/Config.cs
+++ b/CompatBot/Config.cs
@@ -8,10 +8,14 @@ using System.Threading;
using DSharpPlus.Entities;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.UserSecrets;
+using Microsoft.Extensions.Logging;
using NLog;
+using NLog.Extensions.Logging;
using NLog.Filters;
using NLog.Targets;
using NLog.Targets.Wrappers;
+using ILogger = NLog.ILogger;
+using LogLevel = NLog.LogLevel;
namespace CompatBot
{
@@ -19,6 +23,7 @@ namespace CompatBot
{
private static readonly IConfigurationRoot config;
internal static readonly ILogger Log;
+ internal static readonly ILoggerFactory LoggerFactory;
internal static readonly ConcurrentDictionary inMemorySettings = new ConcurrentDictionary();
public static readonly CancellationTokenSource Cts = new CancellationTokenSource();
@@ -46,6 +51,8 @@ namespace CompatBot
public static int LogSizeLimit => config.GetValue(nameof(LogSizeLimit), 64 * 1024 * 1024);
public static int MinimumBufferSize => config.GetValue(nameof(MinimumBufferSize), 512);
public static int BuildNumberDifferenceForOutdatedBuilds => config.GetValue(nameof(BuildNumberDifferenceForOutdatedBuilds), 10);
+ public static int MinimumPiracyTriggerLength => config.GetValue(nameof(MinimumPiracyTriggerLength), 4);
+
public static string Token => config.GetValue(nameof(Token), "");
public static string LogPath => config.GetValue(nameof(LogPath), "logs/bot.log"); // paths are relative to the assembly, so this will put it in the project's root
public static string IrdCachePath => config.GetValue(nameof(IrdCachePath), "./ird/");
@@ -147,6 +154,7 @@ namespace CompatBot
.AddInMemoryCollection(inMemorySettings) // higher priority
.Build();
Log = GetLog();
+ LoggerFactory = new NLogLoggerFactory();
}
catch (Exception e)
{
diff --git a/CompatBot/Database/BotDb.cs b/CompatBot/Database/BotDb.cs
index ec84f773..9e39c4ed 100644
--- a/CompatBot/Database/BotDb.cs
+++ b/CompatBot/Database/BotDb.cs
@@ -21,6 +21,9 @@ namespace CompatBot.Database
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
var dbPath = DbImporter.GetDbPath("bot.db", Environment.SpecialFolder.ApplicationData);
+#if DEBUG
+ optionsBuilder.UseLoggerFactory(Config.LoggerFactory);
+#endif
optionsBuilder.UseSqlite($"Data Source=\"{dbPath}\"");
}
@@ -30,6 +33,8 @@ namespace CompatBot.Database
modelBuilder.Entity().HasIndex(m => m.Key).IsUnique().HasName("bot_state_key");
modelBuilder.Entity().HasIndex(m => m.DiscordId).IsUnique().HasName("moderator_discord_id");
modelBuilder.Entity().HasIndex(ps => ps.String).IsUnique().HasName("piracystring_string");
+ modelBuilder.Entity().Property(ps => ps.Context).HasDefaultValue(FilterContext.Chat | FilterContext.Log);
+ modelBuilder.Entity().Property(ps => ps.Actions).HasDefaultValue(FilterAction.RemoveMessage | FilterAction.IssueWarning | FilterAction.SendMessage);
modelBuilder.Entity().HasIndex(w => w.DiscordId).HasName("warning_discord_id");
modelBuilder.Entity().HasIndex(e => e.Keyword).IsUnique().HasName("explanation_keyword");
modelBuilder.Entity().HasIndex(c => c.Command).IsUnique().HasName("disabled_command_command");
@@ -64,6 +69,28 @@ namespace CompatBot.Database
public int Id { get; set; }
[Required, Column(TypeName = "varchar(255)")]
public string String { get; set; }
+ public string ValidatingRegex { get; set; }
+ public FilterContext Context { get; set; }
+ public FilterAction Actions { get; set; }
+ public string ExplainTerm { get; set; }
+ public string CustomMessage { get; set; }
+ public bool Disabled { get; set; }
+ }
+
+ [Flags]
+ internal enum FilterContext: byte
+ {
+ Chat = 0b_0000_0001,
+ Log = 0b_0000_0010,
+ }
+
+ [Flags]
+ internal enum FilterAction
+ {
+ RemoveMessage = 0b_0000_0001,
+ IssueWarning = 0b_0000_0010,
+ ShowExplain = 0b_0000_0100,
+ SendMessage = 0b_0000_1000,
}
internal class Warning
diff --git a/CompatBot/Database/Migrations/BotDb/20190723151803_ContentFilter2.0.Designer.cs b/CompatBot/Database/Migrations/BotDb/20190723151803_ContentFilter2.0.Designer.cs
new file mode 100644
index 00000000..d328f146
--- /dev/null
+++ b/CompatBot/Database/Migrations/BotDb/20190723151803_ContentFilter2.0.Designer.cs
@@ -0,0 +1,290 @@
+//
+using System;
+using CompatBot.Database;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+namespace CompatBot.Database.Migrations
+{
+ [DbContext(typeof(BotDb))]
+ [Migration("20190723151803_ContentFilter2.0")]
+ partial class ContentFilter20
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "2.2.6-servicing-10079");
+
+ modelBuilder.Entity("CompatBot.Database.BotState", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id");
+
+ b.Property("Key")
+ .HasColumnName("key");
+
+ b.Property("Value")
+ .HasColumnName("value");
+
+ b.HasKey("Id")
+ .HasName("id");
+
+ b.HasIndex("Key")
+ .IsUnique()
+ .HasName("bot_state_key");
+
+ b.ToTable("bot_state");
+ });
+
+ modelBuilder.Entity("CompatBot.Database.DisabledCommand", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id");
+
+ b.Property("Command")
+ .IsRequired()
+ .HasColumnName("command");
+
+ b.HasKey("Id")
+ .HasName("id");
+
+ b.HasIndex("Command")
+ .IsUnique()
+ .HasName("disabled_command_command");
+
+ b.ToTable("disabled_commands");
+ });
+
+ modelBuilder.Entity("CompatBot.Database.EventSchedule", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id");
+
+ b.Property("End")
+ .HasColumnName("end");
+
+ b.Property("EventName")
+ .HasColumnName("event_name");
+
+ b.Property("Name")
+ .HasColumnName("name");
+
+ b.Property("Start")
+ .HasColumnName("start");
+
+ b.Property("Year")
+ .HasColumnName("year");
+
+ b.HasKey("Id")
+ .HasName("id");
+
+ b.HasIndex("Year", "EventName")
+ .HasName("event_schedule_year_event_name");
+
+ b.ToTable("event_schedule");
+ });
+
+ modelBuilder.Entity("CompatBot.Database.Explanation", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id");
+
+ b.Property("Attachment")
+ .HasColumnName("attachment")
+ .HasMaxLength(7340032);
+
+ b.Property("AttachmentFilename")
+ .HasColumnName("attachment_filename");
+
+ b.Property("Keyword")
+ .IsRequired()
+ .HasColumnName("keyword");
+
+ b.Property("Text")
+ .IsRequired()
+ .HasColumnName("text");
+
+ b.HasKey("Id")
+ .HasName("id");
+
+ b.HasIndex("Keyword")
+ .IsUnique()
+ .HasName("explanation_keyword");
+
+ b.ToTable("explanation");
+ });
+
+ modelBuilder.Entity("CompatBot.Database.Moderator", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id");
+
+ b.Property("DiscordId")
+ .HasColumnName("discord_id");
+
+ b.Property("Sudoer")
+ .HasColumnName("sudoer");
+
+ b.HasKey("Id")
+ .HasName("id");
+
+ b.HasIndex("DiscordId")
+ .IsUnique()
+ .HasName("moderator_discord_id");
+
+ b.ToTable("moderator");
+ });
+
+ modelBuilder.Entity("CompatBot.Database.Piracystring", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id");
+
+ b.Property("Actions")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("actions")
+ .HasDefaultValue(11);
+
+ b.Property("Context")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("context")
+ .HasDefaultValue((byte)3);
+
+ b.Property("CustomMessage")
+ .HasColumnName("custom_message");
+
+ b.Property("Disabled")
+ .HasColumnName("disabled");
+
+ b.Property("ExplainTerm")
+ .HasColumnName("explain_term");
+
+ b.Property("String")
+ .IsRequired()
+ .HasColumnName("string")
+ .HasColumnType("varchar(255)");
+
+ b.Property("ValidatingRegex")
+ .HasColumnName("validating_regex");
+
+ b.HasKey("Id")
+ .HasName("id");
+
+ b.HasIndex("String")
+ .IsUnique()
+ .HasName("piracystring_string");
+
+ b.ToTable("piracystring");
+ });
+
+ modelBuilder.Entity("CompatBot.Database.Stats", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id");
+
+ b.Property("Category")
+ .IsRequired()
+ .HasColumnName("category");
+
+ b.Property("ExpirationTimestamp")
+ .HasColumnName("expiration_timestamp");
+
+ b.Property("Key")
+ .IsRequired()
+ .HasColumnName("key");
+
+ b.Property("Value")
+ .HasColumnName("value");
+
+ b.HasKey("Id")
+ .HasName("id");
+
+ b.HasIndex("Category", "Key")
+ .IsUnique()
+ .HasName("stats_category_key");
+
+ b.ToTable("stats");
+ });
+
+ modelBuilder.Entity("CompatBot.Database.Warning", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id");
+
+ b.Property("DiscordId")
+ .HasColumnName("discord_id");
+
+ b.Property("FullReason")
+ .IsRequired()
+ .HasColumnName("full_reason");
+
+ b.Property("IssuerId")
+ .HasColumnName("issuer_id");
+
+ b.Property("Reason")
+ .IsRequired()
+ .HasColumnName("reason");
+
+ b.Property("Retracted")
+ .HasColumnName("retracted");
+
+ b.Property("RetractedBy")
+ .HasColumnName("retracted_by");
+
+ b.Property("RetractionReason")
+ .HasColumnName("retraction_reason");
+
+ b.Property("RetractionTimestamp")
+ .HasColumnName("retraction_timestamp");
+
+ b.Property("Timestamp")
+ .HasColumnName("timestamp");
+
+ b.HasKey("Id")
+ .HasName("id");
+
+ b.HasIndex("DiscordId")
+ .HasName("warning_discord_id");
+
+ b.ToTable("warning");
+ });
+
+ modelBuilder.Entity("CompatBot.Database.WhitelistedInvite", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id");
+
+ b.Property("GuildId")
+ .HasColumnName("guild_id");
+
+ b.Property("InviteCode")
+ .HasColumnName("invite_code");
+
+ b.Property("Name")
+ .HasColumnName("name");
+
+ b.HasKey("Id")
+ .HasName("id");
+
+ b.HasIndex("GuildId")
+ .IsUnique()
+ .HasName("whitelisted_invite_guild_id");
+
+ b.ToTable("whitelisted_invites");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/CompatBot/Database/Migrations/BotDb/20190723151803_ContentFilter2.0.cs b/CompatBot/Database/Migrations/BotDb/20190723151803_ContentFilter2.0.cs
new file mode 100644
index 00000000..a1cde6e9
--- /dev/null
+++ b/CompatBot/Database/Migrations/BotDb/20190723151803_ContentFilter2.0.cs
@@ -0,0 +1,70 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+namespace CompatBot.Database.Migrations
+{
+ public partial class ContentFilter20 : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "actions",
+ table: "piracystring",
+ nullable: false,
+ defaultValue: 11);
+
+ migrationBuilder.AddColumn(
+ name: "context",
+ table: "piracystring",
+ nullable: false,
+ defaultValue: (byte)3);
+
+ migrationBuilder.AddColumn(
+ name: "custom_message",
+ table: "piracystring",
+ nullable: true);
+
+ migrationBuilder.AddColumn(
+ name: "disabled",
+ table: "piracystring",
+ nullable: false,
+ defaultValue: false);
+
+ migrationBuilder.AddColumn(
+ name: "explain_term",
+ table: "piracystring",
+ nullable: true);
+
+ migrationBuilder.AddColumn(
+ name: "validating_regex",
+ table: "piracystring",
+ nullable: true);
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "actions",
+ table: "piracystring");
+
+ migrationBuilder.DropColumn(
+ name: "context",
+ table: "piracystring");
+
+ migrationBuilder.DropColumn(
+ name: "custom_message",
+ table: "piracystring");
+
+ migrationBuilder.DropColumn(
+ name: "disabled",
+ table: "piracystring");
+
+ migrationBuilder.DropColumn(
+ name: "explain_term",
+ table: "piracystring");
+
+ migrationBuilder.DropColumn(
+ name: "validating_regex",
+ table: "piracystring");
+ }
+ }
+}
diff --git a/CompatBot/Database/Migrations/BotDb/BotDbModelSnapshot.cs b/CompatBot/Database/Migrations/BotDb/BotDbModelSnapshot.cs
index acd6e9b4..d0c3b95f 100644
--- a/CompatBot/Database/Migrations/BotDb/BotDbModelSnapshot.cs
+++ b/CompatBot/Database/Migrations/BotDb/BotDbModelSnapshot.cs
@@ -14,7 +14,7 @@ namespace CompatBot.Database.Migrations
{
#pragma warning disable 612, 618
modelBuilder
- .HasAnnotation("ProductVersion", "2.2.2-servicing-10034");
+ .HasAnnotation("ProductVersion", "2.2.6-servicing-10079");
modelBuilder.Entity("CompatBot.Database.BotState", b =>
{
@@ -147,11 +147,33 @@ namespace CompatBot.Database.Migrations
.ValueGeneratedOnAdd()
.HasColumnName("id");
+ b.Property("Actions")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("actions")
+ .HasDefaultValue(11);
+
+ b.Property("Context")
+ .ValueGeneratedOnAdd()
+ .HasColumnName("context")
+ .HasDefaultValue((byte)3);
+
+ b.Property("CustomMessage")
+ .HasColumnName("custom_message");
+
+ b.Property("Disabled")
+ .HasColumnName("disabled");
+
+ b.Property("ExplainTerm")
+ .HasColumnName("explain_term");
+
b.Property("String")
.IsRequired()
.HasColumnName("string")
.HasColumnType("varchar(255)");
+ b.Property("ValidatingRegex")
+ .HasColumnName("validating_regex");
+
b.HasKey("Id")
.HasName("id");
diff --git a/CompatBot/Database/Providers/ContentFilter.cs b/CompatBot/Database/Providers/ContentFilter.cs
new file mode 100644
index 00000000..45717b92
--- /dev/null
+++ b/CompatBot/Database/Providers/ContentFilter.cs
@@ -0,0 +1,148 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using CompatApiClient.Utils;
+using CompatBot.Commands;
+using CompatBot.Utils;
+using CompatBot.Utils.Extensions;
+using DSharpPlus;
+using DSharpPlus.Entities;
+using Microsoft.EntityFrameworkCore;
+using NReco.Text;
+
+namespace CompatBot.Database.Providers
+{
+ internal static class ContentFilter
+ {
+ private static readonly object SyncObject = new object();
+ private static Dictionary> filters = new Dictionary>();
+
+ static ContentFilter()
+ {
+ RebuildMatcher();
+ }
+
+ public static Task FindTriggerAsync(FilterContext ctx, string str)
+ {
+ if (string.IsNullOrEmpty(str))
+ return Task.FromResult((Piracystring)null);
+
+
+ if (!filters.TryGetValue(ctx, out var matcher))
+ return Task.FromResult((Piracystring)null);
+
+ Piracystring result = null;
+ matcher?.ParseText(str, h =>
+ {
+ if (string.IsNullOrEmpty(h.Value.ValidatingRegex) || Regex.IsMatch(str, h.Value.ValidatingRegex, RegexOptions.IgnoreCase | RegexOptions.Multiline))
+ {
+ result = h.Value;
+ return h.Value.Actions.HasFlag(FilterAction.RemoveMessage);
+ }
+ return true;
+ });
+
+ return Task.FromResult(result);
+ }
+
+ private static void RebuildMatcher()
+ {
+ var newFilters = new Dictionary>();
+ using (var db = new BotDb())
+ foreach (FilterContext ctx in Enum.GetValues(typeof(FilterContext)))
+ {
+ var f = db.Piracystring.Where(ps => ps.Disabled == false && ps.Context.HasFlag(ctx)).AsNoTracking().ToList();
+ newFilters[ctx] = f.Count == 0 ? null : new AhoCorasickDoubleArrayTrie(f.ToDictionary(s => s.String, s => s), true);
+ }
+ filters = newFilters;
+ }
+
+
+ public static async Task IsClean(DiscordClient client, DiscordMessage message)
+ {
+ if (message.Channel.IsPrivate)
+ return true;
+
+ if (message.Author.IsBotSafeCheck())
+ return true;
+
+#if !DEBUG
+ if (message.Author.IsWhitelisted(client, message.Channel.Guild))
+ return true;
+#endif
+
+ if (string.IsNullOrEmpty(message.Content))
+ return true;
+
+ var severity = ReportSeverity.Low;
+ var completedActions = new List();
+ var trigger = await FindTriggerAsync(FilterContext.Chat, message.Content).ConfigureAwait(false);
+ if (trigger == null)
+ return true;
+
+ if (trigger.Actions.HasFlag(FilterAction.RemoveMessage))
+ {
+ try
+ {
+ await message.Channel.DeleteMessageAsync(message, $"Removed according to filter '{trigger}'").ConfigureAwait(false);
+ completedActions.Add(FilterAction.RemoveMessage);
+ }
+ catch
+ {
+ severity = ReportSeverity.High;
+ }
+ }
+
+ if (trigger.Actions.HasFlag(FilterAction.IssueWarning))
+ {
+ try
+ {
+ await Warnings.AddAsync(client, message, message.Author.Id, message.Author.Username, client.CurrentUser, "Mention of piracy", message.Content.Sanitize()).ConfigureAwait(false);
+ completedActions.Add(FilterAction.IssueWarning);
+ }
+ catch (Exception e)
+ {
+ Config.Log.Warn(e, $"Couldn't issue warning in #{message.Channel.Name}");
+ }
+ }
+
+ if (trigger.Actions.HasFlag(FilterAction.SendMessage))
+ {
+ try
+ {
+ var msgContent = trigger.CustomMessage;
+ if (string.IsNullOrEmpty(msgContent))
+ {
+ var rules = await client.GetChannelAsync(Config.BotRulesChannelId).ConfigureAwait(false);
+ msgContent = $"{message.Author.Mention} Please follow the {rules.Mention} and do not discuss piracy on this server. Repeated offence may result in a ban.";
+ }
+ await message.Channel.SendMessageAsync(msgContent).ConfigureAwait(false);
+ completedActions.Add(FilterAction.SendMessage);
+ }
+ catch (Exception e)
+ {
+ Config.Log.Warn(e, $"Failed to send message in #{message.Channel.Name}");
+ }
+ }
+
+ var actionList = "";
+ foreach (FilterAction fa in Enum.GetValues(typeof(FilterAction)))
+ {
+ if (trigger.Actions.HasFlag(fa))
+ actionList += (completedActions.Contains(fa) ? "✅" : "❌") + " " + fa + ' ';
+ }
+
+ try
+ {
+ await client.ReportAsync("🤬 Content filter", message, trigger.String, message.Content, severity, actionList).ConfigureAwait(false);
+ }
+ catch (Exception e)
+ {
+ Config.Log.Error(e, "Failed to report content filter");
+ }
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/CompatBot/Database/Providers/PiracyStringProvider.cs b/CompatBot/Database/Providers/PiracyStringProvider.cs
deleted file mode 100644
index 0683c1cd..00000000
--- a/CompatBot/Database/Providers/PiracyStringProvider.cs
+++ /dev/null
@@ -1,85 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading.Tasks;
-using Microsoft.EntityFrameworkCore;
-using NReco.Text;
-
-namespace CompatBot.Database.Providers
-{
- internal static class PiracyStringProvider
- {
- private static readonly BotDb db = new BotDb();
- private static readonly object SyncObj = new object();
- private static readonly List PiracyStrings;
- private static AhoCorasickDoubleArrayTrie matcher;
-
- static PiracyStringProvider()
- {
- PiracyStrings = db.Piracystring.Select(ps => ps.String).ToList();
- RebuildMatcher();
- }
-
- public static async Task AddAsync(string trigger)
- {
- if (PiracyStrings.Contains(trigger, StringComparer.InvariantCultureIgnoreCase))
- return false;
-
- lock (SyncObj)
- {
- if (PiracyStrings.Contains(trigger, StringComparer.InvariantCultureIgnoreCase))
- return false;
-
- PiracyStrings.Add(trigger);
- RebuildMatcher();
- }
- await db.Piracystring.AddAsync(new Piracystring {String = trigger}).ConfigureAwait(false);
- await db.SaveChangesAsync().ConfigureAwait(false);
- return true;
- }
-
- public static async Task RemoveAsync(int id)
- {
- var dbItem = await db.Piracystring.FirstOrDefaultAsync(ps => ps.Id == id).ConfigureAwait(false);
- if (dbItem == null)
- return false;
-
- db.Piracystring.Remove(dbItem);
- if (!PiracyStrings.Contains(dbItem.String))
- return false;
-
- lock (SyncObj)
- {
- if (!PiracyStrings.Remove(dbItem.String))
- return false;
-
- RebuildMatcher();
- }
-
- await db.SaveChangesAsync().ConfigureAwait(false);
- return true;
- }
-
- public static async Task GetTriggerAsync(int id)
- {
- var dbItem = await db.Piracystring.FirstOrDefaultAsync(ps => ps.Id == id).ConfigureAwait(false);
- return dbItem?.String;
- }
-
- public static Task FindTriggerAsync(string str)
- {
- string result = null;
- matcher?.ParseText(str, h =>
- {
- result = h.Value;
- return false;
- });
- return Task.FromResult(result);
- }
-
- private static void RebuildMatcher()
- {
- matcher = PiracyStrings.Count == 0 ? null : new AhoCorasickDoubleArrayTrie(PiracyStrings.ToDictionary(s => s, s => s), true);
- }
- }
-}
\ No newline at end of file
diff --git a/CompatBot/Database/ThumbnailDb.cs b/CompatBot/Database/ThumbnailDb.cs
index c69b4d80..c2f0d2d7 100644
--- a/CompatBot/Database/ThumbnailDb.cs
+++ b/CompatBot/Database/ThumbnailDb.cs
@@ -1,6 +1,5 @@
using System;
using System.ComponentModel.DataAnnotations;
-using System.IO;
using CompatApiClient;
using Microsoft.EntityFrameworkCore;
@@ -15,6 +14,9 @@ namespace CompatBot.Database
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
var dbPath = DbImporter.GetDbPath("thumbs.db", Environment.SpecialFolder.LocalApplicationData);
+#if DEBUG
+ optionsBuilder.UseLoggerFactory(Config.LoggerFactory);
+#endif
optionsBuilder.UseSqlite($"Data Source=\"{dbPath}\"");
}
diff --git a/CompatBot/EventHandlers/AntipiracyMonitor.cs b/CompatBot/EventHandlers/AntipiracyMonitor.cs
index 4fb3efca..598aa723 100644
--- a/CompatBot/EventHandlers/AntipiracyMonitor.cs
+++ b/CompatBot/EventHandlers/AntipiracyMonitor.cs
@@ -1,12 +1,7 @@
-using System;
-using System.Threading.Tasks;
-using CompatApiClient.Utils;
-using CompatBot.Commands;
+using System.Threading.Tasks;
using CompatBot.Database.Providers;
using CompatBot.Utils;
using CompatBot.Utils.Extensions;
-using DSharpPlus;
-using DSharpPlus.Entities;
using DSharpPlus.EventArgs;
namespace CompatBot.EventHandlers
@@ -15,12 +10,12 @@ namespace CompatBot.EventHandlers
{
public static async Task OnMessageCreated(MessageCreateEventArgs args)
{
- args.Handled = !await IsClean(args.Client, args.Message).ConfigureAwait(false);
+ args.Handled = !await ContentFilter.IsClean(args.Client, args.Message).ConfigureAwait(false);
}
public static async Task OnMessageUpdated(MessageUpdateEventArgs args)
{
- args.Handled = !await IsClean(args.Client, args.Message).ConfigureAwait(false);
+ args.Handled = !await ContentFilter.IsClean(args.Client, args.Message).ConfigureAwait(false);
}
public static async Task OnReaction(MessageReactionAddEventArgs e)
@@ -33,55 +28,7 @@ namespace CompatBot.EventHandlers
return;
var message = await e.Channel.GetMessageAsync(e.Message.Id).ConfigureAwait(false);
- await IsClean(e.Client, message).ConfigureAwait(false);
- }
-
- public static async Task IsClean(DiscordClient client, DiscordMessage message)
- {
- if (message.Channel.IsPrivate)
- return true;
-
- if (message.Author.IsBotSafeCheck())
- return true;
-
-#if !DEBUG
- if (message.Author.IsWhitelisted(client, message.Channel.Guild))
- return true;
-#endif
-
- if (string.IsNullOrEmpty(message.Content))
- return true;
-
- string trigger = null;
- var severity = ReportSeverity.Low;
- try
- {
- trigger = await PiracyStringProvider.FindTriggerAsync(message.Content);
- if (trigger == null)
- return true;
-
- await message.Channel.DeleteMessageAsync(message, $"Mention of piracy trigger '{trigger}'").ConfigureAwait(false);
- }
- catch (Exception e)
- {
- Config.Log.Warn(e, $"Couldn't delete message in {message.Channel.Name}");
- severity = ReportSeverity.High;
- }
- try
- {
- var rules = await client.GetChannelAsync(Config.BotRulesChannelId).ConfigureAwait(false);
- var yarr = client.GetEmoji(":piratethink:", "🦜");
- await Task.WhenAll(
- message.Channel.SendMessageAsync($"{message.Author.Mention} Please follow the {rules.Mention} and do not discuss piracy on this server. Repeated offence may result in a ban."),
- client.ReportAsync(yarr + " Mention of piracy", message, trigger, message.Content, severity),
- Warnings.AddAsync(client, message, message.Author.Id, message.Author.Username, client.CurrentUser, "Mention of piracy", message.Content.Sanitize())
- ).ConfigureAwait(false);
- }
- catch (Exception e)
- {
- Config.Log.Warn(e, $"Couldn't finish piracy trigger actions for a message in {message.Channel.Name}");
- }
- return false;
+ await ContentFilter.IsClean(e.Client, message).ConfigureAwait(false);
}
}
}
diff --git a/CompatBot/EventHandlers/LogParsing/LogParser.LogSections.cs b/CompatBot/EventHandlers/LogParsing/LogParser.LogSections.cs
index 869a3b47..27a81bc6 100644
--- a/CompatBot/EventHandlers/LogParsing/LogParser.LogSections.cs
+++ b/CompatBot/EventHandlers/LogParsing/LogParser.LogSections.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Collections.Specialized;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
+using CompatBot.Database;
using CompatBot.Database.Providers;
using CompatBot.EventHandlers.LogParsing.POCOs;
using CompatBot.Utils;
@@ -210,9 +211,10 @@ namespace CompatBot.EventHandlers.LogParsing
private static async Task PiracyCheckAsync(string line, LogParseState state)
{
- if (await PiracyStringProvider.FindTriggerAsync(line).ConfigureAwait(false) is string match)
+ if (await ContentFilter.FindTriggerAsync(FilterContext.Log, line).ConfigureAwait(false) is Piracystring match
+ && match.Actions.HasFlag(FilterAction.RemoveMessage))
{
- state.PiracyTrigger = match;
+ state.PiracyTrigger = match.String;
state.PiracyContext = line.ToUtf8();
state.Error = LogParseState.ErrorCode.PiracyDetected;
}
diff --git a/CompatBot/Utils/Extensions/BotDbExtensions.cs b/CompatBot/Utils/Extensions/BotDbExtensions.cs
index 387f0bf7..c87d20ab 100644
--- a/CompatBot/Utils/Extensions/BotDbExtensions.cs
+++ b/CompatBot/Utils/Extensions/BotDbExtensions.cs
@@ -11,5 +11,15 @@ namespace CompatBot.Utils
&& evt.Year > 0
&& !string.IsNullOrEmpty(evt.Name);
}
+
+ public static bool IsComplete(this Piracystring filter)
+ {
+ var result = !string.IsNullOrEmpty(filter.String)
+ && filter.String.Length > 4
+ && filter.Actions != 0;
+ if (result && filter.Actions.HasFlag(FilterAction.ShowExplain))
+ result = !string.IsNullOrEmpty(filter.ExplainTerm);
+ return result;
+ }
}
}
diff --git a/CompatBot/Utils/Extensions/DiscordClientExtensions.cs b/CompatBot/Utils/Extensions/DiscordClientExtensions.cs
index 4a8b0154..2c47adbc 100644
--- a/CompatBot/Utils/Extensions/DiscordClientExtensions.cs
+++ b/CompatBot/Utils/Extensions/DiscordClientExtensions.cs
@@ -103,10 +103,10 @@ namespace CompatBot.Utils
return messages.TakeWhile(m => m.CreationTimestamp > afterTime).ToList().AsReadOnly();
}
- public static async Task ReportAsync(this DiscordClient client, string infraction, DiscordMessage message, string trigger, string context, ReportSeverity severity)
+ public static async Task ReportAsync(this DiscordClient client, string infraction, DiscordMessage message, string trigger, string context, ReportSeverity severity, string actionList = null)
{
var getLogChannelTask = client.GetChannelAsync(Config.BotLogId);
- var embedBuilder = MakeReportTemplate(client, infraction, message, severity);
+ var embedBuilder = MakeReportTemplate(client, infraction, message, severity, actionList);
var reportText = string.IsNullOrEmpty(trigger) ? "" : $"Triggered by: `{trigger}`{Environment.NewLine}";
if (!string.IsNullOrEmpty(context))
reportText += $"Triggered in: ```{context.Sanitize()}```{Environment.NewLine}";
@@ -180,7 +180,7 @@ namespace CompatBot.Utils
return channel.SendMessageAsync(message);
}
- private static DiscordEmbedBuilder MakeReportTemplate(DiscordClient client, string infraction, DiscordMessage message, ReportSeverity severity)
+ private static DiscordEmbedBuilder MakeReportTemplate(DiscordClient client, string infraction, DiscordMessage message, ReportSeverity severity, string actionList = null)
{
var content = message.Content;
if (message.Channel.IsPrivate)
@@ -222,8 +222,13 @@ namespace CompatBot.Utils
.AddField("Channel", message.Channel.IsPrivate ? "Bot's DM" : message.Channel.Mention, true)
.AddField("Time (UTC)", message.CreationTimestamp.ToString("yyyy-MM-dd HH:mm:ss"), true)
.AddField("Content of the offending item", content);
+ if (!string.IsNullOrEmpty(actionList))
+ result.AddField("Filter Actions", actionList, true);
if (needsAttention && !message.Channel.IsPrivate)
result.AddField("Link to the message", message.JumpLink.ToString());
+#if DEBUG
+ result.WithFooter("Test bot instance");
+#endif
return result;
}
diff --git a/CompatBot/Utils/Extensions/DiscordEmbedBuilderExtensions.cs b/CompatBot/Utils/Extensions/DiscordEmbedBuilderExtensions.cs
index 4a59efff..c8e630c9 100644
--- a/CompatBot/Utils/Extensions/DiscordEmbedBuilderExtensions.cs
+++ b/CompatBot/Utils/Extensions/DiscordEmbedBuilderExtensions.cs
@@ -6,6 +6,7 @@ namespace CompatBot.Utils
{
public static DiscordEmbedBuilder AddFieldEx(this DiscordEmbedBuilder builder, string header, string content, bool underline = false, bool inline = false)
{
+ content = string.IsNullOrEmpty(content) ? "-" : content;
return builder.AddField(underline ? $"__{header}__" : header, content, inline);
}
}
diff --git a/CompatBot/Utils/Extensions/FilterActionExtensions.cs b/CompatBot/Utils/Extensions/FilterActionExtensions.cs
new file mode 100644
index 00000000..2d46e11a
--- /dev/null
+++ b/CompatBot/Utils/Extensions/FilterActionExtensions.cs
@@ -0,0 +1,36 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using CompatBot.Database;
+
+namespace CompatBot.Utils.Extensions
+{
+ internal static class FilterActionExtensions
+ {
+ private static readonly Dictionary ActionFlags = new Dictionary
+ {
+ [FilterAction.RemoveMessage] = 'r',
+ [FilterAction.IssueWarning] = 'w',
+ [FilterAction.SendMessage] = 'm',
+ [FilterAction.ShowExplain] = 'e',
+ };
+
+ public static string ToFlagsString(this FilterAction flags)
+ {
+ var result = Enum.GetValues(typeof(FilterAction))
+ .Cast()
+ .Select(fa => flags.HasFlag(fa) ? ActionFlags[fa] : '-')
+ .ToArray();
+ return new string(result);
+ }
+
+ public static string GetLegend()
+ {
+ var result = new StringBuilder("Actions flag legend:");
+ foreach (FilterAction fa in Enum.GetValues(typeof(FilterAction)))
+ result.Append($"\n`{ActionFlags[fa]}` = {fa}");
+ return result.ToString();
+ }
+ }
+}
diff --git a/CompatBot/Utils/Extensions/InteractivityExtensions.cs b/CompatBot/Utils/Extensions/InteractivityExtensions.cs
index c79afdcc..92387cf5 100644
--- a/CompatBot/Utils/Extensions/InteractivityExtensions.cs
+++ b/CompatBot/Utils/Extensions/InteractivityExtensions.cs
@@ -29,33 +29,46 @@ namespace CompatBot.Utils
if (reactions.Length == 0)
throw new ArgumentException("At least one reaction must be specified", nameof(reactions));
- reactions = reactions.Where(r => r != null).ToArray();
- foreach (var emoji in reactions)
- await message.ReactWithAsync(interactivity.Client, emoji).ConfigureAwait(false);
- var waitTextResponseTask = interactivity.WaitForMessageAsync(m => m.Author == user && m.Channel == message.Channel && !string.IsNullOrEmpty(m.Content), timeout);
- var waitReactionResponse = interactivity.WaitForReactionAsync(arg => reactions.Contains(arg.Emoji), message, user, timeout);
- await Task.WhenAny(
- waitTextResponseTask,
- waitReactionResponse
- ).ConfigureAwait(false);
try
{
- await message.DeleteAllReactionsAsync().ConfigureAwait(false);
+ reactions = reactions.Where(r => r != null).ToArray();
+ foreach (var emoji in reactions)
+ await message.ReactWithAsync(interactivity.Client, emoji).ConfigureAwait(false);
+ var expectedChannel = message.Channel;
+ var waitTextResponseTask = interactivity.WaitForMessageAsync(m => m.Author == user && m.Channel == expectedChannel && !string.IsNullOrEmpty(m.Content), timeout);
+ var waitReactionResponse = interactivity.WaitForReactionAsync(arg => reactions.Contains(arg.Emoji), message, user, timeout);
+ await Task.WhenAny(
+ waitTextResponseTask,
+ waitReactionResponse
+ ).ConfigureAwait(false);
+ try
+ {
+ await message.DeleteAllReactionsAsync().ConfigureAwait(false);
+ }
+ catch
+ {
+ await message.DeleteAsync().ConfigureAwait(false);
+ message = null;
+ }
+ DiscordMessage text = null;
+ MessageReactionAddEventArgs reaction = null;
+ if (waitTextResponseTask.IsCompletedSuccessfully)
+ text = (await waitTextResponseTask).Result;
+ if (waitReactionResponse.IsCompletedSuccessfully)
+ reaction = (await waitReactionResponse).Result;
+ if (text != null)
+ try
+ {
+ await text.DeleteAsync().ConfigureAwait(false);
+ }
+ catch {}
+ return (message, text, reaction);
}
- catch
+ catch (Exception e)
{
- await message.DeleteAsync().ConfigureAwait(false);
- message = null;
+ Config.Log.Warn(e, "Failed to get interactive reaction");
+ return (message, null, null);
}
- DiscordMessage text = null;
- MessageReactionAddEventArgs reaction = null;
- if (waitTextResponseTask.IsCompletedSuccessfully)
- text = (await waitTextResponseTask).Result;
- if (waitReactionResponse.IsCompletedSuccessfully)
- reaction = (await waitReactionResponse).Result;
- if (text != null)
- try { await text.DeleteAsync().ConfigureAwait(false); } catch { }
- return (message, text, reaction);
}
}
}