diff --git a/.idea/.idea.catalogger/.idea/dataSources.xml b/.idea/.idea.catalogger/.idea/dataSources.xml new file mode 100644 index 0000000..b15909d --- /dev/null +++ b/.idea/.idea.catalogger/.idea/dataSources.xml @@ -0,0 +1,18 @@ + + + + + postgresql + true + org.postgresql.Driver + jdbc:postgresql://localhost:5432/catalogger + + + + + + + $ProjectFileDir$ + + + \ No newline at end of file diff --git a/.idea/.idea.catalogger/.idea/sqldialects.xml b/.idea/.idea.catalogger/.idea/sqldialects.xml new file mode 100644 index 0000000..6df4889 --- /dev/null +++ b/.idea/.idea.catalogger/.idea/sqldialects.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Catalogger.Backend/Bot/Responders/Channels/ChannelCreateResponder.cs b/Catalogger.Backend/Bot/Responders/Channels/ChannelCreateResponder.cs index f1c4b8f..b2d5f50 100644 --- a/Catalogger.Backend/Bot/Responders/Channels/ChannelCreateResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Channels/ChannelCreateResponder.cs @@ -14,8 +14,7 @@ // along with this program. If not, see . using Catalogger.Backend.Cache.InMemoryCache; -using Catalogger.Backend.Database; -using Catalogger.Backend.Database.Queries; +using Catalogger.Backend.Database.Dapper.Repositories; using Catalogger.Backend.Extensions; using Catalogger.Backend.Services; using Remora.Discord.API.Abstractions.Gateway.Events; @@ -27,7 +26,7 @@ using Remora.Results; namespace Catalogger.Backend.Bot.Responders.Channels; public class ChannelCreateResponder( - DatabaseContext db, + GuildRepository guildRepository, RoleCache roleCache, ChannelCache channelCache, UserCache userCache, @@ -96,7 +95,7 @@ public class ChannelCreateResponder( } } - var guildConfig = await db.GetGuildAsync(ch.GuildID.Value, false, ct); + var guildConfig = await guildRepository.GetAsync(ch.GuildID); webhookExecutor.QueueLog( guildConfig, LogChannelType.ChannelCreate, diff --git a/Catalogger.Backend/Bot/Responders/Channels/ChannelDeleteResponder.cs b/Catalogger.Backend/Bot/Responders/Channels/ChannelDeleteResponder.cs index a10eed7..78b5e89 100644 --- a/Catalogger.Backend/Bot/Responders/Channels/ChannelDeleteResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Channels/ChannelDeleteResponder.cs @@ -15,6 +15,7 @@ using Catalogger.Backend.Cache.InMemoryCache; using Catalogger.Backend.Database; +using Catalogger.Backend.Database.Dapper.Repositories; using Catalogger.Backend.Database.Queries; using Catalogger.Backend.Extensions; using Catalogger.Backend.Services; @@ -27,7 +28,7 @@ namespace Catalogger.Backend.Bot.Responders.Channels; public class ChannelDeleteResponder( ILogger logger, - DatabaseContext db, + GuildRepository guildRepository, ChannelCache channelCache, WebhookExecutorService webhookExecutor ) : IResponder @@ -49,7 +50,7 @@ public class ChannelDeleteResponder( return Result.Success; } - var guildConfig = await db.GetGuildAsync(evt.GuildID.Value, false, ct); + var guildConfig = await guildRepository.GetAsync(evt.GuildID.Value); var embed = new EmbedBuilder() .WithTitle("Channel deleted") .WithColour(DiscordUtils.Red) diff --git a/Catalogger.Backend/Bot/Responders/Channels/ChannelUpdateResponder.cs b/Catalogger.Backend/Bot/Responders/Channels/ChannelUpdateResponder.cs index 591cbe1..ea762f9 100644 --- a/Catalogger.Backend/Bot/Responders/Channels/ChannelUpdateResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Channels/ChannelUpdateResponder.cs @@ -15,6 +15,7 @@ using Catalogger.Backend.Cache.InMemoryCache; using Catalogger.Backend.Database; +using Catalogger.Backend.Database.Dapper.Repositories; using Catalogger.Backend.Database.Queries; using Catalogger.Backend.Extensions; using Catalogger.Backend.Services; @@ -30,7 +31,7 @@ namespace Catalogger.Backend.Bot.Responders.Channels; public class ChannelUpdateResponder( ILogger logger, - DatabaseContext db, + GuildRepository guildRepository, ChannelCache channelCache, RoleCache roleCache, UserCache userCache, @@ -49,7 +50,7 @@ public class ChannelUpdateResponder( return Result.Success; } - var guildConfig = await db.GetGuildAsync(evt.GuildID.Value, false, ct); + var guildConfig = await guildRepository.GetAsync(evt.GuildID); var builder = new EmbedBuilder() .WithTitle( diff --git a/Catalogger.Backend/Bot/Responders/Guilds/GuildBanAddResponder.cs b/Catalogger.Backend/Bot/Responders/Guilds/GuildBanAddResponder.cs index 80e0196..2001efb 100644 --- a/Catalogger.Backend/Bot/Responders/Guilds/GuildBanAddResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Guilds/GuildBanAddResponder.cs @@ -15,6 +15,7 @@ using Catalogger.Backend.Cache.InMemoryCache; using Catalogger.Backend.Database; +using Catalogger.Backend.Database.Dapper.Repositories; using Catalogger.Backend.Database.Queries; using Catalogger.Backend.Extensions; using Catalogger.Backend.Services; @@ -27,7 +28,7 @@ namespace Catalogger.Backend.Bot.Responders.Guilds; public class GuildBanAddResponder( ILogger logger, - DatabaseContext db, + GuildRepository guildRepository, WebhookExecutorService webhookExecutor, UserCache userCache, AuditLogCache auditLogCache, @@ -38,7 +39,7 @@ public class GuildBanAddResponder( public async Task RespondAsync(IGuildBanAdd evt, CancellationToken ct = default) { - var guildConfig = await db.GetGuildAsync(evt.GuildID, true, ct); + var guildConfig = await guildRepository.GetAsync(evt.GuildID); // Delay 2 seconds for the audit log await Task.Delay(2000, ct); @@ -76,10 +77,8 @@ public class GuildBanAddResponder( pkSystem.Id, evt.GuildID ); - guildConfig.BannedSystems.Add(pkSystem.Id); - guildConfig.BannedSystems.Add(pkSystem.Uuid.ToString()); - db.Update(guildConfig); - await db.SaveChangesAsync(ct); + + await guildRepository.BanSystemAsync(evt.GuildID, pkSystem.Id, pkSystem.Uuid); } embed.AddField( diff --git a/Catalogger.Backend/Bot/Responders/Guilds/GuildBanRemoveResponder.cs b/Catalogger.Backend/Bot/Responders/Guilds/GuildBanRemoveResponder.cs index 35d7363..0885298 100644 --- a/Catalogger.Backend/Bot/Responders/Guilds/GuildBanRemoveResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Guilds/GuildBanRemoveResponder.cs @@ -15,6 +15,7 @@ using Catalogger.Backend.Cache.InMemoryCache; using Catalogger.Backend.Database; +using Catalogger.Backend.Database.Dapper.Repositories; using Catalogger.Backend.Database.Queries; using Catalogger.Backend.Extensions; using Catalogger.Backend.Services; @@ -27,7 +28,7 @@ namespace Catalogger.Backend.Bot.Responders.Guilds; public class GuildBanRemoveResponder( ILogger logger, - DatabaseContext db, + GuildRepository guildRepository, WebhookExecutorService webhookExecutor, UserCache userCache, AuditLogCache auditLogCache, @@ -38,7 +39,7 @@ public class GuildBanRemoveResponder( public async Task RespondAsync(IGuildBanRemove evt, CancellationToken ct = default) { - var guildConfig = await db.GetGuildAsync(evt.GuildID, false, ct); + var guildConfig = await guildRepository.GetAsync(evt.GuildID); // Delay 2 seconds for the audit log await Task.Delay(2000, ct); @@ -68,10 +69,7 @@ public class GuildBanRemoveResponder( var pkSystem = await pluralkitApi.GetPluralKitSystemAsync(evt.User.ID.Value, ct); if (pkSystem != null) { - guildConfig.BannedSystems.Remove(pkSystem.Id); - guildConfig.BannedSystems.Remove(pkSystem.Uuid.ToString()); - db.Update(guildConfig); - await db.SaveChangesAsync(ct); + await guildRepository.UnbanSystemAsync(evt.GuildID, pkSystem.Id, pkSystem.Uuid); embed.AddField( "PluralKit system", diff --git a/Catalogger.Backend/Bot/Responders/Guilds/GuildCreateResponder.cs b/Catalogger.Backend/Bot/Responders/Guilds/GuildCreateResponder.cs index db1dbd7..0ee92df 100644 --- a/Catalogger.Backend/Bot/Responders/Guilds/GuildCreateResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Guilds/GuildCreateResponder.cs @@ -16,6 +16,7 @@ using Catalogger.Backend.Cache; using Catalogger.Backend.Cache.InMemoryCache; using Catalogger.Backend.Database; +using Catalogger.Backend.Database.Dapper.Repositories; using Catalogger.Backend.Extensions; using Catalogger.Backend.Services; using Remora.Discord.API.Abstractions.Gateway.Events; @@ -29,7 +30,7 @@ namespace Catalogger.Backend.Bot.Responders.Guilds; public class GuildCreateResponder( Config config, ILogger logger, - DatabaseContext db, + GuildRepository guildRepository, GuildCache guildCache, EmojiCache emojiCache, ChannelCache channelCache, @@ -75,13 +76,9 @@ public class GuildCreateResponder( guildId = unavailableGuild.ID.ToUlong(); } - var tx = await db.Database.BeginTransactionAsync(ct); - if (await db.Guilds.FindAsync([guildId], ct) != null) + if (await guildRepository.IsGuildKnown(guildId)) return Result.Success; - - db.Add(new Guild { Id = guildId }); - await db.SaveChangesAsync(ct); - await tx.CommitAsync(ct); + await guildRepository.AddGuildAsync(guildId); _logger.Information("Joined new guild {GuildName} / {GuildId}", guildName, guildId); diff --git a/Catalogger.Backend/Bot/Responders/Guilds/GuildEmojisUpdateResponder.cs b/Catalogger.Backend/Bot/Responders/Guilds/GuildEmojisUpdateResponder.cs index 48d26f7..ebdd9bb 100644 --- a/Catalogger.Backend/Bot/Responders/Guilds/GuildEmojisUpdateResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Guilds/GuildEmojisUpdateResponder.cs @@ -15,6 +15,7 @@ using Catalogger.Backend.Cache.InMemoryCache; using Catalogger.Backend.Database; +using Catalogger.Backend.Database.Dapper.Repositories; using Catalogger.Backend.Database.Queries; using Catalogger.Backend.Extensions; using Catalogger.Backend.Services; @@ -29,7 +30,7 @@ namespace Catalogger.Backend.Bot.Responders.Guilds; public class GuildEmojisUpdateResponder( ILogger logger, - DatabaseContext db, + GuildRepository guildRepository, EmojiCache emojiCache, WebhookExecutorService webhookExecutor ) : IResponder @@ -111,7 +112,7 @@ public class GuildEmojisUpdateResponder( return Result.Success; } - var guildConfig = await db.GetGuildAsync(evt.GuildID, false, ct); + var guildConfig = await guildRepository.GetAsync(evt.GuildID); webhookExecutor.QueueLog(guildConfig, LogChannelType.GuildEmojisUpdate, embed); return Result.Success; } diff --git a/Catalogger.Backend/Bot/Responders/Guilds/GuildUpdateResponder.cs b/Catalogger.Backend/Bot/Responders/Guilds/GuildUpdateResponder.cs index 23f3458..89ef472 100644 --- a/Catalogger.Backend/Bot/Responders/Guilds/GuildUpdateResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Guilds/GuildUpdateResponder.cs @@ -15,6 +15,7 @@ using Catalogger.Backend.Cache.InMemoryCache; using Catalogger.Backend.Database; +using Catalogger.Backend.Database.Dapper.Repositories; using Catalogger.Backend.Database.Queries; using Catalogger.Backend.Extensions; using Catalogger.Backend.Services; @@ -28,7 +29,7 @@ namespace Catalogger.Backend.Bot.Responders.Guilds; public class GuildUpdateResponder( ILogger logger, - DatabaseContext db, + GuildRepository guildRepository, GuildCache guildCache, UserCache userCache, WebhookExecutorService webhookExecutor @@ -97,7 +98,7 @@ public class GuildUpdateResponder( if (embed.Fields.Count != 0) { - var guildConfig = await db.GetGuildAsync(evt.ID, false, ct); + var guildConfig = await guildRepository.GetAsync(evt.ID); webhookExecutor.QueueLog( guildConfig, LogChannelType.GuildUpdate, diff --git a/Catalogger.Backend/Bot/Responders/Invites/InviteCreateResponder.cs b/Catalogger.Backend/Bot/Responders/Invites/InviteCreateResponder.cs index 11a7444..225f04a 100644 --- a/Catalogger.Backend/Bot/Responders/Invites/InviteCreateResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Invites/InviteCreateResponder.cs @@ -15,6 +15,7 @@ using Catalogger.Backend.Cache; using Catalogger.Backend.Database; +using Catalogger.Backend.Database.Dapper.Repositories; using Catalogger.Backend.Database.Queries; using Catalogger.Backend.Extensions; using Catalogger.Backend.Services; @@ -28,7 +29,7 @@ namespace Catalogger.Backend.Bot.Responders.Invites; public class InviteCreateResponder( ILogger logger, - DatabaseContext db, + GuildRepository guildRepository, IInviteCache inviteCache, IDiscordRestGuildAPI guildApi, WebhookExecutorService webhookExecutor @@ -74,7 +75,7 @@ public class InviteCreateResponder( inline: true ); - var guildConfig = await db.GetGuildAsync(guildId, false, ct); + var guildConfig = await guildRepository.GetAsync(evt.GuildID); webhookExecutor.QueueLog( guildConfig, LogChannelType.InviteCreate, diff --git a/Catalogger.Backend/Bot/Responders/Invites/InviteDeleteResponder.cs b/Catalogger.Backend/Bot/Responders/Invites/InviteDeleteResponder.cs index e5a5351..f736953 100644 --- a/Catalogger.Backend/Bot/Responders/Invites/InviteDeleteResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Invites/InviteDeleteResponder.cs @@ -15,6 +15,8 @@ using Catalogger.Backend.Cache; using Catalogger.Backend.Database; +using Catalogger.Backend.Database.Dapper; +using Catalogger.Backend.Database.Dapper.Repositories; using Catalogger.Backend.Database.Queries; using Catalogger.Backend.Extensions; using Catalogger.Backend.Services; @@ -29,6 +31,7 @@ namespace Catalogger.Backend.Bot.Responders.Invites; public class InviteDeleteResponder( ILogger logger, + GuildRepository guildRepository, DatabaseContext db, IInviteCache inviteCache, WebhookExecutorService webhookExecutor, @@ -89,7 +92,7 @@ public class InviteDeleteResponder( inline: true ); - var guildConfig = await db.GetGuildAsync(guildId, false, ct); + var guildConfig = await guildRepository.GetAsync(guildId); webhookExecutor.QueueLog( guildConfig, LogChannelType.InviteDelete, diff --git a/Catalogger.Backend/Bot/Responders/Members/GuildMemberAddResponder.cs b/Catalogger.Backend/Bot/Responders/Members/GuildMemberAddResponder.cs index abbbd8b..c92c8e0 100644 --- a/Catalogger.Backend/Bot/Responders/Members/GuildMemberAddResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Members/GuildMemberAddResponder.cs @@ -16,6 +16,7 @@ using Catalogger.Backend.Cache; using Catalogger.Backend.Cache.InMemoryCache; using Catalogger.Backend.Database; +using Catalogger.Backend.Database.Dapper.Repositories; using Catalogger.Backend.Database.Queries; using Catalogger.Backend.Extensions; using Catalogger.Backend.Services; @@ -35,6 +36,7 @@ namespace Catalogger.Backend.Bot.Responders.Members; public class GuildMemberAddResponder( ILogger logger, DatabaseContext db, + GuildRepository guildRepository, IMemberCache memberCache, IInviteCache inviteCache, UserCache userCache, @@ -62,7 +64,7 @@ public class GuildMemberAddResponder( .WithCurrentTimestamp() .WithFooter($"ID: {user.ID}"); - var guildConfig = await db.GetGuildAsync(member.GuildID, false, ct); + var guildConfig = await guildRepository.GetAsync(member.GuildID); var guildRes = await guildApi.GetGuildAsync(member.GuildID, withCounts: true, ct); if (guildRes.IsSuccess && guildRes.Entity.ApproximateMemberCount.IsDefined()) builder.Description += diff --git a/Catalogger.Backend/Bot/Responders/Members/GuildMemberRemoveResponder.cs b/Catalogger.Backend/Bot/Responders/Members/GuildMemberRemoveResponder.cs index 0e95276..bbeea22 100644 --- a/Catalogger.Backend/Bot/Responders/Members/GuildMemberRemoveResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Members/GuildMemberRemoveResponder.cs @@ -15,8 +15,7 @@ using Catalogger.Backend.Cache; using Catalogger.Backend.Cache.InMemoryCache; -using Catalogger.Backend.Database; -using Catalogger.Backend.Database.Queries; +using Catalogger.Backend.Database.Dapper.Repositories; using Catalogger.Backend.Extensions; using Catalogger.Backend.Services; using Remora.Discord.API.Abstractions.Gateway.Events; @@ -28,7 +27,7 @@ namespace Catalogger.Backend.Bot.Responders.Members; public class GuildMemberRemoveResponder( ILogger logger, - DatabaseContext db, + GuildRepository guildRepository, IMemberCache memberCache, RoleCache roleCache, UserCache userCache, @@ -50,7 +49,7 @@ public class GuildMemberRemoveResponder( .WithFooter($"ID: {evt.User.ID}") .WithCurrentTimestamp(); - var guildConfig = await db.GetGuildAsync(evt.GuildID, false, ct); + var guildConfig = await guildRepository.GetAsync(evt.GuildID); var member = await memberCache.TryGetAsync(evt.GuildID, evt.User.ID); if (member == null) diff --git a/Catalogger.Backend/Bot/Responders/Members/GuildMemberUpdateResponder.cs b/Catalogger.Backend/Bot/Responders/Members/GuildMemberUpdateResponder.cs index 96a5a92..a4dac29 100644 --- a/Catalogger.Backend/Bot/Responders/Members/GuildMemberUpdateResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Members/GuildMemberUpdateResponder.cs @@ -16,6 +16,7 @@ using Catalogger.Backend.Cache; using Catalogger.Backend.Cache.InMemoryCache; using Catalogger.Backend.Database; +using Catalogger.Backend.Database.Dapper.Repositories; using Catalogger.Backend.Database.Queries; using Catalogger.Backend.Extensions; using Catalogger.Backend.Services; @@ -30,7 +31,7 @@ namespace Catalogger.Backend.Bot.Responders.Members; public class GuildMemberUpdateResponder( ILogger logger, - DatabaseContext db, + GuildRepository guildRepository, UserCache userCache, RoleCache roleCache, IMemberCache memberCache, @@ -145,7 +146,7 @@ public class GuildMemberUpdateResponder( .GetOrThrow(); } - var guildConfig = await db.GetGuildAsync(newMember.GuildID, false, ct); + var guildConfig = await guildRepository.GetAsync(newMember.GuildID); webhookExecutor.QueueLog(guildConfig, LogChannelType.GuildMemberAvatarUpdate, embed); return Result.Success; } @@ -204,7 +205,7 @@ public class GuildMemberUpdateResponder( ); } - var guildConfig = await db.GetGuildAsync(newMember.GuildID, false, ct); + var guildConfig = await guildRepository.GetAsync(newMember.GuildID); webhookExecutor.QueueLog( guildConfig, LogChannelType.GuildMemberNickUpdate, @@ -253,7 +254,7 @@ public class GuildMemberUpdateResponder( embed.AddField("Reason", "*(unknown)*"); } - var guildConfig = await db.GetGuildAsync(member.GuildID, false, ct); + var guildConfig = await guildRepository.GetAsync(member.GuildID); webhookExecutor.QueueLog( guildConfig, LogChannelType.GuildMemberTimeout, @@ -268,7 +269,7 @@ public class GuildMemberUpdateResponder( CancellationToken ct = default ) { - var guildConfig = await db.GetGuildAsync(member.GuildID, false, ct); + var guildConfig = await guildRepository.GetAsync(member.GuildID); var guildRoles = roleCache.GuildRoles(member.GuildID).ToList(); var keyRoleUpdate = new EmbedBuilder() diff --git a/Catalogger.Backend/Bot/Responders/Messages/MessageCreateResponder.cs b/Catalogger.Backend/Bot/Responders/Messages/MessageCreateResponder.cs index 2fe0789..c83e9b1 100644 --- a/Catalogger.Backend/Bot/Responders/Messages/MessageCreateResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Messages/MessageCreateResponder.cs @@ -15,9 +15,7 @@ using System.Text.RegularExpressions; using Catalogger.Backend.Cache.InMemoryCache; -using Catalogger.Backend.Database; -using Catalogger.Backend.Database.Models; -using Catalogger.Backend.Database.Queries; +using Catalogger.Backend.Database.Dapper.Repositories; using Catalogger.Backend.Extensions; using Catalogger.Backend.Services; using Humanizer; @@ -30,8 +28,8 @@ namespace Catalogger.Backend.Bot.Responders.Messages; public class MessageCreateResponder( ILogger logger, Config config, - DatabaseContext db, - MessageRepository messageRepository, + GuildRepository guildRepository, + DapperMessageRepository messageRepository, UserCache userCache, PkMessageHandler pkMessageHandler ) : IResponder @@ -52,13 +50,12 @@ public class MessageCreateResponder( return Result.Success; } - var guild = await db.GetGuildAsync(msg.GuildID, false, ct); + var guild = await guildRepository.GetAsync(msg.GuildID); // The guild needs to have enabled at least one of the message logging events, // and the channel must not be ignored, to store the message. if (guild.IsMessageIgnored(msg.ChannelID, msg.Author.ID)) { - db.IgnoredMessages.Add(new IgnoredMessage(msg.ID.ToUlong())); - await db.SaveChangesAsync(ct); + await messageRepository.IgnoreMessageAsync(msg.ID.Value); return Result.Success; } @@ -68,8 +65,7 @@ public class MessageCreateResponder( _ = pkMessageHandler.HandleProxiedMessageAsync(msg.ID.Value); else if (msg.ApplicationID.HasValue && msg.ApplicationID.Is(config.Discord.ApplicationId)) { - db.IgnoredMessages.Add(new IgnoredMessage(msg.ID.Value)); - await db.SaveChangesAsync(ct); + await messageRepository.IgnoreMessageAsync(msg.ID.Value); return Result.Success; } @@ -149,19 +145,19 @@ public partial class PkMessageHandler(ILogger logger, IServiceProvider services) } await using var scope = services.CreateAsyncScope(); - await using var db = scope.ServiceProvider.GetRequiredService(); - var messageRepository = scope.ServiceProvider.GetRequiredService(); + await using var messageRepository = + scope.ServiceProvider.GetRequiredService(); - await messageRepository.SetProxiedMessageDataAsync( - msgId, - originalId, - authorId, - systemId: match.Groups[1].Value, - memberId: match.Groups[2].Value + await Task.WhenAll( + messageRepository.SetProxiedMessageDataAsync( + msgId, + originalId, + authorId, + systemId: match.Groups[1].Value, + memberId: match.Groups[2].Value + ), + messageRepository.IgnoreMessageAsync(originalId) ); - - db.IgnoredMessages.Add(new IgnoredMessage(originalId)); - await db.SaveChangesAsync(); } public async Task HandleProxiedMessageAsync(ulong msgId) @@ -169,8 +165,8 @@ public partial class PkMessageHandler(ILogger logger, IServiceProvider services) await Task.Delay(3.Seconds()); await using var scope = services.CreateAsyncScope(); - await using var db = scope.ServiceProvider.GetRequiredService(); - var messageRepository = scope.ServiceProvider.GetRequiredService(); + await using var messageRepository = + scope.ServiceProvider.GetRequiredService(); var pluralkitApi = scope.ServiceProvider.GetRequiredService(); var (isStored, hasProxyInfo) = await messageRepository.HasProxyInfoAsync(msgId); @@ -193,15 +189,15 @@ public partial class PkMessageHandler(ILogger logger, IServiceProvider services) return; } - await messageRepository.SetProxiedMessageDataAsync( - msgId, - pkMessage.Original, - pkMessage.Sender, - pkMessage.System?.Id, - pkMessage.Member?.Id + await Task.WhenAll( + messageRepository.SetProxiedMessageDataAsync( + msgId, + pkMessage.Original, + pkMessage.Sender, + pkMessage.System?.Id, + pkMessage.Member?.Id + ), + messageRepository.IgnoreMessageAsync(pkMessage.Original) ); - - db.IgnoredMessages.Add(new IgnoredMessage(pkMessage.Original)); - await db.SaveChangesAsync(); } } diff --git a/Catalogger.Backend/Bot/Responders/Messages/MessageDeleteBulkResponder.cs b/Catalogger.Backend/Bot/Responders/Messages/MessageDeleteBulkResponder.cs index 903fdba..a986113 100644 --- a/Catalogger.Backend/Bot/Responders/Messages/MessageDeleteBulkResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Messages/MessageDeleteBulkResponder.cs @@ -15,8 +15,7 @@ using System.Text; using Catalogger.Backend.Cache.InMemoryCache; -using Catalogger.Backend.Database; -using Catalogger.Backend.Database.Queries; +using Catalogger.Backend.Database.Dapper.Repositories; using Catalogger.Backend.Extensions; using Catalogger.Backend.Services; using NodaTime.Extensions; @@ -32,8 +31,8 @@ namespace Catalogger.Backend.Bot.Responders.Messages; public class MessageDeleteBulkResponder( ILogger logger, - DatabaseContext db, - MessageRepository messageRepository, + GuildRepository guildRepository, + DapperMessageRepository messageRepository, WebhookExecutorService webhookExecutor, ChannelCache channelCache ) : IResponder @@ -42,7 +41,7 @@ public class MessageDeleteBulkResponder( public async Task RespondAsync(IMessageDeleteBulk evt, CancellationToken ct = default) { - var guild = await db.GetGuildAsync(evt.GuildID, false, ct); + var guild = await guildRepository.GetAsync(evt.GuildID); if (guild.IsMessageIgnored(evt.ChannelID, null)) return Result.Success; @@ -77,7 +76,7 @@ public class MessageDeleteBulkResponder( foreach (var msgId in evt.IDs.Order()) { - if (await messageRepository.IsMessageIgnoredAsync(msgId.Value, ct)) + if (await messageRepository.IsMessageIgnoredAsync(msgId.Value)) { ignoredMessages++; continue; @@ -129,7 +128,7 @@ public class MessageDeleteBulkResponder( return Result.Success; } - private string RenderMessage(Snowflake messageId, MessageRepository.Message? message) + private string RenderMessage(Snowflake messageId, DapperMessageRepository.Message? message) { var timestamp = messageId.Timestamp.ToOffsetDateTime().ToString(); diff --git a/Catalogger.Backend/Bot/Responders/Messages/MessageDeleteResponder.cs b/Catalogger.Backend/Bot/Responders/Messages/MessageDeleteResponder.cs index 21b027d..f538995 100644 --- a/Catalogger.Backend/Bot/Responders/Messages/MessageDeleteResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Messages/MessageDeleteResponder.cs @@ -14,8 +14,7 @@ // along with this program. If not, see . using Catalogger.Backend.Cache.InMemoryCache; -using Catalogger.Backend.Database; -using Catalogger.Backend.Database.Queries; +using Catalogger.Backend.Database.Dapper.Repositories; using Catalogger.Backend.Extensions; using Catalogger.Backend.Services; using Humanizer; @@ -33,8 +32,8 @@ namespace Catalogger.Backend.Bot.Responders.Messages; public class MessageDeleteResponder( ILogger logger, - DatabaseContext db, - MessageRepository messageRepository, + GuildRepository guildRepository, + DapperMessageRepository messageRepository, WebhookExecutorService webhookExecutor, ChannelCache channelCache, UserCache userCache, @@ -61,10 +60,10 @@ public class MessageDeleteResponder( await Task.Delay(5.Seconds(), ct); } - if (await messageRepository.IsMessageIgnoredAsync(evt.ID.Value, ct)) + if (await messageRepository.IsMessageIgnoredAsync(evt.ID.Value)) return Result.Success; - var guild = await db.GetGuildAsync(evt.GuildID, false, ct); + var guild = await guildRepository.GetAsync(evt.GuildID); if (guild.IsMessageIgnored(evt.ChannelID, evt.ID)) return Result.Success; diff --git a/Catalogger.Backend/Bot/Responders/Messages/MessageUpdateResponder.cs b/Catalogger.Backend/Bot/Responders/Messages/MessageUpdateResponder.cs index 8287620..8933c06 100644 --- a/Catalogger.Backend/Bot/Responders/Messages/MessageUpdateResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Messages/MessageUpdateResponder.cs @@ -15,6 +15,7 @@ using Catalogger.Backend.Cache.InMemoryCache; using Catalogger.Backend.Database; +using Catalogger.Backend.Database.Dapper.Repositories; using Catalogger.Backend.Database.Queries; using Catalogger.Backend.Extensions; using Catalogger.Backend.Services; @@ -34,7 +35,7 @@ public class MessageUpdateResponder( DatabaseContext db, ChannelCache channelCache, UserCache userCache, - MessageRepository messageRepository, + DapperMessageRepository messageRepository, WebhookExecutorService webhookExecutor, PluralkitApiService pluralkitApi ) : IResponder @@ -58,7 +59,7 @@ public class MessageUpdateResponder( var guildConfig = await db.GetGuildAsync(msg.GuildID.Value, false, ct); - if (await messageRepository.IsMessageIgnoredAsync(msg.ID.Value, ct)) + if (await messageRepository.IsMessageIgnoredAsync(msg.ID.Value)) { _logger.Debug("Message {MessageId} should be ignored", msg.ID); return Result.Success; @@ -176,7 +177,7 @@ public class MessageUpdateResponder( ) { if ( - !await messageRepository.UpdateMessageAsync(msg, ct) + !await messageRepository.SaveMessageAsync(msg, ct) && msg.ApplicationID.Is(DiscordUtils.PkUserId) ) { diff --git a/Catalogger.Backend/Bot/Responders/Roles/RoleCreateResponder.cs b/Catalogger.Backend/Bot/Responders/Roles/RoleCreateResponder.cs index 2933f60..6dc6374 100644 --- a/Catalogger.Backend/Bot/Responders/Roles/RoleCreateResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Roles/RoleCreateResponder.cs @@ -15,6 +15,7 @@ using Catalogger.Backend.Cache.InMemoryCache; using Catalogger.Backend.Database; +using Catalogger.Backend.Database.Dapper.Repositories; using Catalogger.Backend.Database.Queries; using Catalogger.Backend.Extensions; using Catalogger.Backend.Services; @@ -27,7 +28,7 @@ namespace Catalogger.Backend.Bot.Responders.Roles; public class RoleCreateResponder( ILogger logger, - DatabaseContext db, + GuildRepository guildRepository, RoleCache roleCache, WebhookExecutorService webhookExecutor ) : IResponder @@ -39,7 +40,7 @@ public class RoleCreateResponder( _logger.Debug("Received new role {RoleId} in guild {GuildId}", evt.Role.ID, evt.GuildID); roleCache.Set(evt.Role, evt.GuildID); - var guildConfig = await db.GetGuildAsync(evt.GuildID, false, ct); + var guildConfig = await guildRepository.GetAsync(evt.GuildID); var embed = new EmbedBuilder() .WithTitle("Role created") diff --git a/Catalogger.Backend/Bot/Responders/Roles/RoleDeleteResponder.cs b/Catalogger.Backend/Bot/Responders/Roles/RoleDeleteResponder.cs index 48baef8..303f7dd 100644 --- a/Catalogger.Backend/Bot/Responders/Roles/RoleDeleteResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Roles/RoleDeleteResponder.cs @@ -14,8 +14,7 @@ // along with this program. If not, see . using Catalogger.Backend.Cache.InMemoryCache; -using Catalogger.Backend.Database; -using Catalogger.Backend.Database.Queries; +using Catalogger.Backend.Database.Dapper.Repositories; using Catalogger.Backend.Extensions; using Catalogger.Backend.Services; using Remora.Discord.API.Abstractions.Gateway.Events; @@ -27,7 +26,7 @@ namespace Catalogger.Backend.Bot.Responders.Roles; public class RoleDeleteResponder( ILogger logger, - DatabaseContext db, + GuildRepository guildRepository, RoleCache roleCache, WebhookExecutorService webhookExecutor ) : IResponder @@ -47,7 +46,7 @@ public class RoleDeleteResponder( return Result.Success; } - var guildConfig = await db.GetGuildAsync(evt.GuildID, false, ct); + var guildConfig = await guildRepository.GetAsync(evt.GuildID); var embed = new EmbedBuilder() .WithTitle($"Role \"{role.Name}\" deleted") diff --git a/Catalogger.Backend/Bot/Responders/Roles/RoleUpdateResponder.cs b/Catalogger.Backend/Bot/Responders/Roles/RoleUpdateResponder.cs index 68942e7..c6d142b 100644 --- a/Catalogger.Backend/Bot/Responders/Roles/RoleUpdateResponder.cs +++ b/Catalogger.Backend/Bot/Responders/Roles/RoleUpdateResponder.cs @@ -14,8 +14,7 @@ // along with this program. If not, see . using Catalogger.Backend.Cache.InMemoryCache; -using Catalogger.Backend.Database; -using Catalogger.Backend.Database.Queries; +using Catalogger.Backend.Database.Dapper.Repositories; using Catalogger.Backend.Extensions; using Catalogger.Backend.Services; using Humanizer; @@ -29,7 +28,7 @@ namespace Catalogger.Backend.Bot.Responders.Roles; public class RoleUpdateResponder( ILogger logger, - DatabaseContext db, + GuildRepository guildRepository, RoleCache roleCache, WebhookExecutorService webhookExecutor ) : IResponder @@ -95,7 +94,7 @@ public class RoleUpdateResponder( if (embed.Fields.Count == 0) return Result.Success; - var guildConfig = await db.GetGuildAsync(evt.GuildID, false, ct); + var guildConfig = await guildRepository.GetAsync(evt.GuildID); webhookExecutor.QueueLog( guildConfig, LogChannelType.GuildRoleUpdate, diff --git a/Catalogger.Backend/Catalogger.Backend.csproj b/Catalogger.Backend/Catalogger.Backend.csproj index 5f44fe6..be55129 100644 --- a/Catalogger.Backend/Catalogger.Backend.csproj +++ b/Catalogger.Backend/Catalogger.Backend.csproj @@ -8,6 +8,7 @@ + @@ -22,8 +23,10 @@ + + diff --git a/Catalogger.Backend/Database/Dapper/DatabaseConnection.cs b/Catalogger.Backend/Database/Dapper/DatabaseConnection.cs new file mode 100644 index 0000000..997aab5 --- /dev/null +++ b/Catalogger.Backend/Database/Dapper/DatabaseConnection.cs @@ -0,0 +1,96 @@ +// Copyright (C) 2021-present sam (starshines.gay) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +using System.Data; +using System.Data.Common; +using System.Diagnostics.CodeAnalysis; +using Npgsql; + +namespace Catalogger.Backend.Database.Dapper; + +public class DatabaseConnection(Guid id, ILogger logger, NpgsqlConnection inner) + : DbConnection, + IDisposable +{ + public Guid ConnectionId => id; + private readonly ILogger _logger = logger.ForContext(); + private readonly DateTimeOffset _openTime = DateTimeOffset.UtcNow; + + private bool _hasClosed; + + public override async Task OpenAsync(CancellationToken cancellationToken) => + await inner.OpenAsync(cancellationToken); + + public override async Task CloseAsync() + { + if (_hasClosed) + { + await inner.CloseAsync(); + return; + } + + DatabasePool.DecrementConnections(); + var openFor = DateTimeOffset.UtcNow - _openTime; + _logger.Debug("Closing connection {ConnId}, open for {OpenFor}", ConnectionId, openFor); + _hasClosed = true; + await inner.CloseAsync(); + } + + protected override async ValueTask BeginDbTransactionAsync( + IsolationLevel isolationLevel, + CancellationToken cancellationToken + ) + { + _logger.Debug("Beginning transaction on connection {ConnId}", ConnectionId); + return await inner.BeginTransactionAsync(isolationLevel, cancellationToken); + } + + public new void Dispose() + { + Close(); + inner.Dispose(); + GC.SuppressFinalize(this); + } + + public override async ValueTask DisposeAsync() + { + await CloseAsync(); + await inner.DisposeAsync(); + GC.SuppressFinalize(this); + } + + protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLevel) => + inner.BeginTransaction(isolationLevel); + + public override void ChangeDatabase(string databaseName) => inner.ChangeDatabase(databaseName); + + public override void Close() => inner.Close(); + + public override void Open() => inner.Open(); + + [AllowNull] + public override string ConnectionString + { + get => inner.ConnectionString; + set => inner.ConnectionString = value; + } + + public override string Database => inner.Database; + public override ConnectionState State => inner.State; + public override string DataSource => inner.DataSource; + public override string ServerVersion => inner.ServerVersion; + + protected override DbCommand CreateDbCommand() => inner.CreateCommand(); +} diff --git a/Catalogger.Backend/Database/Dapper/DatabasePool.cs b/Catalogger.Backend/Database/Dapper/DatabasePool.cs new file mode 100644 index 0000000..7c7bf35 --- /dev/null +++ b/Catalogger.Backend/Database/Dapper/DatabasePool.cs @@ -0,0 +1,157 @@ +// Copyright (C) 2021-present sam (starshines.gay) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +using System.Data; +using Dapper; +using NodaTime; +using Npgsql; + +namespace Catalogger.Backend.Database.Dapper; + +public class DatabasePool +{ + private readonly ILogger _rootLogger; + private readonly ILogger _logger; + private readonly NpgsqlDataSource _dataSource; + + private static int _openConnections; + public static int OpenConnections => _openConnections; + + public DatabasePool(Config config, ILogger logger, ILoggerFactory? loggerFactory) + { + _rootLogger = logger; + _logger = logger.ForContext(); + + var connString = new NpgsqlConnectionStringBuilder(config.Database.Url) + { + Timeout = config.Database.Timeout ?? 5, + MaxPoolSize = config.Database.MaxPoolSize ?? 50, + }.ConnectionString; + + var dataSourceBuilder = new NpgsqlDataSourceBuilder(connString); + dataSourceBuilder.EnableDynamicJson().UseNodaTime(); + if (config.Logging.LogQueries) + dataSourceBuilder.UseLoggerFactory(loggerFactory); + _dataSource = dataSourceBuilder.Build(); + } + + public async Task AcquireAsync(CancellationToken ct = default) + { + return new DatabaseConnection( + LogOpen(), + _rootLogger, + await _dataSource.OpenConnectionAsync(ct) + ); + } + + public DatabaseConnection Acquire() + { + return new DatabaseConnection(LogOpen(), _rootLogger, _dataSource.OpenConnection()); + } + + private Guid LogOpen() + { + var connId = Guid.NewGuid(); + _logger.Debug("Opening database connection {ConnId}", connId); + IncrementConnections(); + return connId; + } + + public async Task ExecuteAsync( + Func func, + CancellationToken ct = default + ) + { + await using var conn = await AcquireAsync(ct); + await func(conn); + } + + public async Task ExecuteAsync( + Func> func, + CancellationToken ct = default + ) + { + await using var conn = await AcquireAsync(ct); + return await func(conn); + } + + public async Task> ExecuteAsync( + Func>> func, + CancellationToken ct = default + ) + { + await using var conn = await AcquireAsync(ct); + return await func(conn); + } + + internal static void IncrementConnections() => Interlocked.Increment(ref _openConnections); + + internal static void DecrementConnections() => Interlocked.Decrement(ref _openConnections); + + /// + /// Configures Dapper's SQL mapping, as it handles several types incorrectly by default. + /// Most notably, ulongs and arrays of ulongs. + /// + public static void ConfigureDapper() + { + DefaultTypeMap.MatchNamesWithUnderscores = true; + + SqlMapper.RemoveTypeMap(typeof(ulong)); + SqlMapper.AddTypeHandler(new UlongEncodeAsLongHandler()); + SqlMapper.AddTypeHandler(new UlongArrayHandler()); + + SqlMapper.AddTypeHandler(new PassthroughTypeHandler()); + } + + // Copied from PluralKit: + // https://github.com/PluralKit/PluralKit/blob/4bf60a47d76a068fa0488bf9be96cdaf57a6fe50/PluralKit.Core/Database/Database.cs#L116 + // Thanks for not working with common types by default, Dapper. Really nice of you. + private class PassthroughTypeHandler : SqlMapper.TypeHandler + { + public override void SetValue(IDbDataParameter parameter, T? value) => + parameter.Value = value; + + public override T Parse(object value) => (T)value; + } + + private class UlongEncodeAsLongHandler : SqlMapper.TypeHandler + { + public override ulong Parse(object value) => + // Cast to long to unbox, then to ulong (???) + (ulong)(long)value; + + public override void SetValue(IDbDataParameter parameter, ulong value) => + parameter.Value = (long)value; + } + + private class UlongArrayHandler : SqlMapper.TypeHandler + { + public override void SetValue(IDbDataParameter parameter, ulong[]? value) => + parameter.Value = value != null ? Array.ConvertAll(value, i => (long)i) : null; + + public override ulong[] Parse(object value) => + Array.ConvertAll((long[])value, i => (ulong)i); + } +} + +public static class ServiceCollectionDatabaseExtensions +{ + public static IServiceCollection AddDatabasePool(this IServiceCollection serviceCollection) => + serviceCollection + .AddSingleton() + .AddScoped(services => + services.GetRequiredService().Acquire() + ); +} diff --git a/Catalogger.Backend/Database/Dapper/Repositories/DapperMessageRepository.cs b/Catalogger.Backend/Database/Dapper/Repositories/DapperMessageRepository.cs new file mode 100644 index 0000000..c006f96 --- /dev/null +++ b/Catalogger.Backend/Database/Dapper/Repositories/DapperMessageRepository.cs @@ -0,0 +1,225 @@ +// Copyright (C) 2021-present sam (starshines.gay) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +using System.Text.Json; +using Catalogger.Backend.Extensions; +using Dapper; +using Remora.Discord.API; +using Remora.Discord.API.Abstractions.Gateway.Events; +using Remora.Rest.Core; + +namespace Catalogger.Backend.Database.Dapper.Repositories; + +public class DapperMessageRepository( + ILogger logger, + DatabaseConnection conn, + IEncryptionService encryptionService +) : IDisposable, IAsyncDisposable +{ + private readonly ILogger _logger = logger.ForContext(); + + public async Task GetMessageAsync(ulong id, CancellationToken ct = default) + { + _logger.Debug("Retrieving message {MessageId}", id); + + var dbMsg = await conn.QueryFirstOrDefaultAsync( + "select * from messages where id = @Id", + new { Id = id } + ); + if (dbMsg == null) + return null; + + return new Message( + dbMsg.Id, + dbMsg.OriginalId, + dbMsg.UserId, + dbMsg.ChannelId, + dbMsg.GuildId, + dbMsg.Member, + dbMsg.System, + Username: await Task.Run(() => encryptionService.Decrypt(dbMsg.Username), ct), + Content: await Task.Run(() => encryptionService.Decrypt(dbMsg.Content), ct), + Metadata: dbMsg.Metadata != null + ? JsonSerializer.Deserialize( + await Task.Run(() => encryptionService.Decrypt(dbMsg.Metadata), ct) + ) + : null, + dbMsg.AttachmentSize + ); + } + + /// + /// Adds a new message. If the message is already in the database, updates the existing message instead. + /// + public async Task SaveMessageAsync(IMessageCreate msg, CancellationToken ct = default) + { + var content = await Task.Run( + () => + encryptionService.Encrypt( + string.IsNullOrWhiteSpace(msg.Content) ? "None" : msg.Content + ), + ct + ); + var username = await Task.Run(() => encryptionService.Encrypt(msg.Author.Tag()), ct); + var metadata = await Task.Run( + () => + encryptionService.Encrypt( + JsonSerializer.Serialize( + new Metadata( + IsWebhook: msg.WebhookID.HasValue, + msg.Attachments.Select(a => new Attachment( + a.Filename, + a.Size, + a.ContentType.Value + )) + ) + ) + ), + ct + ); + + // MessageUpdateResponder wants to know whether the message already existed, so query this *before* inserting. + var exists = await conn.ExecuteScalarAsync( + "select exists(select id from messages where id = @Id)", + new { Id = msg.ID.Value } + ); + + await conn.ExecuteAsync( + """ + insert into messages (id, user_id, channel_id, guild_id, username, content, metadata, attachment_size) + values (@Id, @UserId, @ChannelId, @GuildId, @Username, @Content, @Metadata, @AttachmentSize) + on conflict (id) do update set username = @Username, content = @Content, metadata = @Metadata + """, + new + { + Id = msg.ID.Value, + UserId = msg.Author.ID.Value, + ChannelId = msg.ChannelID.Value, + GuildId = msg.GuildID.Map(s => s.Value).OrDefault(), + Content = content, + Username = username, + Metadata = metadata, + AttachmentSize = msg.Attachments.Select(a => a.Size).Sum(), + } + ); + + return exists; + } + + public async Task<(bool IsStored, bool HasProxyInfo)> HasProxyInfoAsync(ulong id) + { + _logger.Debug("Checking if message {MessageId} has proxy information", id); + + var msg = await conn.QueryFirstOrDefaultAsync<(ulong Id, ulong OriginalId)>( + "select id, original_id from messages where id = @Id", + new { Id = id } + ); + return (msg.Id != 0, msg.OriginalId != 0); + } + + /// + /// Updates a stored message with PluralKit information. + /// + /// True if the message exists and was updated, false if it doesn't exist. + public async Task SetProxiedMessageDataAsync( + ulong id, + ulong originalId, + ulong authorId, + string? systemId, + string? memberId + ) + { + _logger.Debug("Setting proxy information for message {MessageId}", id); + + var updatedCount = await conn.ExecuteAsync( + "update messages set original_id = @OriginalId, user_id = @AuthorId, system = @SystemId, member = @MemberId where id = @Id", + new + { + Id = id, + OriginalId = originalId, + AuthorId = authorId, + SystemId = systemId, + MemberId = memberId, + } + ); + if (updatedCount == 0) + { + _logger.Debug("Message {MessageId} not found, can't set proxy data for it", id); + return false; + } + return true; + } + + public async Task IsMessageIgnoredAsync(ulong id) => + await conn.ExecuteScalarAsync( + "select exists(select id from messages where id = @Id)", + new { Id = id } + ); + + public const int MaxMessageAgeDays = 15; + + public async Task<(int Messages, int IgnoredMessages)> DeleteExpiredMessagesAsync() + { + var cutoff = DateTimeOffset.UtcNow - TimeSpan.FromDays(MaxMessageAgeDays); + var cutoffId = Snowflake.CreateTimestampSnowflake(cutoff, Constants.DiscordEpoch).Value; + + var msgCount = await conn.ExecuteAsync( + "delete from messages where id < @Cutoff", + new { Cutoff = cutoffId } + ); + var ignoredMsgCount = await conn.ExecuteAsync( + "delete from ignored_messages where id < @Cutoff", + new { Cutoff = cutoffId } + ); + + return (msgCount, ignoredMsgCount); + } + + public async Task IgnoreMessageAsync(ulong id) => + await conn.ExecuteAsync( + "insert into ignored_messages (id) values (@Id) on conflict do nothing", + new { Id = id } + ); + + public record Message( + ulong Id, + ulong? OriginalId, + ulong UserId, + ulong ChannelId, + ulong GuildId, + string? Member, + string? System, + string Username, + string Content, + Metadata? Metadata, + int AttachmentSize + ); + + public record Metadata(bool IsWebhook, IEnumerable Attachments); + + public record Attachment(string Filename, int Size, string ContentType); + + public void Dispose() + { + conn.Dispose(); + GC.SuppressFinalize(this); + } + + public async ValueTask DisposeAsync() + { + await conn.DisposeAsync(); + GC.SuppressFinalize(this); + } +} diff --git a/Catalogger.Backend/Database/Dapper/Repositories/GuildRepository.cs b/Catalogger.Backend/Database/Dapper/Repositories/GuildRepository.cs new file mode 100644 index 0000000..5a1b664 --- /dev/null +++ b/Catalogger.Backend/Database/Dapper/Repositories/GuildRepository.cs @@ -0,0 +1,89 @@ +// Copyright (C) 2021-present sam (starshines.gay) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +using Catalogger.Backend.Database.Models; +using Dapper; +using Remora.Rest.Core; + +namespace Catalogger.Backend.Database.Dapper.Repositories; + +public class GuildRepository(ILogger logger, DatabaseConnection conn) + : IDisposable, + IAsyncDisposable +{ + private readonly ILogger _logger = logger.ForContext(); + + public async Task GetAsync(Optional id) => await GetAsync(id.Value.Value); + + public async Task GetAsync(Snowflake id) => await GetAsync(id.Value); + + public async Task GetAsync(ulong id) + { + _logger.Debug("Getting guild config for {GuildId}", id); + + var guild = await conn.QueryFirstOrDefaultAsync( + "select * from guilds where id = @Id", + new { Id = id } + ); + if (guild == null) + throw new CataloggerError("Guild not found, was not initialized during guild create"); + return guild; + } + + public async Task IsGuildKnown(ulong id) => + await conn.ExecuteScalarAsync( + "select exists(select id from guilds where id = @Id)", + new { Id = id } + ); + + public async Task AddGuildAsync(ulong id) => + await conn.ExecuteAsync( + """ + insert into guilds (id, key_roles, banned_systems, key_roles, channels) + values (@Id, array[]::bigint[], array[]::text[], array[]::bigint[], @Channels) + on conflict do nothing + """, + new { Id = id, Channels = new Guild.ChannelConfig() } + ); + + public async Task BanSystemAsync(Snowflake guildId, string hid, Guid uuid) => + await conn.ExecuteAsync( + "update guilds set banned_systems = array_cat(banned_systems, @SystemIds) where id = @GuildId", + new { GuildId = guildId.Value, SystemIds = (string[])[hid, uuid.ToString()] } + ); + + public async Task UnbanSystemAsync(Snowflake guildId, string hid, Guid uuid) => + await conn.ExecuteAsync( + "update guilds set banned_systems = array_remove(array_remove(banned_systems, @Hid), @Uuid) where id = @Id", + new + { + GuildId = guildId.Value, + Hid = hid, + Uuid = uuid.ToString(), + } + ); + + public void Dispose() + { + conn.Dispose(); + GC.SuppressFinalize(this); + } + + public async ValueTask DisposeAsync() + { + await conn.DisposeAsync(); + GC.SuppressFinalize(this); + } +} diff --git a/Catalogger.Backend/Database/Models/Message.cs b/Catalogger.Backend/Database/Models/Message.cs index 29bf139..2bbb658 100644 --- a/Catalogger.Backend/Database/Models/Message.cs +++ b/Catalogger.Backend/Database/Models/Message.cs @@ -30,14 +30,11 @@ public class Message public string? Member { get; set; } public string? System { get; set; } - [Column("username")] - public byte[] EncryptedUsername { get; set; } = []; + public byte[] Username { get; set; } = []; - [Column("content")] - public byte[] EncryptedContent { get; set; } = []; + public byte[] Content { get; set; } = []; - [Column("metadata")] - public byte[]? EncryptedMetadata { get; set; } + public byte[]? Metadata { get; set; } public int AttachmentSize { get; set; } = 0; } diff --git a/Catalogger.Backend/Database/Queries/MessageRepository.cs b/Catalogger.Backend/Database/Queries/MessageRepository.cs index e8bb338..6f88bb5 100644 --- a/Catalogger.Backend/Database/Queries/MessageRepository.cs +++ b/Catalogger.Backend/Database/Queries/MessageRepository.cs @@ -47,18 +47,15 @@ public class MessageRepository( ChannelId = msg.ChannelID.ToUlong(), GuildId = msg.GuildID.ToUlong(), - EncryptedContent = await Task.Run( + Content = await Task.Run( () => encryptionService.Encrypt( string.IsNullOrWhiteSpace(msg.Content) ? "None" : msg.Content ), ct ), - EncryptedUsername = await Task.Run( - () => encryptionService.Encrypt(msg.Author.Tag()), - ct - ), - EncryptedMetadata = await Task.Run( + Username = await Task.Run(() => encryptionService.Encrypt(msg.Author.Tag()), ct), + Metadata = await Task.Run( () => encryptionService.Encrypt(JsonSerializer.Serialize(metadata)), ct ), @@ -103,18 +100,15 @@ public class MessageRepository( "Message was null despite HasProxyInfoAsync returning true" ); - dbMsg.EncryptedContent = await Task.Run( + dbMsg.Content = await Task.Run( () => encryptionService.Encrypt( string.IsNullOrWhiteSpace(msg.Content) ? "None" : msg.Content ), ct ); - dbMsg.EncryptedUsername = await Task.Run( - () => encryptionService.Encrypt(msg.Author.Tag()), - ct - ); - dbMsg.EncryptedMetadata = await Task.Run( + dbMsg.Username = await Task.Run(() => encryptionService.Encrypt(msg.Author.Tag()), ct); + dbMsg.Metadata = await Task.Run( () => encryptionService.Encrypt(JsonSerializer.Serialize(metadata)), ct ); @@ -142,11 +136,11 @@ public class MessageRepository( dbMsg.GuildId, dbMsg.Member, dbMsg.System, - Username: await Task.Run(() => encryptionService.Decrypt(dbMsg.EncryptedUsername), ct), - Content: await Task.Run(() => encryptionService.Decrypt(dbMsg.EncryptedContent), ct), - Metadata: dbMsg.EncryptedMetadata != null + Username: await Task.Run(() => encryptionService.Decrypt(dbMsg.Username), ct), + Content: await Task.Run(() => encryptionService.Decrypt(dbMsg.Content), ct), + Metadata: dbMsg.Metadata != null ? JsonSerializer.Deserialize( - await Task.Run(() => encryptionService.Decrypt(dbMsg.EncryptedMetadata), ct) + await Task.Run(() => encryptionService.Decrypt(dbMsg.Metadata), ct) ) : null, dbMsg.AttachmentSize diff --git a/Catalogger.Backend/Extensions/StartupExtensions.cs b/Catalogger.Backend/Extensions/StartupExtensions.cs index 621cb46..e6577c3 100644 --- a/Catalogger.Backend/Extensions/StartupExtensions.cs +++ b/Catalogger.Backend/Extensions/StartupExtensions.cs @@ -22,6 +22,8 @@ using Catalogger.Backend.Cache; using Catalogger.Backend.Cache.InMemoryCache; using Catalogger.Backend.Cache.RedisCache; using Catalogger.Backend.Database; +using Catalogger.Backend.Database.Dapper; +using Catalogger.Backend.Database.Dapper.Repositories; using Catalogger.Backend.Database.Queries; using Catalogger.Backend.Database.Redis; using Catalogger.Backend.Services; @@ -103,6 +105,9 @@ public static class StartupExtensions public static IServiceCollection AddCustomServices(this IServiceCollection services) => services .AddSingleton(SystemClock.Instance) + .AddDatabasePool() + .AddScoped() + .AddScoped() .AddSingleton() .AddSingleton() .AddSingleton() @@ -113,7 +118,7 @@ public static class StartupExtensions .AddSingleton() .AddScoped() .AddSingleton() - .AddScoped() + // .AddScoped() .AddSingleton() .AddSingleton() .AddSingleton(InMemoryDataService.Instance) @@ -189,6 +194,8 @@ public static class StartupExtensions .ServiceProvider.GetRequiredService() .GetCurrentInstant(); + DatabasePool.ConfigureDapper(); + await using (var db = scope.ServiceProvider.GetRequiredService()) { var migrationCount = (await db.Database.GetPendingMigrationsAsync()).Count(); diff --git a/Catalogger.Backend/Services/BackgroundTasksService.cs b/Catalogger.Backend/Services/BackgroundTasksService.cs index 2930d00..6efb6a2 100644 --- a/Catalogger.Backend/Services/BackgroundTasksService.cs +++ b/Catalogger.Backend/Services/BackgroundTasksService.cs @@ -13,7 +13,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -using Catalogger.Backend.Database; +using Catalogger.Backend.Database.Dapper.Repositories; using Catalogger.Backend.Database.Queries; namespace Catalogger.Backend.Services; @@ -34,7 +34,8 @@ public class BackgroundTasksService(ILogger logger, IServiceProvider services) : _logger.Information("Running once per minute periodic tasks"); await using var scope = services.CreateAsyncScope(); - var messageRepository = scope.ServiceProvider.GetRequiredService(); + await using var messageRepository = + scope.ServiceProvider.GetRequiredService(); var (msgCount, ignoredCount) = await messageRepository.DeleteExpiredMessagesAsync(); if (msgCount != 0 || ignoredCount != 0) @@ -43,7 +44,7 @@ public class BackgroundTasksService(ILogger logger, IServiceProvider services) : "Deleted {Count} messages and {IgnoredCount} ignored message IDs older than {MaxDays} days old", msgCount, ignoredCount, - MessageRepository.MaxMessageAgeDays + DapperMessageRepository.MaxMessageAgeDays ); } } diff --git a/Catalogger.Backend/packages.lock.json b/Catalogger.Backend/packages.lock.json index 95d3365..cfc0acc 100644 --- a/Catalogger.Backend/packages.lock.json +++ b/Catalogger.Backend/packages.lock.json @@ -2,6 +2,12 @@ "version": 1, "dependencies": { "net8.0": { + "Dapper": { + "type": "Direct", + "requested": "[2.1.35, )", + "resolved": "2.1.35", + "contentHash": "YKRwjVfrG7GYOovlGyQoMvr1/IJdn+7QzNXJxyMh0YfFF5yvDmTYaJOVYWsckreNjGsGSEtrMTpnzxTUq/tZQw==" + }, "EFCore.NamingConventions": { "type": "Direct", "requested": "[8.0.3, )", @@ -108,6 +114,15 @@ "NodaTime": "[3.0.0, 4.0.0)" } }, + "Npgsql": { + "type": "Direct", + "requested": "[8.0.5, )", + "resolved": "8.0.5", + "contentHash": "zRG5V8cyeZLpzJlKzFKjEwkRMYIYnHWJvEor2lWXeccS2E1G2nIWYYhnukB51iz5XsWSVEtqg3AxTWM0QJ6vfg==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "8.0.0" + } + }, "Npgsql.EntityFrameworkCore.PostgreSQL": { "type": "Direct", "requested": "[8.0.8, )", @@ -130,6 +145,16 @@ "Npgsql.NodaTime": "8.0.4" } }, + "Npgsql.NodaTime": { + "type": "Direct", + "requested": "[8.0.5, )", + "resolved": "8.0.5", + "contentHash": "oC7Ml5TDuQlcGECB5ML0XsPxFrYu3OdpG7c9cuqhB+xunLvqbZ0zXQoPJjvXK9KDNPDB/II61HNdsNas9f2J3A==", + "dependencies": { + "NodaTime": "3.1.9", + "Npgsql": "8.0.5" + } + }, "Polly.Core": { "type": "Direct", "requested": "[8.4.2, )", @@ -581,23 +606,6 @@ "resolved": "0.6.7", "contentHash": "gT6bf5PVayvTuEIuM2XSNqthrtn9W+LlCX4RD//Nb4hrT3agohHvPdjpROgNGgyXDkjwE74F+EwDwqUgJCJG8A==" }, - "Npgsql": { - "type": "Transitive", - "resolved": "8.0.4", - "contentHash": "vaYEUlF/pB9m8bs21wQv3Da0kMHT4A9USe47VfY/L2BO97xz5KfIxhEu22QS9d68ZrLxvtL3wQDfDLPr2OjbjA==", - "dependencies": { - "Microsoft.Extensions.Logging.Abstractions": "8.0.0" - } - }, - "Npgsql.NodaTime": { - "type": "Transitive", - "resolved": "8.0.4", - "contentHash": "nH4yqdl8zC6kCv0kelWhbx0MGBbo7y4rRsAJLEmc2I7NhbvVgBkflYbaC/F1b64UI1TEqJMzcA36MktDSP0Xbw==", - "dependencies": { - "NodaTime": "3.1.9", - "Npgsql": "8.0.4" - } - }, "OneOf": { "type": "Transitive", "resolved": "3.0.271",