diff --git a/Modules/ModLogs/ModLogs.cs b/Modules/ModLogs/ModLogs.cs index 213611a..d2dafee 100644 --- a/Modules/ModLogs/ModLogs.cs +++ b/Modules/ModLogs/ModLogs.cs @@ -12,7 +12,7 @@ internal partial class ModLogs : RegexbotModule { public ModLogs(RegexbotClient bot) : base(bot) { // TODO missing logging features: joins, leaves, bans, kicks, user edits (nick/username/discr) DiscordClient.MessageDeleted += HandleDelete; - bot.EcOnMessageUpdate += HandleUpdate; + bot.SharedEventReceived += FilterIncomingEvents; } public override Task CreateGuildStateAsync(ulong guildID, JToken config) { diff --git a/Modules/ModLogs/ModLogs_Messages.cs b/Modules/ModLogs/ModLogs_Messages.cs index 35f63a4..5c38331 100644 --- a/Modules/ModLogs/ModLogs_Messages.cs +++ b/Modules/ModLogs/ModLogs_Messages.cs @@ -73,6 +73,12 @@ internal partial class ModLogs { await reportChannel.SendMessageAsync(embed: reportEmbed.Build()); } + private async Task FilterIncomingEvents(ISharedEvent ev) { + if (ev is MessageCacheUpdateEvent upd) { + await HandleUpdate(upd.OldMessage, upd.NewMessage); + } + } + private async Task HandleUpdate(CachedGuildMessage? oldMsg, SocketMessage newMsg) { const int MaxPreviewLength = 500; var channel = (SocketTextChannel)newMsg.Channel; diff --git a/RegexbotClient.cs b/RegexbotClient.cs index 3c2c754..bb5d161 100644 --- a/RegexbotClient.cs +++ b/RegexbotClient.cs @@ -26,6 +26,7 @@ public partial class RegexbotClient { // Get all services started up _svcLogging = new Services.Logging.LoggingService(this); + _svcSharedEvents = new Services.SharedEventService.SharedEventService(this); _svcGuildState = new Services.ModuleState.ModuleStateService(this); _svcCommonFunctions = new Services.CommonFunctions.CommonFunctionsService(this); _svcEntityCache = new Services.EntityCache.EntityCacheService(this); diff --git a/Services/EntityCache/EntityCacheService.cs b/Services/EntityCache/EntityCacheService.cs index 509f7d6..580c133 100644 --- a/Services/EntityCache/EntityCacheService.cs +++ b/Services/EntityCache/EntityCacheService.cs @@ -6,12 +6,13 @@ namespace RegexBot.Services.EntityCache; /// class EntityCacheService : Service { private readonly UserCachingSubservice _uc; + #pragma warning disable IDE0052 private readonly MessageCachingSubservice _mc; + #pragma warning restore IDE0052 internal EntityCacheService(RegexbotClient bot) : base(bot) { - // Currently we only have UserCache. May add Channel and Server caches later. _uc = new UserCachingSubservice(bot, Log); - _mc = new MessageCachingSubservice(bot, Log); + _mc = new MessageCachingSubservice(bot); } // Hooked @@ -21,10 +22,4 @@ class EntityCacheService : Service { // Hooked internal CachedGuildUser? QueryGuildUserCache(ulong guildId, string search) => _uc.DoGuildUserQuery(guildId, search); - - // Hooked - internal event RegexbotClient.EcMessageUpdateHandler? OnCachePreUpdate { - add { lock (_mc) _mc.OnCachePreUpdate += value; } - remove { lock (_mc) _mc.OnCachePreUpdate -= value; } - } } diff --git a/Services/EntityCache/Hooks.cs b/Services/EntityCache/Hooks.cs index 019756a..0476e78 100644 --- a/Services/EntityCache/Hooks.cs +++ b/Services/EntityCache/Hooks.cs @@ -23,25 +23,4 @@ partial class RegexbotClient { /// Search string. May be a name with discriminator, a name, or an ID. /// A instance containing cached information, or null if no result. public CachedGuildUser? EcQueryGuildUser(ulong guildId, string search) => _svcEntityCache.QueryGuildUserCache(guildId, search); - - /// - /// Fired after a message edit, when the message cache is about to be updated with the edited message. - /// - /// - /// This event serves as an alternative to , - /// pulling the previous state of the message from the entity cache instead of the library's cache. - /// - public event EcMessageUpdateHandler? EcOnMessageUpdate { - add { _svcEntityCache.OnCachePreUpdate += value; } - remove { _svcEntityCache.OnCachePreUpdate -= value; } - } - - /// - /// Delegate used for the event. - /// - /// - /// The previous state of the message prior to being updated, as known by the entity cache. - /// The new, updated incoming message. - /// - public delegate Task EcMessageUpdateHandler(CachedGuildMessage? oldMsg, SocketMessage newMsg); } diff --git a/Services/EntityCache/MessageCacheUpdateEvent.cs b/Services/EntityCache/MessageCacheUpdateEvent.cs new file mode 100644 index 0000000..a396078 --- /dev/null +++ b/Services/EntityCache/MessageCacheUpdateEvent.cs @@ -0,0 +1,26 @@ +using RegexBot.Data; + +namespace RegexBot; +/// +/// Fired after a message edit, when the message cache is about to be updated with the edited message. +/// +/// +/// Processing this serves as an alternative to , +/// pulling the previous state of the message from the entity cache instead of the library's cache. +/// +public class MessageCacheUpdateEvent : ISharedEvent { + /// + /// Gets the previous state of the message prior to being updated, as known by the entity cache. + /// + public CachedGuildMessage? OldMessage { get; } + + /// + /// Gets the new, updated incoming message. + /// + public SocketMessage NewMessage { get; } + + internal MessageCacheUpdateEvent(CachedGuildMessage? old, SocketMessage @new) { + OldMessage = old; + NewMessage = @new; + } +} \ No newline at end of file diff --git a/Services/EntityCache/MessageCachingSubservice.cs b/Services/EntityCache/MessageCachingSubservice.cs index 86ee9ab..6f4f894 100644 --- a/Services/EntityCache/MessageCachingSubservice.cs +++ b/Services/EntityCache/MessageCachingSubservice.cs @@ -1,16 +1,12 @@ using Discord; using RegexBot.Data; -using static RegexBot.RegexbotClient; namespace RegexBot.Services.EntityCache; class MessageCachingSubservice { - // Hooked - public event EcMessageUpdateHandler? OnCachePreUpdate; + private readonly RegexbotClient _bot; - private readonly Action _log; - - internal MessageCachingSubservice(RegexbotClient bot, Action logMethod) { - _log = logMethod; + internal MessageCachingSubservice(RegexbotClient bot) { + _bot = bot; bot.DiscordClient.MessageReceived += DiscordClient_MessageReceived; bot.DiscordClient.MessageUpdated += DiscordClient_MessageUpdated; } @@ -34,7 +30,8 @@ class MessageCachingSubservice { // Alternative for Discord.Net's MessageUpdated handler: // Notify subscribers of message update using EC entry for the previous message state var oldMsg = CachedGuildMessage.Clone(cachedMsg); - await Task.Factory.StartNew(async () => await RunPreUpdateHandlersAsync(oldMsg, arg)); + var updEvent = new MessageCacheUpdateEvent(oldMsg, arg); + await _bot.PushSharedEventAsync(updEvent); } if (cachedMsg == null) { @@ -55,21 +52,4 @@ class MessageCachingSubservice { } await db.SaveChangesAsync(); } - - private async Task RunPreUpdateHandlersAsync(CachedGuildMessage? oldMsg, SocketMessage newMsg) { - Delegate[]? subscribers; - lock (this) { - subscribers = OnCachePreUpdate?.GetInvocationList(); - if (subscribers == null || subscribers.Length == 0) return; - } - - foreach (var handler in subscribers) { - try { - await (Task)handler.DynamicInvoke(oldMsg, newMsg)!; - } catch (Exception ex) { - _log($"Unhandled exception in {nameof(RegexbotClient.EcOnMessageUpdate)} handler '{handler.Method.Name}':\n" - + ex.ToString()); - } - } - } } diff --git a/Services/EntityCache/UserCachingSubservice.cs b/Services/EntityCache/UserCachingSubservice.cs index 1ffb072..e027b78 100644 --- a/Services/EntityCache/UserCachingSubservice.cs +++ b/Services/EntityCache/UserCachingSubservice.cs @@ -1,4 +1,5 @@ -using Microsoft.EntityFrameworkCore; +#pragma warning disable CA1822 // "Mark members as static" - will not make static to encourage better structure +using Microsoft.EntityFrameworkCore; using RegexBot.Common; using RegexBot.Data; @@ -8,7 +9,6 @@ namespace RegexBot.Services.EntityCache; /// It is meant to work as a supplement to Discord.Net's own user caching capabilities. Its purpose is to /// provide information on users which the library may not be aware about, such as users no longer in a guild. /// -[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static")] class UserCachingSubservice { private readonly Action _log; diff --git a/Services/SharedEventService/Hooks.cs b/Services/SharedEventService/Hooks.cs new file mode 100644 index 0000000..d333afc --- /dev/null +++ b/Services/SharedEventService/Hooks.cs @@ -0,0 +1,34 @@ +using RegexBot.Services.SharedEventService; + +namespace RegexBot; +partial class RegexbotClient { + private readonly SharedEventService _svcSharedEvents; + + /// + /// Delegate used for the event. + /// + /// The incoming event instance. + public delegate Task IncomingSharedEventHandler(ISharedEvent ev); + + /// + /// Sends an object instance implementing to all modules and services + /// subscribed to the event. + /// + /// + /// This method is non-blocking. Event handlers are executed in their own thread. + /// + public Task PushSharedEventAsync(ISharedEvent ev) => _svcSharedEvents.PushSharedEventAsync(ev); + + /// + /// This event is fired after a module or internal service calls . + /// + /// + /// Subscribers to this event are handled on a "fire and forget" basis and may execute on a thread + /// separate from the main one handling Discord events. Ensure that the code executed by the handler + /// executes quickly, is thread-safe, and throws no exceptions. + /// + public event IncomingSharedEventHandler? SharedEventReceived { + add { lock (_svcSharedEvents) _svcSharedEvents.Subscribers += value; } + remove { lock (_svcSharedEvents) _svcSharedEvents.Subscribers -= value; } + } +} \ No newline at end of file diff --git a/Services/SharedEventService/SharedEvent.cs b/Services/SharedEventService/SharedEvent.cs new file mode 100644 index 0000000..f8186f0 --- /dev/null +++ b/Services/SharedEventService/SharedEvent.cs @@ -0,0 +1,6 @@ +namespace RegexBot; // Note: Within RegexBot namespace, for ease of use by modules +/// +/// An empty interface which denotes that the implementing object instance may be passed through +/// the shared event service. +/// +public interface ISharedEvent { } \ No newline at end of file