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",