Add message caching subservice
Partially implements legacy's ModLogs module on the bot side, with the remainder to be implemented as a proper module.
This commit is contained in:
parent
4f773e2573
commit
5671b7b48c
13 changed files with 175 additions and 42 deletions
|
@ -20,6 +20,7 @@ public class BotDatabaseContext : DbContext {
|
||||||
public DbSet<GuildLogLine> GuildLog { get; set; } = null!;
|
public DbSet<GuildLogLine> GuildLog { get; set; } = null!;
|
||||||
public DbSet<CachedUser> UserCache { get; set; } = null!;
|
public DbSet<CachedUser> UserCache { get; set; } = null!;
|
||||||
public DbSet<CachedGuildUser> GuildUserCache { get; set; } = null!;
|
public DbSet<CachedGuildUser> GuildUserCache { get; set; } = null!;
|
||||||
|
public DbSet<CachedGuildMessage> GuildMessageCache { get; set; } = null!;
|
||||||
|
|
||||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||||
=> optionsBuilder
|
=> optionsBuilder
|
||||||
|
@ -27,9 +28,9 @@ public class BotDatabaseContext : DbContext {
|
||||||
.UseSnakeCaseNamingConvention();
|
.UseSnakeCaseNamingConvention();
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder) {
|
protected override void OnModelCreating(ModelBuilder modelBuilder) {
|
||||||
modelBuilder.Entity<GuildLogLine>(entity =>
|
modelBuilder.Entity<GuildLogLine>(entity => entity.Property(e => e.Timestamp).HasDefaultValueSql("now()"));
|
||||||
entity.Property(e => e.Timestamp).HasDefaultValueSql("now()"));
|
|
||||||
modelBuilder.Entity<CachedUser>(entity => entity.Property(e => e.Discriminator).HasMaxLength(4).IsFixedLength());
|
modelBuilder.Entity<CachedUser>(entity => entity.Property(e => e.Discriminator).HasMaxLength(4).IsFixedLength());
|
||||||
modelBuilder.Entity<CachedGuildUser>(entity => entity.Navigation(e => e.User).AutoInclude());
|
modelBuilder.Entity<CachedGuildUser>(entity => entity.Navigation(e => e.User).AutoInclude());
|
||||||
|
modelBuilder.Entity<CachedGuildMessage>(entity => entity.Property(e => e.CreatedAt).HasDefaultValueSql("now()"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
36
RegexBot/Data/CachedGuildMessage.cs
Normal file
36
RegexBot/Data/CachedGuildMessage.cs
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
namespace RegexBot.Data;
|
||||||
|
[Table("cache_messages")]
|
||||||
|
public class CachedGuildMessage {
|
||||||
|
[Key]
|
||||||
|
public long MessageId { get; set; }
|
||||||
|
|
||||||
|
public long AuthorId { get; set; }
|
||||||
|
|
||||||
|
public long GuildId { get; set; }
|
||||||
|
|
||||||
|
public long ChannelId { get; set; }
|
||||||
|
|
||||||
|
public DateTimeOffset CreatedAt { get; set; }
|
||||||
|
|
||||||
|
public DateTimeOffset? EditedAt { get; set; }
|
||||||
|
|
||||||
|
public List<string> AttachmentNames { get; set; } = null!;
|
||||||
|
|
||||||
|
public string Content { get; set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>Gets the timestamp when the message was last updated.</summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This is equivalent to coalescing the value of <see cref="EditedAt"/> and <see cref="CreatedAt"/>.
|
||||||
|
/// </remarks>
|
||||||
|
[NotMapped]
|
||||||
|
public DateTimeOffset LastUpdatedAt => EditedAt ?? CreatedAt;
|
||||||
|
|
||||||
|
[ForeignKey(nameof(CachedUser.UserId))]
|
||||||
|
[InverseProperty(nameof(CachedUser.GuildMessages))]
|
||||||
|
public CachedUser Author { get; set; } = null!;
|
||||||
|
|
||||||
|
internal new CachedGuildMessage MemberwiseClone() => (CachedGuildMessage)base.MemberwiseClone();
|
||||||
|
}
|
|
@ -14,4 +14,7 @@ public class CachedUser {
|
||||||
|
|
||||||
[InverseProperty(nameof(CachedGuildUser.User))]
|
[InverseProperty(nameof(CachedGuildUser.User))]
|
||||||
public ICollection<CachedGuildUser> Guilds { get; set; } = null!;
|
public ICollection<CachedGuildUser> Guilds { get; set; } = null!;
|
||||||
|
|
||||||
|
[InverseProperty(nameof(CachedGuildMessage.Author))]
|
||||||
|
public ICollection<CachedGuildMessage> GuildMessages { get; set; } = null!;
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,20 +55,18 @@ class InstanceConfig {
|
||||||
throw new Exception(pfx + ex.Message, ex);
|
throw new Exception(pfx + ex.Message, ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
#pragma warning disable CS8601 // Possible null reference assignment.
|
|
||||||
// Input validation - throw exception on errors. Exception messages printed as-is.
|
// Input validation - throw exception on errors. Exception messages printed as-is.
|
||||||
BotToken = conf[nameof(BotToken)]?.Value<string>();
|
BotToken = conf[nameof(BotToken)]?.Value<string>()!;
|
||||||
if (string.IsNullOrEmpty(BotToken))
|
if (string.IsNullOrEmpty(BotToken))
|
||||||
throw new Exception($"'{nameof(BotToken)}' is not properly specified in configuration.");
|
throw new Exception($"'{nameof(BotToken)}' is not properly specified in configuration.");
|
||||||
|
|
||||||
PostgresConnString = conf[nameof(PostgresConnString)]?.Value<string>();
|
PostgresConnString = conf[nameof(PostgresConnString)]?.Value<string>()!;
|
||||||
if (string.IsNullOrEmpty(PostgresConnString))
|
if (string.IsNullOrEmpty(PostgresConnString))
|
||||||
throw new Exception($"'{nameof(PostgresConnString)}' is not properly specified in configuration.");
|
throw new Exception($"'{nameof(PostgresConnString)}' is not properly specified in configuration.");
|
||||||
|
|
||||||
InstanceLogTarget = conf[nameof(InstanceLogTarget)]?.Value<string>();
|
InstanceLogTarget = conf[nameof(InstanceLogTarget)]?.Value<string>()!;
|
||||||
if (string.IsNullOrEmpty(InstanceLogTarget))
|
if (string.IsNullOrEmpty(InstanceLogTarget))
|
||||||
throw new Exception($"'{nameof(InstanceLogTarget)}' is not properly specified in configuration.");
|
throw new Exception($"'{nameof(InstanceLogTarget)}' is not properly specified in configuration.");
|
||||||
#pragma warning restore CS8601
|
|
||||||
|
|
||||||
var asmList = conf[nameof(Assemblies)];
|
var asmList = conf[nameof(Assemblies)];
|
||||||
if (asmList == null || asmList.Type != JTokenType.Array) {
|
if (asmList == null || asmList.Type != JTokenType.Array) {
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
using Discord.WebSocket;
|
using Discord.WebSocket;
|
||||||
|
|
||||||
namespace RegexBot;
|
namespace RegexBot;
|
||||||
|
|
||||||
class Program {
|
class Program {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Timestamp specifying the date and time that the program began running.
|
/// Timestamp specifying the date and time that the program began running.
|
||||||
|
@ -51,7 +50,7 @@ class Program {
|
||||||
private static void Console_CancelKeyPress(object? sender, ConsoleCancelEventArgs e) {
|
private static void Console_CancelKeyPress(object? sender, ConsoleCancelEventArgs e) {
|
||||||
e.Cancel = true;
|
e.Cancel = true;
|
||||||
|
|
||||||
_main.InstanceLogAsync(true, nameof(RegexBot), "Shutting down. Reason: Interrupt signal.");
|
_main._svcLogging.DoInstanceLog(true, nameof(RegexBot), "Shutting down. Reason: Interrupt signal.");
|
||||||
|
|
||||||
// 5 seconds of leeway - any currently running tasks will need time to finish executing
|
// 5 seconds of leeway - any currently running tasks will need time to finish executing
|
||||||
var closeWait = Task.Delay(5000);
|
var closeWait = Task.Delay(5000);
|
||||||
|
@ -62,8 +61,8 @@ class Program {
|
||||||
closeWait.Wait();
|
closeWait.Wait();
|
||||||
|
|
||||||
bool success = _main.DiscordClient.StopAsync().Wait(1000);
|
bool success = _main.DiscordClient.StopAsync().Wait(1000);
|
||||||
if (!success) _main.InstanceLogAsync(false, nameof(RegexBot),
|
if (!success) _main._svcLogging.DoInstanceLog(false, nameof(RegexBot),
|
||||||
"Failed to disconnect cleanly from Discord. Will force shut down.").Wait();
|
"Failed to disconnect cleanly from Discord. Will force shut down.");
|
||||||
Environment.Exit(0);
|
Environment.Exit(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,15 +25,15 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="CommandLineParser" Version="2.8.0" />
|
<PackageReference Include="CommandLineParser" Version="2.8.0" />
|
||||||
<PackageReference Include="Discord.Net" Version="3.4.1" />
|
<PackageReference Include="Discord.Net" Version="3.6.1" />
|
||||||
<PackageReference Include="EFCore.NamingConventions" Version="6.0.0" />
|
<PackageReference Include="EFCore.NamingConventions" Version="6.0.0" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.3">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.5">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||||
<PackageReference Include="Npgsql" Version="6.0.3" />
|
<PackageReference Include="Npgsql" Version="6.0.4" />
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.3" />
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.4" />
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0" />
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,6 @@ using static RegexBot.RegexbotClient;
|
||||||
// Instances of this class are created by CommonFunctionService and are meant to be sent to modules,
|
// Instances of this class are created by CommonFunctionService and are meant to be sent to modules,
|
||||||
// therefore we put this in the root RegexBot namespace despite being specific to this service.
|
// therefore we put this in the root RegexBot namespace despite being specific to this service.
|
||||||
namespace RegexBot;
|
namespace RegexBot;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Contains information on various success/failure outcomes for a ban or kick operation.
|
/// Contains information on various success/failure outcomes for a ban or kick operation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -75,7 +74,7 @@ public class BanKickResult {
|
||||||
/// Returns a message representative of the ban/kick result that may be posted as-is
|
/// Returns a message representative of the ban/kick result that may be posted as-is
|
||||||
/// within the a Discord channel.
|
/// within the a Discord channel.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string GetResultString(RegexbotClient bot, ulong guildId) {
|
public string GetResultString(RegexbotClient bot) {
|
||||||
string msg;
|
string msg;
|
||||||
|
|
||||||
if (OperationSuccess) msg = ":white_check_mark: ";
|
if (OperationSuccess) msg = ":white_check_mark: ";
|
||||||
|
@ -92,10 +91,12 @@ public class BanKickResult {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_rptTargetId != 0) {
|
if (_rptTargetId != 0) {
|
||||||
var user = bot.EcQueryUser(guildId, _rptTargetId.ToString()).GetAwaiter().GetResult();
|
var user = bot.EcQueryUser(_rptTargetId.ToString());
|
||||||
if (user != null) {
|
if (user != null) {
|
||||||
// TODO sanitize possible formatting characters in display name
|
// TODO sanitize possible formatting characters in display name
|
||||||
msg += $" user **{user.Username}#{user.Discriminator}**";
|
msg += $" user **{user.Username}#{user.Discriminator}**";
|
||||||
|
} else {
|
||||||
|
msg += $" user with ID **{_rptTargetId}**";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ internal class CommonFunctionsService : Service {
|
||||||
/// Common processing for kicks and bans. Called by DoKickAsync and DoBanAsync.
|
/// Common processing for kicks and bans. Called by DoKickAsync and DoBanAsync.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="logReason">The reason to insert into the Audit Log.</param>
|
/// <param name="logReason">The reason to insert into the Audit Log.</param>
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static")]
|
||||||
internal async Task<BanKickResult> BanOrKickAsync(
|
internal async Task<BanKickResult> BanOrKickAsync(
|
||||||
RemovalType t, SocketGuild guild, string source, ulong target, int banPurgeDays,
|
RemovalType t, SocketGuild guild, string source, ulong target, int banPurgeDays,
|
||||||
string logReason, bool sendDmToTarget) {
|
string logReason, bool sendDmToTarget) {
|
||||||
|
@ -31,7 +32,7 @@ internal class CommonFunctionsService : Service {
|
||||||
// Can't kick without obtaining user object. Quit here.
|
// Can't kick without obtaining user object. Quit here.
|
||||||
if (t == RemovalType.Kick && utarget == null) return new BanKickResult(null, false, true, RemovalType.Kick, 0);
|
if (t == RemovalType.Kick && utarget == null) return new BanKickResult(null, false, true, RemovalType.Kick, 0);
|
||||||
|
|
||||||
// TODO notify services here as soon as we get some who will want to listen to this
|
// TODO notify services here as soon as we get some who will want to listen to this (use source parameter)
|
||||||
|
|
||||||
// Send DM notification
|
// Send DM notification
|
||||||
if (sendDmToTarget) {
|
if (sendDmToTarget) {
|
||||||
|
|
|
@ -2,9 +2,8 @@
|
||||||
using RegexBot.Services.CommonFunctions;
|
using RegexBot.Services.CommonFunctions;
|
||||||
|
|
||||||
namespace RegexBot;
|
namespace RegexBot;
|
||||||
|
|
||||||
partial class RegexbotClient {
|
partial class RegexbotClient {
|
||||||
private CommonFunctionsService _svcCommonFunctions;
|
private readonly CommonFunctionsService _svcCommonFunctions;
|
||||||
|
|
||||||
public enum RemovalType { None, Ban, Kick }
|
public enum RemovalType { None, Ban, Kick }
|
||||||
|
|
||||||
|
@ -31,9 +30,9 @@ partial class RegexbotClient {
|
||||||
/// <param name="targetSearch">The EntityCache search string.</param>
|
/// <param name="targetSearch">The EntityCache search string.</param>
|
||||||
public async Task<BanKickResult> BanAsync(SocketGuild guild, string source, string targetSearch,
|
public async Task<BanKickResult> BanAsync(SocketGuild guild, string source, string targetSearch,
|
||||||
int purgeDays, string reason, bool sendDMToTarget) {
|
int purgeDays, string reason, bool sendDMToTarget) {
|
||||||
var result = await EcQueryUser(guild.Id, targetSearch);
|
var result = EcQueryGuildUser(guild.Id, targetSearch);
|
||||||
if (result == null) return new BanKickResult(null, false, true, RemovalType.Ban, 0);
|
if (result == null) return new BanKickResult(null, false, true, RemovalType.Ban, 0);
|
||||||
return await BanAsync(guild, source, result.UserID, purgeDays, reason, sendDMToTarget);
|
return await BanAsync(guild, source, (ulong)result.UserId, purgeDays, reason, sendDMToTarget);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -44,9 +43,15 @@ partial class RegexbotClient {
|
||||||
/// <returns>A structure containing results of the ban operation.</returns>
|
/// <returns>A structure containing results of the ban operation.</returns>
|
||||||
/// <param name="guild">The guild in which to attempt the kick.</param>
|
/// <param name="guild">The guild in which to attempt the kick.</param>
|
||||||
/// <param name="source">The user, module, or service which is requesting this action to be taken.</param>
|
/// <param name="source">The user, module, or service which is requesting this action to be taken.</param>
|
||||||
/// <param name="targetUser">The user which to perform the action to.</param>
|
/// <param name="targetUser">The user which to perform the action towards.</param>
|
||||||
/// <param name="reason">Reason for the action. Sent to the Audit Log and user (if specified).</param>
|
/// <param name="reason">
|
||||||
/// <param name="sendDMToTarget">Specify whether to send a direct message to the target user informing them of the action being taken.</param>
|
/// Reason for the action. Sent to the guild's audit log and, if
|
||||||
|
/// <paramref name="sendDMToTarget"/> is <see langword="true"/>, the target.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="sendDMToTarget">
|
||||||
|
/// Specify whether to send a direct message to the target user informing them of the action
|
||||||
|
/// (that is, a ban/kick message).
|
||||||
|
/// </param>
|
||||||
public Task<BanKickResult> KickAsync(SocketGuild guild, string source, ulong targetUser, string reason, bool sendDMToTarget)
|
public Task<BanKickResult> KickAsync(SocketGuild guild, string source, ulong targetUser, string reason, bool sendDMToTarget)
|
||||||
=> _svcCommonFunctions.BanOrKickAsync(RemovalType.Ban, guild, source, targetUser, 0, reason, sendDMToTarget);
|
=> _svcCommonFunctions.BanOrKickAsync(RemovalType.Ban, guild, source, targetUser, 0, reason, sendDMToTarget);
|
||||||
|
|
||||||
|
@ -56,8 +61,8 @@ partial class RegexbotClient {
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="targetSearch">The EntityCache search string.</param>
|
/// <param name="targetSearch">The EntityCache search string.</param>
|
||||||
public async Task<BanKickResult> KickAsync(SocketGuild guild, string source, string targetSearch, string reason, bool sendDMToTarget) {
|
public async Task<BanKickResult> KickAsync(SocketGuild guild, string source, string targetSearch, string reason, bool sendDMToTarget) {
|
||||||
var result = await EcQueryUser(guild.Id, targetSearch);
|
var result = EcQueryGuildUser(guild.Id, targetSearch);
|
||||||
if (result == null) return new BanKickResult(null, false, true, RemovalType.Kick, 0);
|
if (result == null) return new BanKickResult(null, false, true, RemovalType.Kick, 0);
|
||||||
return await KickAsync(guild, source, result.UserID, reason, sendDMToTarget);
|
return await KickAsync(guild, source, (ulong)result.UserId, reason, sendDMToTarget);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
using RegexBot.Data;
|
using RegexBot.Data;
|
||||||
|
|
||||||
namespace RegexBot.Services.EntityCache;
|
namespace RegexBot.Services.EntityCache;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provides and maintains a database-backed cache of entities. Portions of information collected by this
|
/// Provides and maintains a database-backed cache of entities. Portions of information collected by this
|
||||||
/// service may be used by modules, while other portions are useful only for external applications which may
|
/// service may be used by modules, while other portions are useful only for external applications which may
|
||||||
|
@ -9,17 +8,25 @@ namespace RegexBot.Services.EntityCache;
|
||||||
/// </summary>
|
/// </summary>
|
||||||
class EntityCacheService : Service {
|
class EntityCacheService : Service {
|
||||||
private readonly UserCachingSubservice _uc;
|
private readonly UserCachingSubservice _uc;
|
||||||
|
private readonly MessageCachingSubservice _mc;
|
||||||
|
|
||||||
internal EntityCacheService(RegexbotClient bot) : base(bot) {
|
internal EntityCacheService(RegexbotClient bot) : base(bot) {
|
||||||
// Currently we only have UserCache. May add Channel and Server caches later.
|
// Currently we only have UserCache. May add Channel and Server caches later.
|
||||||
_uc = new UserCachingSubservice(bot);
|
_uc = new UserCachingSubservice(bot);
|
||||||
|
_mc = new MessageCachingSubservice(bot, Log);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hooked
|
// Hooked
|
||||||
internal static CachedUser? QueryUserCache(string search)
|
internal CachedUser? QueryUserCache(string search)
|
||||||
=> UserCachingSubservice.DoUserQuery(search);
|
=> _uc.DoUserQuery(search);
|
||||||
|
|
||||||
// Hooked
|
// Hooked
|
||||||
internal static CachedGuildUser? QueryGuildUserCache(ulong guildId, string search)
|
internal CachedGuildUser? QueryGuildUserCache(ulong guildId, string search)
|
||||||
=> UserCachingSubservice.DoGuildUserQuery(guildId, search);
|
=> _uc.DoGuildUserQuery(guildId, search);
|
||||||
|
|
||||||
|
// Hooked
|
||||||
|
internal event RegexbotClient.CachePreUpdateHandler? OnCachePreUpdate {
|
||||||
|
add { lock (_mc) _mc.OnCachePreUpdate += value; }
|
||||||
|
remove { lock (_mc) _mc.OnCachePreUpdate -= value; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
#pragma warning disable CA1822
|
using RegexBot.Data;
|
||||||
using RegexBot.Data;
|
|
||||||
using RegexBot.Services.EntityCache;
|
using RegexBot.Services.EntityCache;
|
||||||
|
|
||||||
namespace RegexBot;
|
namespace RegexBot;
|
||||||
|
|
||||||
partial class RegexbotClient {
|
partial class RegexbotClient {
|
||||||
private EntityCacheService _svcEntityCache;
|
private readonly EntityCacheService _svcEntityCache;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Queries the entity cache for user information. The given search string may contain a user ID
|
/// Queries the entity cache for user information. The given search string may contain a user ID
|
||||||
|
@ -14,7 +12,7 @@ partial class RegexbotClient {
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <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="CachedUser"/> instance containing cached information, or null if no result.</returns>
|
/// <returns>A <see cref="CachedUser"/> instance containing cached information, or null if no result.</returns>
|
||||||
public CachedUser? EcQueryUser(string search) => EntityCacheService.QueryUserCache(search);
|
public CachedUser? EcQueryUser(string search) => _svcEntityCache.QueryUserCache(search);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Queries the entity cache for guild-specific user information. The given search string may contain a user ID,
|
/// Queries the entity cache for guild-specific user information. The given search string may contain a user ID,
|
||||||
|
@ -24,5 +22,20 @@ partial class RegexbotClient {
|
||||||
/// <param name="guildId">ID of the corresponding guild in which to search.</param>
|
/// <param name="guildId">ID of the corresponding guild in which to search.</param>
|
||||||
/// <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) => EntityCacheService.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>
|
||||||
|
/// An alternative to <seealso cref="Discord.WebSocket.BaseSocketClient.MessageUpdated"/>.<br />
|
||||||
|
/// This event is fired in response to a guild message being edited and provides handlers with existing
|
||||||
|
/// cached contents before it is updated and the previous contents permanently lost.
|
||||||
|
/// </remarks>
|
||||||
|
public event CachePreUpdateHandler? OnCachePreUpdate {
|
||||||
|
add { _svcEntityCache.OnCachePreUpdate += value; }
|
||||||
|
remove { _svcEntityCache.OnCachePreUpdate -= value; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public delegate Task CachePreUpdateHandler(CachedGuildMessage cachedMsg);
|
||||||
}
|
}
|
||||||
|
|
69
RegexBot/Services/EntityCache/MessageCachingSubservice.cs
Normal file
69
RegexBot/Services/EntityCache/MessageCachingSubservice.cs
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
using Discord;
|
||||||
|
using Discord.WebSocket;
|
||||||
|
using RegexBot.Data;
|
||||||
|
using static RegexBot.RegexbotClient;
|
||||||
|
|
||||||
|
namespace RegexBot.Services.EntityCache;
|
||||||
|
class MessageCachingSubservice {
|
||||||
|
// Hooked
|
||||||
|
public event CachePreUpdateHandler? OnCachePreUpdate;
|
||||||
|
|
||||||
|
private readonly Action<string, bool> _log;
|
||||||
|
|
||||||
|
internal MessageCachingSubservice(RegexbotClient bot, Action<string, bool> logMethod) {
|
||||||
|
_log = logMethod;
|
||||||
|
bot.DiscordClient.MessageReceived += DiscordClient_MessageReceived;
|
||||||
|
bot.DiscordClient.MessageUpdated += DiscordClient_MessageUpdated;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task DiscordClient_MessageReceived(SocketMessage arg) {
|
||||||
|
if (arg.Channel is IDMChannel || arg is not SocketSystemMessage) return Task.CompletedTask;
|
||||||
|
return AddOrUpdateCacheItemAsync(arg);
|
||||||
|
}
|
||||||
|
private Task DiscordClient_MessageUpdated(Cacheable<IMessage, ulong> arg1, SocketMessage arg2, ISocketMessageChannel arg3) {
|
||||||
|
if (arg2.Channel is IDMChannel || arg2 is not SocketSystemMessage) return Task.CompletedTask;
|
||||||
|
return AddOrUpdateCacheItemAsync(arg2);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task AddOrUpdateCacheItemAsync(SocketMessage arg) {
|
||||||
|
using var db = new BotDatabaseContext();
|
||||||
|
|
||||||
|
CachedGuildMessage? msg = db.GuildMessageCache.Where(m => m.MessageId == (long)arg.Id).SingleOrDefault();
|
||||||
|
if (msg == null) {
|
||||||
|
msg = new() {
|
||||||
|
MessageId = (long)arg.Id,
|
||||||
|
AuthorId = (long)arg.Author.Id,
|
||||||
|
GuildId = (long)((SocketGuildUser)arg.Author).Guild.Id,
|
||||||
|
ChannelId = (long)arg.Channel.Id,
|
||||||
|
AttachmentNames = arg.Attachments.Select(a => a.Filename).ToList(),
|
||||||
|
Content = arg.Content
|
||||||
|
};
|
||||||
|
db.GuildMessageCache.Add(msg);
|
||||||
|
} else {
|
||||||
|
// Notify any listeners of cache update before it happens
|
||||||
|
var oldMsg = msg.MemberwiseClone();
|
||||||
|
await Task.Factory.StartNew(async () => await RunPreUpdateHandlersAsync(oldMsg));
|
||||||
|
|
||||||
|
msg.EditedAt = DateTimeOffset.UtcNow;
|
||||||
|
msg.Content = arg.Content;
|
||||||
|
msg.AttachmentNames = arg.Attachments.Select(a => a.Filename).ToList();
|
||||||
|
db.GuildMessageCache.Update(msg);
|
||||||
|
}
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RunPreUpdateHandlersAsync(CachedGuildMessage msg) {
|
||||||
|
CachePreUpdateHandler? eventList;
|
||||||
|
lock (this) eventList = OnCachePreUpdate;
|
||||||
|
if (eventList == null) return;
|
||||||
|
|
||||||
|
foreach (var handler in eventList.GetInvocationList()) {
|
||||||
|
try {
|
||||||
|
await (Task)handler.DynamicInvoke(msg)!;
|
||||||
|
} catch (Exception ex) {
|
||||||
|
_log($"Unhandled exception in {nameof(RegexbotClient.OnCachePreUpdate)} handler '{handler.Method.Name}':", false);
|
||||||
|
_log(ex.ToString(), false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,12 +3,12 @@ using RegexBot.Data;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
namespace RegexBot.Services.EntityCache;
|
namespace RegexBot.Services.EntityCache;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provides and maintains a database-backed cache of users.
|
/// Provides and maintains a database-backed cache of users.
|
||||||
/// 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 static Regex DiscriminatorSearch { get; } = new(@"(.+)#(\d{4}(?!\d))", RegexOptions.Compiled);
|
private static Regex DiscriminatorSearch { get; } = new(@"(.+)#(\d{4}(?!\d))", RegexOptions.Compiled);
|
||||||
|
|
||||||
|
@ -58,7 +58,7 @@ class UserCachingSubservice {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hooked
|
// Hooked
|
||||||
internal static CachedUser? DoUserQuery(string search) {
|
internal CachedUser? DoUserQuery(string search) {
|
||||||
static CachedUser? innerQuery(ulong? sID, (string name, string? disc)? nameSearch) {
|
static CachedUser? innerQuery(ulong? sID, (string name, string? disc)? nameSearch) {
|
||||||
var db = new BotDatabaseContext();
|
var db = new BotDatabaseContext();
|
||||||
|
|
||||||
|
@ -87,7 +87,7 @@ class UserCachingSubservice {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hooked
|
// Hooked
|
||||||
internal static CachedGuildUser? DoGuildUserQuery(ulong guildId, string search) {
|
internal CachedGuildUser? DoGuildUserQuery(ulong guildId, string search) {
|
||||||
static CachedGuildUser? innerQuery(ulong guildId, ulong? sID, (string name, string? disc)? nameSearch) {
|
static CachedGuildUser? innerQuery(ulong guildId, ulong? sID, (string name, string? disc)? nameSearch) {
|
||||||
var db = new BotDatabaseContext();
|
var db = new BotDatabaseContext();
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue