Implement SharedEventService; remove cache update event

This commit is contained in:
Noi 2022-08-17 16:59:30 -07:00
parent 4f896e8311
commit b4db1fcff8
10 changed files with 84 additions and 57 deletions

View file

@ -12,7 +12,7 @@ internal partial class ModLogs : RegexbotModule {
public ModLogs(RegexbotClient bot) : base(bot) { public ModLogs(RegexbotClient bot) : base(bot) {
// TODO missing logging features: joins, leaves, bans, kicks, user edits (nick/username/discr) // TODO missing logging features: joins, leaves, bans, kicks, user edits (nick/username/discr)
DiscordClient.MessageDeleted += HandleDelete; DiscordClient.MessageDeleted += HandleDelete;
bot.EcOnMessageUpdate += HandleUpdate; bot.SharedEventReceived += FilterIncomingEvents;
} }
public override Task<object?> CreateGuildStateAsync(ulong guildID, JToken config) { public override Task<object?> CreateGuildStateAsync(ulong guildID, JToken config) {

View file

@ -73,6 +73,12 @@ internal partial class ModLogs {
await reportChannel.SendMessageAsync(embed: reportEmbed.Build()); 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) { private async Task HandleUpdate(CachedGuildMessage? oldMsg, SocketMessage newMsg) {
const int MaxPreviewLength = 500; const int MaxPreviewLength = 500;
var channel = (SocketTextChannel)newMsg.Channel; var channel = (SocketTextChannel)newMsg.Channel;

View file

@ -26,6 +26,7 @@ public partial class RegexbotClient {
// Get all services started up // Get all services started up
_svcLogging = new Services.Logging.LoggingService(this); _svcLogging = new Services.Logging.LoggingService(this);
_svcSharedEvents = new Services.SharedEventService.SharedEventService(this);
_svcGuildState = new Services.ModuleState.ModuleStateService(this); _svcGuildState = new Services.ModuleState.ModuleStateService(this);
_svcCommonFunctions = new Services.CommonFunctions.CommonFunctionsService(this); _svcCommonFunctions = new Services.CommonFunctions.CommonFunctionsService(this);
_svcEntityCache = new Services.EntityCache.EntityCacheService(this); _svcEntityCache = new Services.EntityCache.EntityCacheService(this);

View file

@ -6,12 +6,13 @@ namespace RegexBot.Services.EntityCache;
/// </summary> /// </summary>
class EntityCacheService : Service { class EntityCacheService : Service {
private readonly UserCachingSubservice _uc; private readonly UserCachingSubservice _uc;
#pragma warning disable IDE0052
private readonly MessageCachingSubservice _mc; private readonly MessageCachingSubservice _mc;
#pragma warning restore IDE0052
internal EntityCacheService(RegexbotClient bot) : base(bot) { internal EntityCacheService(RegexbotClient bot) : base(bot) {
// Currently we only have UserCache. May add Channel and Server caches later.
_uc = new UserCachingSubservice(bot, Log); _uc = new UserCachingSubservice(bot, Log);
_mc = new MessageCachingSubservice(bot, Log); _mc = new MessageCachingSubservice(bot);
} }
// Hooked // Hooked
@ -21,10 +22,4 @@ class EntityCacheService : Service {
// Hooked // Hooked
internal CachedGuildUser? QueryGuildUserCache(ulong guildId, string search) internal CachedGuildUser? QueryGuildUserCache(ulong guildId, string search)
=> _uc.DoGuildUserQuery(guildId, search); => _uc.DoGuildUserQuery(guildId, search);
// Hooked
internal event RegexbotClient.EcMessageUpdateHandler? OnCachePreUpdate {
add { lock (_mc) _mc.OnCachePreUpdate += value; }
remove { lock (_mc) _mc.OnCachePreUpdate -= value; }
}
} }

View file

@ -23,25 +23,4 @@ partial class RegexbotClient {
/// <param name="search">Search string. May be a name with discriminator, a name, or an ID.</param> /// <param name="search">Search string. May be a name with discriminator, a name, or an ID.</param>
/// <returns>A <see cref="CachedGuildUser"/> instance containing cached information, or null if no result.</returns> /// <returns>A <see cref="CachedGuildUser"/> instance containing cached information, or null if no result.</returns>
public CachedGuildUser? EcQueryGuildUser(ulong guildId, string search) => _svcEntityCache.QueryGuildUserCache(guildId, search); public CachedGuildUser? EcQueryGuildUser(ulong guildId, string search) => _svcEntityCache.QueryGuildUserCache(guildId, search);
/// <summary>
/// Fired after a message edit, when the message cache is about to be updated with the edited message.
/// </summary>
/// <remarks>
/// This event serves as an alternative to <seealso cref="BaseSocketClient.MessageUpdated"/>,
/// pulling the previous state of the message from the entity cache instead of the library's cache.
/// </remarks>
public event EcMessageUpdateHandler? EcOnMessageUpdate {
add { _svcEntityCache.OnCachePreUpdate += value; }
remove { _svcEntityCache.OnCachePreUpdate -= value; }
}
/// <summary>
/// Delegate used for the <seealso cref="EcOnMessageUpdate"/> event.
/// </summary>
/// <params>
/// <param name="oldMsg">The previous state of the message prior to being updated, as known by the entity cache.</param>
/// <param name="newMsg">The new, updated incoming message.</param>
/// </params>
public delegate Task EcMessageUpdateHandler(CachedGuildMessage? oldMsg, SocketMessage newMsg);
} }

View file

@ -0,0 +1,26 @@
using RegexBot.Data;
namespace RegexBot;
/// <summary>
/// Fired after a message edit, when the message cache is about to be updated with the edited message.
/// </summary>
/// <remarks>
/// Processing this serves as an alternative to <seealso cref="BaseSocketClient.MessageUpdated"/>,
/// pulling the previous state of the message from the entity cache instead of the library's cache.
/// </remarks>
public class MessageCacheUpdateEvent : ISharedEvent {
/// <summary>
/// Gets the previous state of the message prior to being updated, as known by the entity cache.
/// </summary>
public CachedGuildMessage? OldMessage { get; }
/// <summary>
/// Gets the new, updated incoming message.
/// </summary>
public SocketMessage NewMessage { get; }
internal MessageCacheUpdateEvent(CachedGuildMessage? old, SocketMessage @new) {
OldMessage = old;
NewMessage = @new;
}
}

View file

@ -1,16 +1,12 @@
using Discord; using Discord;
using RegexBot.Data; using RegexBot.Data;
using static RegexBot.RegexbotClient;
namespace RegexBot.Services.EntityCache; namespace RegexBot.Services.EntityCache;
class MessageCachingSubservice { class MessageCachingSubservice {
// Hooked private readonly RegexbotClient _bot;
public event EcMessageUpdateHandler? OnCachePreUpdate;
private readonly Action<string> _log; internal MessageCachingSubservice(RegexbotClient bot) {
_bot = bot;
internal MessageCachingSubservice(RegexbotClient bot, Action<string> logMethod) {
_log = logMethod;
bot.DiscordClient.MessageReceived += DiscordClient_MessageReceived; bot.DiscordClient.MessageReceived += DiscordClient_MessageReceived;
bot.DiscordClient.MessageUpdated += DiscordClient_MessageUpdated; bot.DiscordClient.MessageUpdated += DiscordClient_MessageUpdated;
} }
@ -34,7 +30,8 @@ class MessageCachingSubservice {
// Alternative for Discord.Net's MessageUpdated handler: // Alternative for Discord.Net's MessageUpdated handler:
// Notify subscribers of message update using EC entry for the previous message state // Notify subscribers of message update using EC entry for the previous message state
var oldMsg = CachedGuildMessage.Clone(cachedMsg); 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) { if (cachedMsg == null) {
@ -55,21 +52,4 @@ class MessageCachingSubservice {
} }
await db.SaveChangesAsync(); 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());
}
}
}
} }

View file

@ -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.Common;
using RegexBot.Data; 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 /// 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. /// provide information on users which the library may not be aware about, such as users no longer in a guild.
/// </summary> /// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static")]
class UserCachingSubservice { class UserCachingSubservice {
private readonly Action<string> _log; private readonly Action<string> _log;

View file

@ -0,0 +1,34 @@
using RegexBot.Services.SharedEventService;
namespace RegexBot;
partial class RegexbotClient {
private readonly SharedEventService _svcSharedEvents;
/// <summary>
/// Delegate used for the <seealso cref="SharedEventReceived"/> event.
/// </summary>
/// <param name="ev">The incoming event instance.</param>
public delegate Task IncomingSharedEventHandler(ISharedEvent ev);
/// <summary>
/// Sends an object instance implementing <seealso cref="ISharedEvent"/> to all modules and services
/// subscribed to the <seealso cref="SharedEventReceived"/> event.
/// </summary>
/// <remarks>
/// This method is non-blocking. Event handlers are executed in their own thread.
/// </remarks>
public Task PushSharedEventAsync(ISharedEvent ev) => _svcSharedEvents.PushSharedEventAsync(ev);
/// <summary>
/// This event is fired after a module or internal service calls <see cref="PushSharedEventAsync"/>.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public event IncomingSharedEventHandler? SharedEventReceived {
add { lock (_svcSharedEvents) _svcSharedEvents.Subscribers += value; }
remove { lock (_svcSharedEvents) _svcSharedEvents.Subscribers -= value; }
}
}

View file

@ -0,0 +1,6 @@
namespace RegexBot; // Note: Within RegexBot namespace, for ease of use by modules
/// <summary>
/// An empty interface which denotes that the implementing object instance may be passed through
/// the shared event service.
/// </summary>
public interface ISharedEvent { }