mirror of
https://github.com/RPCS3/discord-bot.git
synced 2024-12-14 06:18:42 +00:00
Merge pull request #353 from 13xforever/feature/content_filter_v2
Content filter v2.0
This commit is contained in:
commit
74370e61cd
@ -15,7 +15,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.7" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
|
||||
<PackageReference Include="NLog" Version="4.6.5" />
|
||||
<PackageReference Include="NLog" Version="4.6.6" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -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<int>();
|
||||
var removedFilters = new List<string>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<byte[]> Poster = new Lazy<byte[]>(() =>
|
||||
{
|
||||
using (var client = HttpClientFactory.Create())
|
||||
return client.GetByteArrayAsync(Source).ConfigureAwait(true).GetAwaiter().GetResult();
|
||||
});
|
||||
|
||||
public override async Task<bool> 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;
|
||||
}
|
||||
}
|
||||
|
@ -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)]
|
||||
|
@ -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();
|
||||
|
@ -22,9 +22,9 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DSharpPlus" Version="4.0.0-nightly-00618" />
|
||||
<PackageReference Include="DSharpPlus.CommandsNext" Version="4.0.0-nightly-00618" />
|
||||
<PackageReference Include="DSharpPlus.Interactivity" Version="4.0.0-nightly-00618" />
|
||||
<PackageReference Include="DSharpPlus" Version="4.0.0-nightly-00621" />
|
||||
<PackageReference Include="DSharpPlus.CommandsNext" Version="4.0.0-nightly-00621" />
|
||||
<PackageReference Include="DSharpPlus.Interactivity" Version="4.0.0-nightly-00621" />
|
||||
<PackageReference Include="DuoVia.FuzzyStrings" Version="2.0.1" />
|
||||
<PackageReference Include="Google.Apis.Drive.v3" Version="1.40.2.1649" />
|
||||
<PackageReference Include="ksemenenko.ColorThief" Version="1.1.1.4" />
|
||||
@ -37,9 +37,10 @@
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="2.2.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="2.2.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="2.2.0" />
|
||||
<PackageReference Include="Nerdbank.Streams" Version="2.2.38" />
|
||||
<PackageReference Include="Nerdbank.Streams" Version="2.2.42" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
|
||||
<PackageReference Include="NLog" Version="4.6.5" />
|
||||
<PackageReference Include="NLog" Version="4.6.6" />
|
||||
<PackageReference Include="NLog.Extensions.Logging" Version="1.5.2" />
|
||||
<PackageReference Include="NReco.Text.AhoCorasickDoubleArrayTrie" Version="1.0.2" />
|
||||
<PackageReference Include="SharpCompress" Version="0.23.0" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="4.5.1" />
|
||||
|
@ -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<string, string> inMemorySettings = new ConcurrentDictionary<string, string>();
|
||||
|
||||
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)
|
||||
{
|
||||
|
@ -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<BotState>().HasIndex(m => m.Key).IsUnique().HasName("bot_state_key");
|
||||
modelBuilder.Entity<Moderator>().HasIndex(m => m.DiscordId).IsUnique().HasName("moderator_discord_id");
|
||||
modelBuilder.Entity<Piracystring>().HasIndex(ps => ps.String).IsUnique().HasName("piracystring_string");
|
||||
modelBuilder.Entity<Piracystring>().Property(ps => ps.Context).HasDefaultValue(FilterContext.Chat | FilterContext.Log);
|
||||
modelBuilder.Entity<Piracystring>().Property(ps => ps.Actions).HasDefaultValue(FilterAction.RemoveMessage | FilterAction.IssueWarning | FilterAction.SendMessage);
|
||||
modelBuilder.Entity<Warning>().HasIndex(w => w.DiscordId).HasName("warning_discord_id");
|
||||
modelBuilder.Entity<Explanation>().HasIndex(e => e.Keyword).IsUnique().HasName("explanation_keyword");
|
||||
modelBuilder.Entity<DisabledCommand>().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
|
||||
|
290
CompatBot/Database/Migrations/BotDb/20190723151803_ContentFilter2.0.Designer.cs
generated
Normal file
290
CompatBot/Database/Migrations/BotDb/20190723151803_ContentFilter2.0.Designer.cs
generated
Normal file
@ -0,0 +1,290 @@
|
||||
// <auto-generated />
|
||||
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<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Key")
|
||||
.HasColumnName("key");
|
||||
|
||||
b.Property<string>("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<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("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<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<long>("End")
|
||||
.HasColumnName("end");
|
||||
|
||||
b.Property<string>("EventName")
|
||||
.HasColumnName("event_name");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<long>("Start")
|
||||
.HasColumnName("start");
|
||||
|
||||
b.Property<int>("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<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<byte[]>("Attachment")
|
||||
.HasColumnName("attachment")
|
||||
.HasMaxLength(7340032);
|
||||
|
||||
b.Property<string>("AttachmentFilename")
|
||||
.HasColumnName("attachment_filename");
|
||||
|
||||
b.Property<string>("Keyword")
|
||||
.IsRequired()
|
||||
.HasColumnName("keyword");
|
||||
|
||||
b.Property<string>("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<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<ulong>("DiscordId")
|
||||
.HasColumnName("discord_id");
|
||||
|
||||
b.Property<bool>("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<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<int>("Actions")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnName("actions")
|
||||
.HasDefaultValue(11);
|
||||
|
||||
b.Property<byte>("Context")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnName("context")
|
||||
.HasDefaultValue((byte)3);
|
||||
|
||||
b.Property<string>("CustomMessage")
|
||||
.HasColumnName("custom_message");
|
||||
|
||||
b.Property<bool>("Disabled")
|
||||
.HasColumnName("disabled");
|
||||
|
||||
b.Property<string>("ExplainTerm")
|
||||
.HasColumnName("explain_term");
|
||||
|
||||
b.Property<string>("String")
|
||||
.IsRequired()
|
||||
.HasColumnName("string")
|
||||
.HasColumnType("varchar(255)");
|
||||
|
||||
b.Property<string>("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<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Category")
|
||||
.IsRequired()
|
||||
.HasColumnName("category");
|
||||
|
||||
b.Property<long>("ExpirationTimestamp")
|
||||
.HasColumnName("expiration_timestamp");
|
||||
|
||||
b.Property<string>("Key")
|
||||
.IsRequired()
|
||||
.HasColumnName("key");
|
||||
|
||||
b.Property<int>("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<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<ulong>("DiscordId")
|
||||
.HasColumnName("discord_id");
|
||||
|
||||
b.Property<string>("FullReason")
|
||||
.IsRequired()
|
||||
.HasColumnName("full_reason");
|
||||
|
||||
b.Property<ulong>("IssuerId")
|
||||
.HasColumnName("issuer_id");
|
||||
|
||||
b.Property<string>("Reason")
|
||||
.IsRequired()
|
||||
.HasColumnName("reason");
|
||||
|
||||
b.Property<bool>("Retracted")
|
||||
.HasColumnName("retracted");
|
||||
|
||||
b.Property<ulong?>("RetractedBy")
|
||||
.HasColumnName("retracted_by");
|
||||
|
||||
b.Property<string>("RetractionReason")
|
||||
.HasColumnName("retraction_reason");
|
||||
|
||||
b.Property<long?>("RetractionTimestamp")
|
||||
.HasColumnName("retraction_timestamp");
|
||||
|
||||
b.Property<long?>("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<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<ulong>("GuildId")
|
||||
.HasColumnName("guild_id");
|
||||
|
||||
b.Property<string>("InviteCode")
|
||||
.HasColumnName("invite_code");
|
||||
|
||||
b.Property<string>("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
|
||||
}
|
||||
}
|
||||
}
|
@ -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<int>(
|
||||
name: "actions",
|
||||
table: "piracystring",
|
||||
nullable: false,
|
||||
defaultValue: 11);
|
||||
|
||||
migrationBuilder.AddColumn<byte>(
|
||||
name: "context",
|
||||
table: "piracystring",
|
||||
nullable: false,
|
||||
defaultValue: (byte)3);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "custom_message",
|
||||
table: "piracystring",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "disabled",
|
||||
table: "piracystring",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "explain_term",
|
||||
table: "piracystring",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
@ -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<int>("Actions")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnName("actions")
|
||||
.HasDefaultValue(11);
|
||||
|
||||
b.Property<byte>("Context")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnName("context")
|
||||
.HasDefaultValue((byte)3);
|
||||
|
||||
b.Property<string>("CustomMessage")
|
||||
.HasColumnName("custom_message");
|
||||
|
||||
b.Property<bool>("Disabled")
|
||||
.HasColumnName("disabled");
|
||||
|
||||
b.Property<string>("ExplainTerm")
|
||||
.HasColumnName("explain_term");
|
||||
|
||||
b.Property<string>("String")
|
||||
.IsRequired()
|
||||
.HasColumnName("string")
|
||||
.HasColumnType("varchar(255)");
|
||||
|
||||
b.Property<string>("ValidatingRegex")
|
||||
.HasColumnName("validating_regex");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("id");
|
||||
|
||||
|
148
CompatBot/Database/Providers/ContentFilter.cs
Normal file
148
CompatBot/Database/Providers/ContentFilter.cs
Normal file
@ -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<FilterContext, AhoCorasickDoubleArrayTrie<Piracystring>> filters = new Dictionary<FilterContext, AhoCorasickDoubleArrayTrie<Piracystring>>();
|
||||
|
||||
static ContentFilter()
|
||||
{
|
||||
RebuildMatcher();
|
||||
}
|
||||
|
||||
public static Task<Piracystring> 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<FilterContext, AhoCorasickDoubleArrayTrie<Piracystring>>();
|
||||
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<Piracystring>(f.ToDictionary(s => s.String, s => s), true);
|
||||
}
|
||||
filters = newFilters;
|
||||
}
|
||||
|
||||
|
||||
public static async Task<bool> 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<FilterAction>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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<string> PiracyStrings;
|
||||
private static AhoCorasickDoubleArrayTrie<string> matcher;
|
||||
|
||||
static PiracyStringProvider()
|
||||
{
|
||||
PiracyStrings = db.Piracystring.Select(ps => ps.String).ToList();
|
||||
RebuildMatcher();
|
||||
}
|
||||
|
||||
public static async Task<bool> 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<bool> 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<string> GetTriggerAsync(int id)
|
||||
{
|
||||
var dbItem = await db.Piracystring.FirstOrDefaultAsync(ps => ps.Id == id).ConfigureAwait(false);
|
||||
return dbItem?.String;
|
||||
}
|
||||
|
||||
public static Task<string> 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<string>(PiracyStrings.ToDictionary(s => s, s => s), true);
|
||||
}
|
||||
}
|
||||
}
|
@ -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}\"");
|
||||
}
|
||||
|
||||
|
@ -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<bool> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -103,10 +103,10 @@ namespace CompatBot.Utils
|
||||
return messages.TakeWhile(m => m.CreationTimestamp > afterTime).ToList().AsReadOnly();
|
||||
}
|
||||
|
||||
public static async Task<DiscordMessage> ReportAsync(this DiscordClient client, string infraction, DiscordMessage message, string trigger, string context, ReportSeverity severity)
|
||||
public static async Task<DiscordMessage> 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;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
36
CompatBot/Utils/Extensions/FilterActionExtensions.cs
Normal file
36
CompatBot/Utils/Extensions/FilterActionExtensions.cs
Normal file
@ -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<FilterAction, char> ActionFlags = new Dictionary<FilterAction, char>
|
||||
{
|
||||
[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<FilterAction>()
|
||||
.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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user