using System.Threading.Channels; using CompatApiClient.Utils; using CompatBot.Commands; using CompatBot.Database; using CompatBot.Database.Providers; using CompatBot.Ocr; using CompatBot.Utils.Extensions; using Microsoft.Extensions.Caching.Memory; namespace CompatBot.EventHandlers; internal sealed class MediaScreenshotMonitor { private static readonly Channel<(DiscordMessage msg, string imgUrl)> WorkQueue = Channel.CreateUnbounded<(DiscordMessage msg, string imgUrl)>(); private static readonly MemoryCache RemovedMessages = new(new MemoryCacheOptions() { ExpirationScanFrequency = TimeSpan.FromMinutes(10) }); private static readonly TimeSpan CachedTime = TimeSpan.FromMinutes(5); public DiscordClient Client { get; internal set; } = null!; public static int MaxQueueLength { get; private set; } public async Task OnMessageCreated(DiscordClient client, MessageCreatedEventArgs evt) { if (!OcrProvider.IsAvailable) return; if (evt.Message is not {} message) return; // if (!Config.Moderation.OcrChannels.Contains(evt.Channel.Id)) // return; if (message.Author.IsBotSafeCheck()) return; #if !DEBUG if (await message.Author.IsSmartlistedAsync(client).ConfigureAwait(false)) return; #endif EnqueueOcrTask(evt.Message); } public static void EnqueueOcrTask(DiscordMessage message) { if (!message.Attachments.Any() && !message.Embeds.Any()) return; var images = Vision.GetImageAttachments(message) .Concat(Vision.GetImagesFromEmbeds(message)) .ToList(); foreach (var url in images) { if (!WorkQueue.Writer.TryWrite((message, url))) Config.Log.Warn($"Failed to create a new text recognition task for message {message.JumpLink} / {url}"); } } public async Task ProcessWorkQueue() { if (!OcrProvider.IsAvailable) return; do { MaxQueueLength = Math.Max(MaxQueueLength, WorkQueue.Reader.Count); var (msg, imgUrl) = await WorkQueue.Reader.ReadAsync(Config.Cts.Token).ConfigureAwait(false); if (Config.Cts.IsCancellationRequested) return; if (RemovedMessages.TryGetValue(msg.Id, out bool removed) && removed) continue; try { var signature = GetSignature(msg); var prefix = $"[{msg.Id % 100:00}]"; if (RemovedMessages.TryGetValue(signature, out (Piracystring hit, DiscordMessage msg) previousItem)) { Config.Log.Debug($"{prefix} OCR result of message {msg.JumpLink} from user {msg.Author?.Username} ({msg.Author?.Id}):"); var ocrTextBuf = new StringBuilder($"OCR result of message <{msg.JumpLink}>:").AppendLine(); FilterAction suppressFlags = 0; if ("media".Equals(msg.Channel?.Name)) { suppressFlags = FilterAction.SendMessage | FilterAction.ShowExplain; } await ContentFilter.PerformFilterActions( Client, msg, previousItem.hit, suppressFlags, $"Matched to previously removed message from {previousItem.msg.Author?.Mention}: {previousItem.msg.JumpLink}", "🖼 Screenshot of an undesirable content", "Screenshot of an undesirable content" ).ConfigureAwait(false); if (previousItem.hit.Actions.HasFlag(FilterAction.RemoveContent)) RemovedMessages.Set(msg.Id, true, CachedTime); } else if (await OcrProvider.GetTextAsync(imgUrl, Config.Cts.Token).ConfigureAwait(false) is ({ Length: > 0 } result, var confidence)) { var cnt = true; var duplicates = new HashSet(); Config.Log.Debug($""" {prefix} OCR result of message {msg.JumpLink} from user {msg.Author?.Username} ({msg.Author?.Id}) ({confidence * 100:0.00}%): {result} """ ); var ocrTextBuf = new StringBuilder($"OCR result of message <{msg.JumpLink}> ({confidence * 100:0.00}%):").AppendLine() .AppendLine(result.Sanitize()); if (cnt && confidence > 0.65 && await ContentFilter.FindTriggerAsync(FilterContext.Chat, result).ConfigureAwait(false) is Piracystring hit && duplicates.Add(hit.String)) { FilterAction suppressFlags = 0; if ("media".Equals(msg.Channel?.Name)) { suppressFlags = FilterAction.SendMessage | FilterAction.ShowExplain; } await ContentFilter.PerformFilterActions( Client, msg, hit, suppressFlags, result, "🖼 Screenshot of an undesirable content", "Screenshot of an undesirable content" ).ConfigureAwait(false); if (hit.Actions.HasFlag(FilterAction.RemoveContent)) { RemovedMessages.Set(msg.Id, true, CachedTime); if (signature is { Length: >0}) RemovedMessages.Set(signature, (hit, msg), CachedTime); } cnt &= !hit.Actions.HasFlag(FilterAction.RemoveContent) && !hit.Actions.HasFlag(FilterAction.IssueWarning); } var ocrText = ocrTextBuf.ToString(); var hasVkDiagInfo = ocrText.Contains("Vulkan Diagnostics Tool v") || ocrText.Contains("VkDiag Version:"); if (!cnt || hasVkDiagInfo) { try { var botSpamCh = await Client.GetChannelAsync(Config.ThumbnailSpamId).ConfigureAwait(false); await botSpamCh.SendAutosplitMessageAsync(ocrTextBuf, blockStart: "", blockEnd: "").ConfigureAwait(false); } catch (Exception ex) { Config.Log.Warn(ex); } } } } catch (Exception e) { Config.Log.Warn(e); } } while (!Config.Cts.IsCancellationRequested); } private static string GetSignature(DiscordMessage msg) { var result = msg.Content ?? ""; foreach (var att in msg.Attachments) result += $"📎 {att.FileName} ({att.FileSize})\n"; return result.TrimEnd(); } }