diff --git a/CompatBot/Config.cs b/CompatBot/Config.cs index 9550ec6a..8fec28eb 100644 --- a/CompatBot/Config.cs +++ b/CompatBot/Config.cs @@ -74,6 +74,7 @@ namespace CompatBot public static int BuildNumberDifferenceForOutdatedBuilds => config.GetValue(nameof(BuildNumberDifferenceForOutdatedBuilds), 10); public static int MinimumPiracyTriggerLength => config.GetValue(nameof(MinimumPiracyTriggerLength), 4); public static int MaxSyscallResultLines => config.GetValue(nameof(MaxSyscallResultLines), 13); + public static int ChannelMessageHistorySize => config.GetValue(nameof(ChannelMessageHistorySize), 100); public static string Token => config.GetValue(nameof(Token), ""); public static string AzureDevOpsToken => config.GetValue(nameof(AzureDevOpsToken), ""); public static string AzureComputerVisionKey => config.GetValue(nameof(AzureComputerVisionKey), ""); diff --git a/CompatBot/EventHandlers/GlobalMessageCache.cs b/CompatBot/EventHandlers/GlobalMessageCache.cs new file mode 100644 index 00000000..fc6bb08a --- /dev/null +++ b/CompatBot/EventHandlers/GlobalMessageCache.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Concurrent; +using System.Threading.Tasks; +using CompatBot.Utils; +using DSharpPlus.Entities; +using DSharpPlus.EventArgs; + +namespace CompatBot.EventHandlers +{ + using TCache = ConcurrentDictionary>; + + internal static class GlobalMessageCache + { + private static readonly TCache MessageQueue = new TCache(); + private static readonly Func KeyGen = (DiscordMessage m) => m.Id; + + public static Task OnMessageCreated(MessageCreateEventArgs args) + { + if (args.Channel.IsPrivate) + return Task.CompletedTask; + + if (!MessageQueue.TryGetValue(args.Channel.Id, out var queue)) + MessageQueue[args.Channel.Id] = queue = new FixedLengthBuffer(KeyGen); + lock(queue.syncObj) + queue.Add(args.Message); + + while (queue.Count > Config.ChannelMessageHistorySize) + lock(queue.syncObj) + queue.TrimExcess(); + return Task.CompletedTask; + } + + public static Task OnMessageDeleted(MessageDeleteEventArgs args) + { + if (args.Channel.IsPrivate) + return Task.CompletedTask; + + if (!MessageQueue.TryGetValue(args.Channel.Id, out var queue)) + return Task.CompletedTask; + + lock (queue.syncObj) + queue.Evict(args.Message.Id); + return Task.CompletedTask; + } + + public static Task OnMessagesBulkDeleted(MessageBulkDeleteEventArgs args) + { + if (args.Channel.IsPrivate) + return Task.CompletedTask; + + if (!MessageQueue.TryGetValue(args.Channel.Id, out var queue)) + return Task.CompletedTask; + + lock (queue.syncObj) + foreach (var m in args.Messages) + queue.Evict(m.Id); + return Task.CompletedTask; + } + + public static Task OnMessageUpdated(MessageUpdateEventArgs args) + { + if (args.Channel.IsPrivate) + return Task.CompletedTask; + + if (!MessageQueue.TryGetValue(args.Channel.Id, out var queue)) + return Task.CompletedTask; + + lock(queue.syncObj) + queue.AddOrReplace(args.Message); + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/CompatBot/Program.cs b/CompatBot/Program.cs index c2076f57..5869ada2 100644 --- a/CompatBot/Program.cs +++ b/CompatBot/Program.cs @@ -231,6 +231,7 @@ namespace CompatBot client.MessageCreated += _ => { Watchdog.TimeSinceLastIncomingMessage.Restart(); return Task.CompletedTask;}; client.MessageCreated += ContentFilterMonitor.OnMessageCreated; // should be first + client.MessageCreated += GlobalMessageCache.OnMessageCreated; if (!string.IsNullOrEmpty(Config.AzureComputerVisionKey)) client.MessageCreated += MediaScreenshotMonitor.OnMessageCreated; client.MessageCreated += ProductCodeLookup.OnMessageCreated; @@ -245,15 +246,19 @@ namespace CompatBot client.MessageCreated += IsTheGamePlayableHandler.OnMessageCreated; client.MessageCreated += EmpathySimulationHandler.OnMessageCreated; + client.MessageUpdated += GlobalMessageCache.OnMessageUpdated; client.MessageUpdated += ContentFilterMonitor.OnMessageUpdated; client.MessageUpdated += DiscordInviteFilter.OnMessageUpdated; client.MessageUpdated += EmpathySimulationHandler.OnMessageUpdated; + client.MessageDeleted += GlobalMessageCache.OnMessageDeleted; if (Config.DeletedMessagesLogChannelId > 0) client.MessageDeleted += DeletedMessagesMonitor.OnMessageDeleted; client.MessageDeleted += ThumbnailCacheMonitor.OnMessageDeleted; client.MessageDeleted += EmpathySimulationHandler.OnMessageDeleted; + client.MessagesBulkDeleted += GlobalMessageCache.OnMessagesBulkDeleted; + client.UserUpdated += UsernameSpoofMonitor.OnUserUpdated; client.UserUpdated += UsernameZalgoMonitor.OnUserUpdated; diff --git a/CompatBot/Utils/BufferCopyStream.cs b/CompatBot/Utils/BufferCopyStream.cs index 5893c248..80e9e79d 100644 --- a/CompatBot/Utils/BufferCopyStream.cs +++ b/CompatBot/Utils/BufferCopyStream.cs @@ -4,7 +4,7 @@ using System.IO; namespace CompatBot.Utils { - internal class BufferCopyStream : Stream, IDisposable + internal class BufferCopyStream : Stream, IDisposable { private readonly Stream baseStream; private bool usedForReads; diff --git a/CompatBot/Utils/FixedLengthBuffer.cs b/CompatBot/Utils/FixedLengthBuffer.cs new file mode 100644 index 00000000..cd68d772 --- /dev/null +++ b/CompatBot/Utils/FixedLengthBuffer.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace CompatBot.Utils +{ + internal class FixedLengthBuffer: IList + { + internal readonly object syncObj = new object(); + + public FixedLengthBuffer(Func keyGenerator) + { + makeKey = keyGenerator ?? throw new ArgumentNullException(nameof(keyGenerator)); + } + + public FixedLengthBuffer CloneShallow() + { + var result = new FixedLengthBuffer(makeKey); + foreach (var key in keyList) + result.keyList.Add(key); + foreach (var kvp in lookup) + result.lookup[kvp.Key] = kvp.Value; + return result; + } + + public IEnumerator GetEnumerator() => keyList.Select(k => lookup[k]).GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public void Add(TValue item) + { + TKey key = makeKey(item); + if (!lookup.ContainsKey(key)) + keyList.Add(key); + lookup[key] = item; + } + + public void AddOrReplace(TValue item) => Add(item); + + public void Clear() + { + keyList.Clear(); + lookup.Clear(); + } + + public void TrimOldItems(int count) + { + var keys = keyList.Take(count).ToList(); + keyList.RemoveRange(0, keys.Count); + foreach (var k in keys) + lookup.Remove(k); + } + + public void TrimExcess() + { + if (Count <= MaxLength) + return; + + TrimOldItems(Count - MaxLength); + } + + public List GetOldItems(int count) + { + return keyList.Take(Math.Min(Count, count)).Select(k => lookup[k]).ToList(); + } + + public List GetExcess() + { + if (Count <= MaxLength) + return new List(0); + return GetOldItems(Count - MaxLength); + } + + public TValue Evict(TKey key) + { + var result = lookup[key]; + lookup.Remove(key); + keyList.Remove(key); + return result; + } + + public bool Remove(TValue item) + { + var key = makeKey(item); + if (!Contains(key)) + return false; + + Evict(key); + return true; + } + + public bool Contains(TKey key) => lookup.ContainsKey(key); + public bool Contains(TValue item) => Contains(makeKey(item)); + + public void CopyTo(TValue[] array, int arrayIndex) => throw new NotSupportedException(); + public int IndexOf(TValue item) => throw new NotSupportedException(); + public void Insert(int index, TValue item) => throw new NotSupportedException(); + public void RemoveAt(int index) => throw new NotSupportedException(); + + public TValue this[int index] + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + + public int Count => lookup.Count; + public bool IsReadOnly => false; + + public bool NeedTrimming => Count > MaxLength + 20; + public TValue this[TKey index] => lookup[index]; + + private int MaxLength => Config.ChannelMessageHistorySize; + private readonly Func makeKey; + private readonly List keyList = new List(); + private readonly Dictionary lookup = new Dictionary(); + } +} \ No newline at end of file