From de9d8851d3d4dc92d0489fe7a5e2176877c5c997 Mon Sep 17 00:00:00 2001 From: BuildTools Date: Sun, 21 Mar 2021 21:46:40 +0000 Subject: [PATCH 01/11] add stuff to prevent seizures and things --- Cliptok.csproj | 1 + Modules/ConfigJson.cs | 30 ++++ Modules/GenericModCmds.cs | 18 ++ Modules/SeizureDetection.cs | 339 ++++++++++++++++++++++++++++++++++++ Modules/Warnings.cs | 1 + Program.cs | 44 +++++ 6 files changed, 433 insertions(+) create mode 100644 Modules/SeizureDetection.cs diff --git a/Cliptok.csproj b/Cliptok.csproj index f8d590e6..f3080ffb 100644 --- a/Cliptok.csproj +++ b/Cliptok.csproj @@ -13,6 +13,7 @@ + diff --git a/Modules/ConfigJson.cs b/Modules/ConfigJson.cs index 580cbc49..4c977ab1 100644 --- a/Modules/ConfigJson.cs +++ b/Modules/ConfigJson.cs @@ -118,6 +118,36 @@ public struct ConfigJson [JsonProperty("restrictedHeartosoftPhrases")] public List RestrictedHeartosoftPhrases { get; private set; } + [JsonProperty("seizureDetection")] + public SeizureDetectionConfig SeizureDetection { get; private set; } + + } + + public class SeizureDetectionConfig + { + [JsonProperty("comparisonAccuracy")] + public int ComparisonAccuracy { get; private set; } + + [JsonProperty("maxDifferingPixels")] + public int MaxDifferingPixels { get; private set; } + + [JsonProperty("maxComparableFrames")] + public int MaxComparableFrames { get; private set; } + + [JsonProperty("harmlessAverageFrameDifference")] + public int HarmlessAverageFrameDifference { get; private set; } + + [JsonProperty("safeAverageFrameDifference")] + public int SafeAverageFrameDifference { get; private set; } + + [JsonProperty("penaltyAverageFrameDifference")] + public int PenaltyAverageFrameDifference { get; private set; } + + [JsonProperty("maxAverageFrameDifference")] + public int MaxAverageFrameDifference { get; private set; } + + [JsonProperty("unsafeGifValues")] + public List UnsafeGifValues { get; private set; } } public class WordListJson diff --git a/Modules/GenericModCmds.cs b/Modules/GenericModCmds.cs index d9eb8a00..ac092bda 100644 --- a/Modules/GenericModCmds.cs +++ b/Modules/GenericModCmds.cs @@ -687,6 +687,24 @@ public static async Task CheckRemindersAsync(bool includeRemutes = false) [HomeServer, RequireHomeserverPerm(ServerPermLevel.Mod)] class DebugCmds : BaseCommandModule { + [Command("gif")] + [Description("Debug GIF properties.")] + public async Task GifDebug(CommandContext ctx, string gifToCheck) + { + SeizureDetection.ImageInfo Gif = SeizureDetection.GetGifProperties(gifToCheck); + string strOut = "**GIF information**\n"; + strOut += $"----------\nFrame count: **{Gif.FrameCount}**\n"; + strOut += $"Unique frame count: **{Gif.UniqueFrameCount}**\n"; + strOut += $"Average frame difference: **{Math.Round(Gif.AverageFrameDifference, 2)}**\n"; + strOut += $"Average frame contrast: **{Math.Round(Gif.AverageContrast, 2)}**\n"; + strOut += $"Length: **{Gif.Length}ms**\n"; + strOut += $"Frame duration: **{Math.Round(Gif.Duration, 2)}ms**\n"; + strOut += $"Framerate: **{Math.Round(Gif.FrameRate, 2)}fps**\n"; + strOut += $"Seizure-inducing: **{Gif.IsSeizureInducing}**\n"; + strOut += "----------"; + await ctx.RespondAsync(strOut); + } + [Command("mutes")] [Description("Debug the list of mutes.")] public async Task MuteDebug(CommandContext ctx) diff --git a/Modules/SeizureDetection.cs b/Modules/SeizureDetection.cs new file mode 100644 index 00000000..c80d502d --- /dev/null +++ b/Modules/SeizureDetection.cs @@ -0,0 +1,339 @@ +using DSharpPlus.CommandsNext; +using DSharpPlus.CommandsNext.Attributes; +using DSharpPlus.Entities; +using Newtonsoft.Json; +using StackExchange.Redis; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Drawing; +using System.Drawing.Imaging; +using System.Drawing.Drawing2D; +using System.IO; + +namespace Cliptok.Modules +{ + + + public class SeizureDetection + { + + static int comparisonAccuracy = Program.cfgjson.SeizureDetection.ComparisonAccuracy; // Only compares every [value] pixels + static int maxDifferingPixels = Program.cfgjson.SeizureDetection.MaxDifferingPixels; // Ignore [value] differing pixels per cycle + static int maxComparableFrames = Program.cfgjson.SeizureDetection.MaxComparableFrames; // If GIF has more than [value] frames, don't compare it frame-by-frame + static int harmlessAverageFrameDifference = Program.cfgjson.SeizureDetection.HarmlessAverageFrameDifference; // Ignore GIFs where the average difference between all their frames is less than [value]% + static int safeAverageFrameDifference = Program.cfgjson.SeizureDetection.SafeAverageFrameDifference; // Treat GIFs less harshly if the average difference between all their frames is less than [value]% + static int penaltyAverageFrameDifference = Program.cfgjson.SeizureDetection.PenaltyAverageFrameDifference; // Apply a penalty for GIFs where the average difference between all their frames is greater than [value]% + static int maxAverageFrameDifference = Program.cfgjson.SeizureDetection.MaxAverageFrameDifference; // Flag GIFs where the average difference between all their frames is greater than [value]% + static List unsafeGifValues = Program.cfgjson.SeizureDetection.UnsafeGifValues; // If GIF has only [key] frames, then the average length of each frame has to be at least [value] ms long + + public static ImageInfo GetGifProperties(string input) + { + ImageInfo Gif = GetImageInfo(input); + + // The actual check for whether or not the GIF might trigger a seizure + try + { + if (Gif.AverageFrameDifference < harmlessAverageFrameDifference) // Checks to see if its too low to possibly be harmful + { + Gif.IsSeizureInducing = false; + } + else if (Gif.AverageFrameDifference > maxAverageFrameDifference && Gif.FrameCount <= (2 * unsafeGifValues.Count)) // Checks to see if its high enough to be potentially risky + { + Gif.IsSeizureInducing = true; + } + else + { + if (safeAverageFrameDifference > Gif.AverageFrameDifference && unsafeGifValues[Gif.UniqueFrameCount] > (Gif.Duration + (Gif.Duration / 2))) + { + Gif.IsSeizureInducing = false; + } + else if (unsafeGifValues[Gif.UniqueFrameCount] > Gif.Duration) + { + Gif.IsSeizureInducing = true; + } + else if (penaltyAverageFrameDifference < Gif.AverageFrameDifference && unsafeGifValues[Gif.UniqueFrameCount] > (double)Gif.Duration * 1.5) + { + Gif.IsSeizureInducing = true; + } + else + { + Gif.IsSeizureInducing = false; + } + } + } + catch (ArgumentOutOfRangeException) + { + Gif.IsSeizureInducing = false; + } + + Console.WriteLine($"----------\nFrame count: {Gif.FrameCount}"); + Console.WriteLine($"Unique frame count: {Gif.UniqueFrameCount}"); + Console.WriteLine($"Average frame difference: {Math.Round(Gif.AverageFrameDifference, 2)}"); + Console.WriteLine($"Average frame contrast: {Math.Round(Gif.AverageContrast, 2)}"); + Console.WriteLine($"Length: {Gif.Length}ms"); + Console.WriteLine($"Frame duration: {Math.Round(Gif.Duration, 2)}ms"); + Console.WriteLine($"Framerate: {Math.Round(Gif.FrameRate, 2)}fps"); + Console.Write($"Seizure-inducing: "); + if (Gif.UniqueFrameCount != Gif.FrameCount && Gif.IsSeizureInducing) + Console.ForegroundColor = ConsoleColor.DarkRed; + else if (Gif.UniqueFrameCount != Gif.FrameCount) + Console.ForegroundColor = ConsoleColor.DarkGreen; + else if (Gif.IsSeizureInducing) + Console.ForegroundColor = ConsoleColor.Red; + else + Console.ForegroundColor = ConsoleColor.Green; + Console.Write($"{Gif.IsSeizureInducing}"); + Console.ResetColor(); + Console.Write(".\n----------\n\n"); + return Gif; + } + + public static ImageInfo GetImageInfo(string url) + { + ImageInfo info = new ImageInfo(); + + using (Image image = Image.FromStream(GetStreamFromUrl(url))) + { + info.Height = image.Height; + info.Width = image.Width; + + if (image.RawFormat.Equals(ImageFormat.Gif)) + { + if (ImageAnimator.CanAnimate(image)) + { + FrameDimension frameDimension = new FrameDimension(image.FrameDimensionsList[0]); + + int frameCount = image.GetFrameCount(frameDimension); + decimal delay = 0; + decimal this_delay = 0; + int index = 0; + decimal averageFrameDifference = 0; + decimal averageContrast = 0; + List ExtractedFrames = new List(); + List> isUniqueList = new List>(frameCount); + for (int i = 0; i < frameCount; i++) + { + isUniqueList.Add(new List()); // -1 means the image hasn't yet been checked at all + } + + int uniqueFrameCount = 0; + + if (frameCount < maxComparableFrames) + { + for (int e = 0; e < frameCount; e++) + { + image.SelectActiveFrame(frameDimension, e); + ExtractedFrames.Add(new Bitmap(image)); + } + } + + //\\ ================ Beginning of compare loop ================ //\\ + for (int f = 0; f < frameCount; f++) + { + this_delay = BitConverter.ToInt32(image.GetPropertyItem(20736).Value, index) * 10; + delay += this_delay; + index += 4; + + if (frameCount < maxComparableFrames) + { + int p = 0; + foreach (Bitmap bmp1 in ExtractedFrames) + { + bool compareFullFrame = false; + if (p == f + 1) + { + compareFullFrame = true; + } + if (p != f && isUniqueList[f].Count == 0) + { + var comparsionData = Compare(bmp1, ExtractedFrames[f], compareFullFrame); + if (comparsionData.Item1) + { + isUniqueList[p].Add(f); + + } + if (comparsionData.Item2 != -1) + { + averageFrameDifference += comparsionData.Item2; + averageContrast += comparsionData.Item3; + } + } + else if (compareFullFrame == true && p != f) + { + var comparsionData = Compare(bmp1, ExtractedFrames[f], compareFullFrame); + if (comparsionData.Item2 != -1) + { + averageFrameDifference += comparsionData.Item2; + averageContrast += comparsionData.Item3; + } + } + p++; + } + } + } + //\\ =================== End of compare loop =================== //\\ + + // Get number of unique frames + foreach (List b in isUniqueList) + { + if (b.Count == 0) + { + uniqueFrameCount++; + } + } + // If it's zero then every frame is unique uwu + if (uniqueFrameCount == 0) + { + uniqueFrameCount = frameCount; + } + // Dispose of all those nasty bitmaps + foreach (Bitmap bmp in ExtractedFrames) + { + bmp.Dispose(); + } + + + // Adds info to struct + try + { + info.Duration = delay / frameCount; + info.FrameCount = frameCount; + info.UniqueFrameCount = uniqueFrameCount; + info.AverageFrameDifference = averageFrameDifference / (frameCount - 1); + info.AverageContrast = averageContrast / (frameCount - 1); + info.IsAnimated = true; + info.IsLooped = BitConverter.ToInt16(image.GetPropertyItem(20737).Value, 0) != 1; + info.Length = delay; + info.FrameRate = frameCount / (info.Length / 1000); + } + // If anything is accidentally zero because GIFs suck then just assume its safe + // Mods can deal with the chaos that ensues + catch (DivideByZeroException) + { + info.Duration = 1000; + info.FrameCount = frameCount; + info.UniqueFrameCount = uniqueFrameCount; + info.AverageFrameDifference = 0; + info.AverageContrast = 0; + info.IsAnimated = true; + info.IsLooped = BitConverter.ToInt16(image.GetPropertyItem(20737).Value, 0) != 1; + info.Length = 1000 * frameCount; + info.FrameRate = -1; + } + } + } + } + return info; // Finally! We're free of this mess! + } + + // The almighty comparison mechanism. Takes in two images and checks to see if they're the same. Optionally it will compare the whole image. + // Returns a bool stating whether or not the images are identical, and a decimal that contains a percentage of how similar the two images are (if compareFullFrame is true) + public static (bool, decimal, decimal) Compare(Bitmap bmp1, Bitmap bmp2, bool compareFullFrame) + { + int pixelCount = bmp1.Width * bmp1.Height; // Total pixels in the frame + int differingPixels = 0; // Keeps track of how many pixels differ between the two images + decimal pixelDifference = 0; // Keeps a percentage of how many differing pixels there are + decimal pixelContrast = 0; + + for (int x = 0; x < bmp1.Width; x += comparisonAccuracy) + { + for (int y = 0; y < bmp1.Height; y += comparisonAccuracy) + { + if (bmp1.GetPixel(x, y) != bmp2.GetPixel(x, y)) + { + differingPixels++; + //Console.WriteLine($"[NOTE] Pixel {x},{y} in bitmap 1 did not match pixel {x},{y} in bitmap 2"); + if (differingPixels > maxDifferingPixels) + { + if (!compareFullFrame) + { + return (false, pixelDifference, pixelContrast); + } + } + } + } + } + if (compareFullFrame) + { + pixelDifference = (Convert.ToDecimal(differingPixels) / Convert.ToDecimal(pixelCount)) * Convert.ToDecimal(100) * (comparisonAccuracy * comparisonAccuracy); + pixelContrast = (decimal)GetContrast(GetAverageColour(bmp1), GetAverageColour(bmp2)); + } + if (differingPixels > maxDifferingPixels && compareFullFrame) + { + return (false, pixelDifference, pixelContrast); + } + else + { + return (true, -1, -1); + } + } + + // Gets the average colour of an image + private static Color GetAverageColour(Bitmap bmp) + { + Bitmap bmp1px = new Bitmap(1, 1); + using (Graphics g = Graphics.FromImage(bmp1px)) + { + g.InterpolationMode = InterpolationMode.HighQualityBilinear; + g.DrawImage(bmp, new Rectangle(0, 0, 1, 1)); + } + return bmp1px.GetPixel(0, 0); + } + + private static double GetLuminance(Color c) + { + byte[] colourArray = { c.R, c.G, c.B }; + double[] luminanceArray = new double[3]; + for (int i = 0; i < 3; i++) + { + luminanceArray[i] = colourArray[i] / 255.0; + luminanceArray[i] = luminanceArray[i] <= 0.03928 + ? luminanceArray[i] / 12.92 + : Math.Pow((luminanceArray[i] + 0.055) / 1.055, 2.4); + } + return luminanceArray[0] * 0.2126 + luminanceArray[1] * 0.7152 + luminanceArray[2] * 0.0722; + } + + private static double GetContrast(Color c1, Color c2) + { + var lum1 = GetLuminance(c1); + var lum2 = GetLuminance(c2); + var brightest = Math.Max(lum1, lum2); + var darkest = Math.Min(lum1, lum2); + return (brightest + 0.05) + / (darkest + 0.05); + } + + // Nothing fancy, just gets the GIF. + private static Stream GetStreamFromUrl(string url) + { + byte[] imageData = null; + + using (var wc = new System.Net.WebClient()) + imageData = wc.DownloadData(url); + + Console.WriteLine("Downloaded GIF"); + return new MemoryStream(imageData); + } + + // Definition for a loaded GIF + public struct ImageInfo + { + public bool IsSeizureInducing; // Whether or not the GIF likely to be painful + public int Width; // Width of GIF + public int Height; // Height of GIF + public int FrameCount; // Number of frames in the GIF + public int UniqueFrameCount; // Number of unique frames in the GIF + public decimal AverageFrameDifference; // Average difference between all frames + public decimal AverageContrast; // Average contrast between all frames + public bool IsAnimated; // Whether or not the GIF is animated + public bool IsLooped; // Whether or not the GIF loops + public decimal Duration; // Average duration of one frame in milliseconds + public decimal Length; // Length of entire GIF in milliseconds + public decimal FrameRate; // Average framerate of entire GIF in FPS + } + } +} diff --git a/Modules/Warnings.cs b/Modules/Warnings.cs index 41d829e2..c1843f7e 100644 --- a/Modules/Warnings.cs +++ b/Modules/Warnings.cs @@ -34,6 +34,7 @@ public enum ServerPermLevel [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = false)] public class RequireHomeserverPermAttribute : CheckBaseAttribute { + public ServerPermLevel TargetLvl { get; set; } public RequireHomeserverPermAttribute(ServerPermLevel targetlvl) diff --git a/Program.cs b/Program.cs index 61eb7492..bd0e0e5c 100644 --- a/Program.cs +++ b/Program.cs @@ -28,6 +28,8 @@ class Program : BaseCommandModule public static List processedMessages = new List(); public static Dictionary wordLists = new Dictionary(); readonly static Regex emoji_rx = new Regex("((\u203c|\u2049|\u2139|[\u2194-\u2199]|[\u21a9-\u21aa]|[\u231a-\u231b]|\u23cf|[\u23e9-\u23f3]|[\u23f8-\u23fa]|\u24c2|[\u25aa–\u25ab]|\u25b6|\u25c0|[\u25fb–\u25fe]|[\u2600–\u2604]|\u260E|\u2611|[\u2614–\u2615]|\u2618|\u261D|\u2620|[\u2622–\u2623]|\u2626|\u262A|[\u262E–\u262F]|[\u2638–\u263A]|\u2640|\u2642|[\u2648–\u2653]|[\u265F–\u2660]|\u2663|[\u2665–\u2666]|\u2668|\u267B|[\u267E–\u267F]|[\u2692–\u2697]|\u2699|[\u269B–\u269C]|[\u26A0–\u26A1]|\u26A7|[\u26AA–\u26AB]|[\u26B0–\u26B1]|[\u26BD–\u26BE]|[\u26C4–\u26C5]|\u26C8|[\u26CE–\u26CF]|\u26D1|[\u26D3–\u26D4]|[\u26E9–\u26EA]|[\u26F0–\u26F5]|[\u26F7–\u26FA]|\u26FD|\u2702|\u2705|[\u2708–\u270D]|\u270F|\u2712|\u2714|\u2716|\u271D|\u2721|\u2728|[\u2733–\u2734]|\u2744|\u2747|\u274C|\u274E|[\u2753–\u2755]|\u2757|[\u2763–\u2764]|[\u2795–\u2797]|\u27A1|\u27B0|\u27BF|[\u2934–\u2935]|[\u2B05–\u2B07]|[\u2B1B–\u2B1C]|\u2B50|\u2B55|\u3030|\u303D|\u3297|\u3299|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff]))|()"); + readonly static Regex animoji_rx = new Regex("()"); + readonly static Regex id_rx = new Regex("([0-9]{1,32}>)"); static bool CheckForNaughtyWords(string input, WordListJson naughtyWordList) { @@ -405,6 +407,48 @@ async Task MessageCreated(DiscordClient client, MessageCreateEventArgs e) return; } } + + // Seizure emoji + var animatches = animoji_rx.Matches(e.Message.Content); + if (animatches.Count > 0) + { + foreach (Match dirtyid in animatches) + { + string id = id_rx.Matches(dirtyid.ToString())[0].ToString().Replace(">", ""); + string url = "https://cdn.discordapp.com/emojis/" + id + ".gif"; + + if (SeizureDetection.GetGifProperties(url).IsSeizureInducing) + { + try + { + e.Message.DeleteAsync(); + var embed = new DiscordEmbedBuilder() + .WithDescription(e.Message.Content) + .WithColor(new DiscordColor(0xf03916)) + .WithTimestamp(e.Message.Timestamp) + .WithFooter( + $"User ID: {e.Author.Id}", + null + ) + .WithAuthor( + $"{e.Author.Username}#{e.Author.Discriminator} in #{e.Channel.Name}", + null, + e.Author.AvatarUrl + ); + logChannel.SendMessageAsync($"{cfgjson.Emoji.Denied} Deleted infringing message by {e.Author.Mention} in {e.Channel.Mention}:", embed); + + } + catch + { + // still warn anyway + } + string reason = "sent a seizure-inducing emoji"; + DiscordMessage msg = await e.Channel.SendMessageAsync($"{Program.cfgjson.Emoji.Denied} {e.Message.Author.Mention} was automatically warned: **{reason.Replace("`", "\\`").Replace("*", "\\*")}**"); + await Warnings.GiveWarningAsync(e.Message.Author, discord.CurrentUser, reason, contextLink: Warnings.MessageLink(msg), e.Channel); + return; + } + } + } } async Task GuildMemberAdded(DiscordClient client, GuildMemberAddEventArgs e) From 4f6e7e42dbc2e716e94d1efb5cadff027812d2c6 Mon Sep 17 00:00:00 2001 From: BuildTools Date: Mon, 22 Mar 2021 19:39:10 +0000 Subject: [PATCH 02/11] add thing for noisymoji + slightly better console log --- Modules/SeizureDetection.cs | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/Modules/SeizureDetection.cs b/Modules/SeizureDetection.cs index c80d502d..2fc045b8 100644 --- a/Modules/SeizureDetection.cs +++ b/Modules/SeizureDetection.cs @@ -36,40 +36,49 @@ public static ImageInfo GetGifProperties(string input) // The actual check for whether or not the GIF might trigger a seizure try { - if (Gif.AverageFrameDifference < harmlessAverageFrameDifference) // Checks to see if its too low to possibly be harmful + if (Gif.AverageFrameDifference > maxAverageFrameDifference && Gif.AverageContrast < 2) { Gif.IsSeizureInducing = false; } - else if (Gif.AverageFrameDifference > maxAverageFrameDifference && Gif.FrameCount <= (2 * unsafeGifValues.Count)) // Checks to see if its high enough to be potentially risky - { - Gif.IsSeizureInducing = true; - } else { - if (safeAverageFrameDifference > Gif.AverageFrameDifference && unsafeGifValues[Gif.UniqueFrameCount] > (Gif.Duration + (Gif.Duration / 2))) + if (Gif.AverageFrameDifference < harmlessAverageFrameDifference) // Checks to see if its too low to possibly be harmful { Gif.IsSeizureInducing = false; } - else if (unsafeGifValues[Gif.UniqueFrameCount] > Gif.Duration) - { - Gif.IsSeizureInducing = true; - } - else if (penaltyAverageFrameDifference < Gif.AverageFrameDifference && unsafeGifValues[Gif.UniqueFrameCount] > (double)Gif.Duration * 1.5) + else if (Gif.AverageFrameDifference > maxAverageFrameDifference && Gif.FrameCount <= (2 * unsafeGifValues.Count)) // Checks to see if its high enough to be potentially risky { Gif.IsSeizureInducing = true; } else { - Gif.IsSeizureInducing = false; + if (safeAverageFrameDifference > Gif.AverageFrameDifference && unsafeGifValues[Gif.UniqueFrameCount] > (Gif.Duration + (Gif.Duration / 2))) + { + Gif.IsSeizureInducing = false; + } + else if (unsafeGifValues[Gif.UniqueFrameCount] > Gif.Duration) + { + Gif.IsSeizureInducing = true; + } + else if (penaltyAverageFrameDifference < Gif.AverageFrameDifference && unsafeGifValues[Gif.UniqueFrameCount] > (double)Gif.Duration * 1.5) + { + Gif.IsSeizureInducing = true; + } + else + { + Gif.IsSeizureInducing = false; + } } } + } catch (ArgumentOutOfRangeException) { Gif.IsSeizureInducing = false; } - Console.WriteLine($"----------\nFrame count: {Gif.FrameCount}"); + Console.WriteLine($"----------\nGIF: {input}"); + Console.WriteLine($"Frame count: {Gif.FrameCount}"); Console.WriteLine($"Unique frame count: {Gif.UniqueFrameCount}"); Console.WriteLine($"Average frame difference: {Math.Round(Gif.AverageFrameDifference, 2)}"); Console.WriteLine($"Average frame contrast: {Math.Round(Gif.AverageContrast, 2)}"); From 219e711f89e788b0085ead19350e43d01bf37ce2 Mon Sep 17 00:00:00 2001 From: BuildTools Date: Mon, 22 Mar 2021 20:38:10 +0000 Subject: [PATCH 03/11] fixeeee thehfenbgre thing. --- Modules/SeizureDetection.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Modules/SeizureDetection.cs b/Modules/SeizureDetection.cs index 2fc045b8..b3687871 100644 --- a/Modules/SeizureDetection.cs +++ b/Modules/SeizureDetection.cs @@ -56,9 +56,12 @@ public static ImageInfo GetGifProperties(string input) { Gif.IsSeizureInducing = false; } - else if (unsafeGifValues[Gif.UniqueFrameCount] > Gif.Duration) + else if (Gif.UniqueFrameCount <= unsafeGifValues.Count) { - Gif.IsSeizureInducing = true; + if (unsafeGifValues[Gif.UniqueFrameCount] > Gif.Duration) + { + Gif.IsSeizureInducing = true; + } } else if (penaltyAverageFrameDifference < Gif.AverageFrameDifference && unsafeGifValues[Gif.UniqueFrameCount] > (double)Gif.Duration * 1.5) { From 8c505f0104b8c00f120186602cf56c8202af14db Mon Sep 17 00:00:00 2001 From: BuildTools Date: Mon, 22 Mar 2021 20:44:05 +0000 Subject: [PATCH 04/11] iohjbgFDXvj --- Modules/SeizureDetection.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Modules/SeizureDetection.cs b/Modules/SeizureDetection.cs index b3687871..b0892d5f 100644 --- a/Modules/SeizureDetection.cs +++ b/Modules/SeizureDetection.cs @@ -62,10 +62,10 @@ public static ImageInfo GetGifProperties(string input) { Gif.IsSeizureInducing = true; } - } - else if (penaltyAverageFrameDifference < Gif.AverageFrameDifference && unsafeGifValues[Gif.UniqueFrameCount] > (double)Gif.Duration * 1.5) - { - Gif.IsSeizureInducing = true; + else if (penaltyAverageFrameDifference < Gif.AverageFrameDifference && unsafeGifValues[Gif.UniqueFrameCount] > (double)Gif.Duration * 1.5) + { + Gif.IsSeizureInducing = true; + } } else { From 0b25360e1037a925b4409f7cf3b6647b75b3ad43 Mon Sep 17 00:00:00 2001 From: FloatingMilkshake Date: Tue, 7 Jul 2026 21:28:59 -0400 Subject: [PATCH 05/11] Add Linux compatibility, accept emoji in debug command Changes dependencies from System.Drawing.Common to SkiaSharp as System.Drawing.Common is now Windows-only --- Cliptok.csproj | 3 +- Commands/DebugCmds.cs | 13 +++++- Events/MessageEvent.cs | 2 +- GlobalUsings.cs | 4 +- Helpers/SeizureDetectionHelpers.cs | 73 ++++++++++++++---------------- 5 files changed, 51 insertions(+), 44 deletions(-) diff --git a/Cliptok.csproj b/Cliptok.csproj index 73e80fe2..2e4b7af4 100644 --- a/Cliptok.csproj +++ b/Cliptok.csproj @@ -36,8 +36,9 @@ + + - diff --git a/Commands/DebugCmds.cs b/Commands/DebugCmds.cs index 7a3ec574..43662ad9 100644 --- a/Commands/DebugCmds.cs +++ b/Commands/DebugCmds.cs @@ -15,7 +15,18 @@ class DebugCmd [Description("Debug GIF properties.")] public async Task GifDebug(CommandContext ctx, string gifToCheck) { - SeizureDetectionHelpers.ImageInfo Gif = SeizureDetectionHelpers.GetGifProperties(gifToCheck); + string gifUrl; + var emojiMatches = Constants.RegexConstants.animoji_rx.Matches(gifToCheck); + if (emojiMatches.Count > 0) + { + gifUrl = "https://cdn.discordapp.com/emojis/" + Constants.RegexConstants.id_rx.Match(emojiMatches.First().Value).Value + ".gif"; + } + else + { + gifUrl = gifToCheck; + } + + SeizureDetectionHelpers.ImageInfo Gif = await SeizureDetectionHelpers.GetGifPropertiesAsync(gifUrl); string strOut = "**GIF information**\n"; strOut += $"----------\nFrame count: **{Gif.FrameCount}**\n"; strOut += $"Unique frame count: **{Gif.UniqueFrameCount}**\n"; diff --git a/Events/MessageEvent.cs b/Events/MessageEvent.cs index 2832ae21..f8392271 100644 --- a/Events/MessageEvent.cs +++ b/Events/MessageEvent.cs @@ -1156,7 +1156,7 @@ private static async Task RunSeizureInducingEmojiFilterAsync(DiscordClient string id = id_rx.Match(dirtyid.ToString()).ToString(); string url = "https://cdn.discordapp.com/emojis/" + id + ".gif"; - if (SeizureDetectionHelpers.GetGifProperties(url).IsSeizureInducing) + if ((await SeizureDetectionHelpers.GetGifPropertiesAsync(url)).IsSeizureInducing) { if (wasAutoModBlock) { diff --git a/GlobalUsings.cs b/GlobalUsings.cs index c37e4a6b..0f39030a 100644 --- a/GlobalUsings.cs +++ b/GlobalUsings.cs @@ -25,14 +25,12 @@ global using Newtonsoft.Json.Linq; global using Serilog; global using Serilog.Sinks.SystemConsole.Themes; +global using SkiaSharp; global using StackExchange.Redis; global using System; global using System.Collections.Generic; global using System.ComponentModel; global using System.Diagnostics; -global using System.Drawing; -global using System.Drawing.Drawing2D; -global using System.Drawing.Imaging; global using System.IO; global using System.Linq; global using System.Net; diff --git a/Helpers/SeizureDetectionHelpers.cs b/Helpers/SeizureDetectionHelpers.cs index 4dbee59a..dda6604a 100644 --- a/Helpers/SeizureDetectionHelpers.cs +++ b/Helpers/SeizureDetectionHelpers.cs @@ -11,9 +11,9 @@ public class SeizureDetectionHelpers static int maxAverageFrameDifference = Program.cfgjson.SeizureDetection.MaxAverageFrameDifference; // Flag GIFs where the average difference between all their frames is greater than [value]% static List unsafeGifValues = Program.cfgjson.SeizureDetection.UnsafeGifValues; // If GIF has only [key] frames, then the average length of each frame has to be at least [value] ms long - public static ImageInfo GetGifProperties(string input) + public static async Task GetGifPropertiesAsync(string input) { - ImageInfo Gif = GetImageInfo(input); + ImageInfo Gif = await GetImageInfoAsync(input); // The actual check for whether or not the GIF might trigger a seizure try @@ -85,28 +85,25 @@ public static ImageInfo GetGifProperties(string input) return Gif; } - public static ImageInfo GetImageInfo(string url) + public static async Task GetImageInfoAsync(string url) { ImageInfo info = new ImageInfo(); - using (Image image = Image.FromStream(GetStreamFromUrl(url))) + using (SKCodec image = SKCodec.Create(await GetStreamFromUrlAsync(url))) { - info.Height = image.Height; - info.Width = image.Width; + info.Height = image.Info.Height; + info.Width = image.Info.Width; - if (image.RawFormat.Equals(ImageFormat.Gif)) + if (image.EncodedFormat == SKEncodedImageFormat.Gif) { - if (ImageAnimator.CanAnimate(image)) + if (image.FrameCount > 1) { - FrameDimension frameDimension = new FrameDimension(image.FrameDimensionsList[0]); - - int frameCount = image.GetFrameCount(frameDimension); + int frameCount = image.FrameCount; decimal delay = 0; decimal this_delay = 0; - int index = 0; decimal averageFrameDifference = 0; decimal averageContrast = 0; - List ExtractedFrames = new List(); + List ExtractedFrames = new List(); List> isUniqueList = new List>(frameCount); for (int i = 0; i < frameCount; i++) { @@ -119,22 +116,24 @@ public static ImageInfo GetImageInfo(string url) { for (int e = 0; e < frameCount; e++) { - image.SelectActiveFrame(frameDimension, e); - ExtractedFrames.Add(new Bitmap(image)); + SKBitmap bitmap = new SKBitmap(image.Info);; + image.GetPixels(image.Info, bitmap.GetPixels(), new SKCodecOptions(e)); + ExtractedFrames.Add(bitmap); } } + SKCodecFrameInfo[] frameInfo = image.FrameInfo; + //\\ ================ Beginning of compare loop ================ //\\ for (int f = 0; f < frameCount; f++) { - this_delay = BitConverter.ToInt32(image.GetPropertyItem(20736).Value, index) * 10; + this_delay = frameInfo[f].Duration; delay += this_delay; - index += 4; if (frameCount < maxComparableFrames) { int p = 0; - foreach (Bitmap bmp1 in ExtractedFrames) + foreach (SKBitmap bmp1 in ExtractedFrames) { bool compareFullFrame = false; if (p == f + 1) @@ -143,16 +142,16 @@ public static ImageInfo GetImageInfo(string url) } if (p != f && isUniqueList[f].Count == 0) { - var comparsionData = Compare(bmp1, ExtractedFrames[f], compareFullFrame); - if (comparsionData.Item1) + var comparisonData = Compare(bmp1, ExtractedFrames[f], compareFullFrame); + if (comparisonData.Item1) { isUniqueList[p].Add(f); } - if (comparsionData.Item2 != -1) + if (comparisonData.Item2 != -1) { - averageFrameDifference += comparsionData.Item2; - averageContrast += comparsionData.Item3; + averageFrameDifference += comparisonData.Item2; + averageContrast += comparisonData.Item3; } } else if (compareFullFrame == true && p != f) @@ -184,7 +183,7 @@ public static ImageInfo GetImageInfo(string url) uniqueFrameCount = frameCount; } // Dispose of all those nasty bitmaps - foreach (Bitmap bmp in ExtractedFrames) + foreach (SKBitmap bmp in ExtractedFrames) { bmp.Dispose(); } @@ -199,7 +198,7 @@ public static ImageInfo GetImageInfo(string url) info.AverageFrameDifference = averageFrameDifference / (frameCount - 1); info.AverageContrast = averageContrast / (frameCount - 1); info.IsAnimated = true; - info.IsLooped = BitConverter.ToInt16(image.GetPropertyItem(20737).Value, 0) != 1; + info.IsLooped = image.RepetitionCount != 0; info.Length = delay; info.FrameRate = frameCount / (info.Length / 1000); } @@ -213,7 +212,7 @@ public static ImageInfo GetImageInfo(string url) info.AverageFrameDifference = 0; info.AverageContrast = 0; info.IsAnimated = true; - info.IsLooped = BitConverter.ToInt16(image.GetPropertyItem(20737).Value, 0) != 1; + info.IsLooped = image.RepetitionCount != 0; info.Length = 1000 * frameCount; info.FrameRate = -1; } @@ -225,7 +224,7 @@ public static ImageInfo GetImageInfo(string url) // The almighty comparison mechanism. Takes in two images and checks to see if they're the same. Optionally it will compare the whole image. // Returns a bool stating whether or not the images are identical, and a decimal that contains a percentage of how similar the two images are (if compareFullFrame is true) - public static (bool, decimal, decimal) Compare(Bitmap bmp1, Bitmap bmp2, bool compareFullFrame) + public static (bool, decimal, decimal) Compare(SKBitmap bmp1, SKBitmap bmp2, bool compareFullFrame) { int pixelCount = bmp1.Width * bmp1.Height; // Total pixels in the frame int differingPixels = 0; // Keeps track of how many pixels differ between the two images @@ -266,20 +265,19 @@ public static (bool, decimal, decimal) Compare(Bitmap bmp1, Bitmap bmp2, bool co } // Gets the average colour of an image - private static Color GetAverageColour(Bitmap bmp) + private static SKColor GetAverageColour(SKBitmap bmp) { - Bitmap bmp1px = new Bitmap(1, 1); - using (Graphics g = Graphics.FromImage(bmp1px)) + SKBitmap bmp1px = new SKBitmap(1, 1); + using (SKCanvas c = new SKCanvas(bmp1px)) { - g.InterpolationMode = InterpolationMode.HighQualityBilinear; - g.DrawImage(bmp, new Rectangle(0, 0, 1, 1)); + c.DrawBitmap(bmp, new SKRect(0, 0, 1, 1), new SKSamplingOptions(SKFilterMode.Linear, SKMipmapMode.Linear)); } return bmp1px.GetPixel(0, 0); } - private static double GetLuminance(Color c) + private static double GetLuminance(SKColor c) { - byte[] colourArray = { c.R, c.G, c.B }; + byte[] colourArray = { c.Red, c.Green, c.Blue }; double[] luminanceArray = new double[3]; for (int i = 0; i < 3; i++) { @@ -291,7 +289,7 @@ private static double GetLuminance(Color c) return luminanceArray[0] * 0.2126 + luminanceArray[1] * 0.7152 + luminanceArray[2] * 0.0722; } - private static double GetContrast(Color c1, Color c2) + private static double GetContrast(SKColor c1, SKColor c2) { var lum1 = GetLuminance(c1); var lum2 = GetLuminance(c2); @@ -302,12 +300,11 @@ private static double GetContrast(Color c1, Color c2) } // Nothing fancy, just gets the GIF. - private static Stream GetStreamFromUrl(string url) + private static async Task GetStreamFromUrlAsync(string url) { byte[] imageData = null; - using (var wc = new System.Net.WebClient()) - imageData = wc.DownloadData(url); + imageData = await Program.httpClient.GetByteArrayAsync(url); Console.WriteLine("Downloaded GIF"); return new MemoryStream(imageData); From 0ba44d43a868cfe7edf70fdc6de17437f4d0260b Mon Sep 17 00:00:00 2001 From: FloatingMilkshake Date: Tue, 7 Jul 2026 21:30:05 -0400 Subject: [PATCH 06/11] Remove unnecessary method --- Helpers/SeizureDetectionHelpers.cs | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/Helpers/SeizureDetectionHelpers.cs b/Helpers/SeizureDetectionHelpers.cs index dda6604a..97ffe5c2 100644 --- a/Helpers/SeizureDetectionHelpers.cs +++ b/Helpers/SeizureDetectionHelpers.cs @@ -89,7 +89,7 @@ public static async Task GetImageInfoAsync(string url) { ImageInfo info = new ImageInfo(); - using (SKCodec image = SKCodec.Create(await GetStreamFromUrlAsync(url))) + using (SKCodec image = SKCodec.Create(new MemoryStream(await Program.httpClient.GetByteArrayAsync(url)))) { info.Height = image.Info.Height; info.Width = image.Info.Width; @@ -299,17 +299,6 @@ private static double GetContrast(SKColor c1, SKColor c2) / (darkest + 0.05); } - // Nothing fancy, just gets the GIF. - private static async Task GetStreamFromUrlAsync(string url) - { - byte[] imageData = null; - - imageData = await Program.httpClient.GetByteArrayAsync(url); - - Console.WriteLine("Downloaded GIF"); - return new MemoryStream(imageData); - } - // Definition for a loaded GIF public struct ImageInfo { From d21f2928e1dc07007aabaef0528d53c5fd2550a6 Mon Sep 17 00:00:00 2001 From: FloatingMilkshake Date: Tue, 7 Jul 2026 22:00:11 -0400 Subject: [PATCH 07/11] Use the logger instead of Console.WriteLine --- Helpers/SeizureDetectionHelpers.cs | 33 +++++++++++------------------- 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/Helpers/SeizureDetectionHelpers.cs b/Helpers/SeizureDetectionHelpers.cs index 97ffe5c2..e5e12023 100644 --- a/Helpers/SeizureDetectionHelpers.cs +++ b/Helpers/SeizureDetectionHelpers.cs @@ -62,26 +62,18 @@ public static async Task GetGifPropertiesAsync(string input) Gif.IsSeizureInducing = false; } - Console.WriteLine($"----------\nGIF: {input}"); - Console.WriteLine($"Frame count: {Gif.FrameCount}"); - Console.WriteLine($"Unique frame count: {Gif.UniqueFrameCount}"); - Console.WriteLine($"Average frame difference: {Math.Round(Gif.AverageFrameDifference, 2)}"); - Console.WriteLine($"Average frame contrast: {Math.Round(Gif.AverageContrast, 2)}"); - Console.WriteLine($"Length: {Gif.Length}ms"); - Console.WriteLine($"Frame duration: {Math.Round(Gif.Duration, 2)}ms"); - Console.WriteLine($"Framerate: {Math.Round(Gif.FrameRate, 2)}fps"); - Console.Write($"Seizure-inducing: "); - if (Gif.UniqueFrameCount != Gif.FrameCount && Gif.IsSeizureInducing) - Console.ForegroundColor = ConsoleColor.DarkRed; - else if (Gif.UniqueFrameCount != Gif.FrameCount) - Console.ForegroundColor = ConsoleColor.DarkGreen; - else if (Gif.IsSeizureInducing) - Console.ForegroundColor = ConsoleColor.Red; - else - Console.ForegroundColor = ConsoleColor.Green; - Console.Write($"{Gif.IsSeizureInducing}"); - Console.ResetColor(); - Console.Write(".\n----------\n\n"); + Program.discord.Logger.LogDebug("GIF: {gif}\n" + + "Frame count: {frameCount}\n" + + "Unique frame count: {uniqueFrameCount}\n" + + "Average frame difference: {averageFrameDifference}\n" + + "Average frame contrast: {averageContrast}\n" + + "Length: {length}ms\n" + + "Frame duration: {duration}ms\n" + + "Framerate: {frameRate}fps\n" + + "Seizure-inducing: {isSeizureInducing}.\n", + input, Gif.FrameCount, Gif.UniqueFrameCount, Math.Round(Gif.AverageFrameDifference, 2), + Math.Round(Gif.AverageContrast, 2), Gif.Length, Math.Round(Gif.Duration, 2), Math.Round(Gif.FrameRate, 2), Gif.IsSeizureInducing); + return Gif; } @@ -238,7 +230,6 @@ public static (bool, decimal, decimal) Compare(SKBitmap bmp1, SKBitmap bmp2, boo if (bmp1.GetPixel(x, y) != bmp2.GetPixel(x, y)) { differingPixels++; - //Console.WriteLine($"[NOTE] Pixel {x},{y} in bitmap 1 did not match pixel {x},{y} in bitmap 2"); if (differingPixels > maxDifferingPixels) { if (!compareFullFrame) From 51de80778c0bdef336ce7847603d49adf13d7922 Mon Sep 17 00:00:00 2001 From: FloatingMilkshake Date: Tue, 7 Jul 2026 21:55:06 -0400 Subject: [PATCH 08/11] Tweak variable names --- Events/MessageEvent.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Events/MessageEvent.cs b/Events/MessageEvent.cs index f8392271..1a6ea3ea 100644 --- a/Events/MessageEvent.cs +++ b/Events/MessageEvent.cs @@ -1148,12 +1148,12 @@ private static async Task RunPhishingApiFilterAsync(DiscordClient client, private static async Task RunSeizureInducingEmojiFilterAsync(DiscordClient client, MockDiscordMessage message, DiscordChannel channel, DiscordMember member, ServerPermLevel permLevel, string msgContentWithEmbedData, bool isAnEdit, bool limitFilters, bool wasAutoModBlock) { - var animatches = animoji_rx.Matches(message.Content); - if (animatches.Count > 0) + var emojiMatches = animoji_rx.Matches(message.Content); + if (emojiMatches.Count > 0) { - foreach (Match dirtyid in animatches) + foreach (Match emoji in emojiMatches) { - string id = id_rx.Match(dirtyid.ToString()).ToString(); + string id = id_rx.Match(emoji.Value).Value; string url = "https://cdn.discordapp.com/emojis/" + id + ".gif"; if ((await SeizureDetectionHelpers.GetGifPropertiesAsync(url)).IsSeizureInducing) From 483afad3db69cbe2050b19256661c9206f74c1c0 Mon Sep 17 00:00:00 2001 From: FloatingMilkshake Date: Tue, 7 Jul 2026 23:18:17 -0400 Subject: [PATCH 09/11] Add config --- config.json | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/config.json b/config.json index 85e91964..5d710765 100644 --- a/config.json +++ b/config.json @@ -353,5 +353,27 @@ "modActionReplyAutoWarnReason": "rule 12", "duplicateMessageExcludedChannels": [ 929902761519239208 - ] + ], + "seizureDetection": { + "comparisonAccuracy": 10, + "maxDifferingPixels": 2, + "maxComparableFrames": 300, + "harmlessAverageFrameDifference": 5, + "safeAverageFrameDifference": 20, + "penaltyAverageFrameDifference": 50, + "maxAverageFrameDifference": 95, + "unsafeGifValues": [ + 0, + 0, + 60, + 40, + 30, + 30, + 30, + 25, + 23, + 20, + 18 + ] + } } From 911b55c059bc144f81797a0ea4fdf9f9554a7bc3 Mon Sep 17 00:00:00 2001 From: FloatingMilkshake Date: Tue, 7 Jul 2026 23:32:01 -0400 Subject: [PATCH 10/11] Remove unnecessary capture group in emoji regex --- Constants/RegexConstants.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Constants/RegexConstants.cs b/Constants/RegexConstants.cs index 09f15691..d6562eff 100644 --- a/Constants/RegexConstants.cs +++ b/Constants/RegexConstants.cs @@ -21,6 +21,6 @@ public class RegexConstants readonly public static Regex webhook_rx = new("(?:https?:\\/\\/)?discord(?:app)?.com\\/api\\/(?:v\\d\\/)?webhooks\\/(?\\d+)\\/(?[A-Za-z0-9_\\-]+)", RegexOptions.ECMAScript); readonly public static Regex id_rx = new("[0-9]{17,}"); readonly public static Regex image_url_rx = new(@"https:\/\/(?:(?:i\.)?imgur\.com\/(?:a\/)?[A-Za-z0-9]+(?:\.[A-Za-z]{3,4})?|i\.ibb\.co\/[A-Za-z0-9]+\/[A-Za-z0-9]+\.[A-Za-z]{3,4}|(?:cdn\.discordapp\.com|media\.discordapp\.net)\/attachments\/[0-9]{17,}\/[0-9]{17,}\/[A-Za-z0-9]+\.[A-Za-z]{3,4}\?ex=[A-Za-z0-9]+&is=[A-Za-z0-9]+&hm=[A-Za-z0-9]+&?(?:=&format=[A-Za-z0-9]+&width=[0-9]+&height=[0-9]+)?)"); - readonly public static Regex animoji_rx = new Regex("()"); + readonly public static Regex animoji_rx = new Regex(""); } } From 0b48e65de2dde94619412309a07ba6cb26f7067b Mon Sep 17 00:00:00 2001 From: FloatingMilkshake Date: Tue, 7 Jul 2026 23:41:10 -0400 Subject: [PATCH 11/11] Handle missing config --- Commands/DebugCmds.cs | 6 ++++++ Events/MessageEvent.cs | 3 +++ 2 files changed, 9 insertions(+) diff --git a/Commands/DebugCmds.cs b/Commands/DebugCmds.cs index 9a414c24..889c8fc8 100644 --- a/Commands/DebugCmds.cs +++ b/Commands/DebugCmds.cs @@ -13,6 +13,12 @@ public class DebugCmds [Description("Debug GIF properties.")] public async Task GifDebug(CommandContext ctx, string gifToCheck) { + if (Program.cfgjson.SeizureDetection is null) + { + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} Seizure detection is not configured! Please check `seizureDetection` in config.json."); + return; + } + string gifUrl; var emojiMatches = Constants.RegexConstants.animoji_rx.Matches(gifToCheck); if (emojiMatches.Count > 0) diff --git a/Events/MessageEvent.cs b/Events/MessageEvent.cs index b4eec77e..2d620d34 100644 --- a/Events/MessageEvent.cs +++ b/Events/MessageEvent.cs @@ -1214,6 +1214,9 @@ private static async Task RunPhishingApiFilterAsync(DiscordClient client, private static async Task RunSeizureInducingEmojiFilterAsync(DiscordClient client, MockDiscordMessage message, DiscordChannel channel, DiscordMember member, ServerPermLevel permLevel, string msgContentWithEmbedData, bool isAnEdit, bool limitFilters, bool wasAutoModBlock) { + if (Program.cfgjson.SeizureDetection is null) + return false; + var emojiMatches = animoji_rx.Matches(message.Content); if (emojiMatches.Count > 0) {