feat: exorcise entity framework core from most responders
This commit is contained in:
parent
33b78a7ac5
commit
5891f28f7c
32 changed files with 743 additions and 145 deletions
|
|
@ -14,8 +14,7 @@
|
|||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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<IChannelDelete>
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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<Result> 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(
|
||||
|
|
|
|||
|
|
@ -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<Result> 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",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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<IGuildEmojisUpdate>
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 +=
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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<IMessageCreate>
|
||||
|
|
@ -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<DatabaseContext>();
|
||||
var messageRepository = scope.ServiceProvider.GetRequiredService<MessageRepository>();
|
||||
await using var messageRepository =
|
||||
scope.ServiceProvider.GetRequiredService<DapperMessageRepository>();
|
||||
|
||||
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<DatabaseContext>();
|
||||
var messageRepository = scope.ServiceProvider.GetRequiredService<MessageRepository>();
|
||||
await using var messageRepository =
|
||||
scope.ServiceProvider.GetRequiredService<DapperMessageRepository>();
|
||||
var pluralkitApi = scope.ServiceProvider.GetRequiredService<PluralkitApiService>();
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<IMessageDeleteBulk>
|
||||
|
|
@ -42,7 +41,7 @@ public class MessageDeleteBulkResponder(
|
|||
|
||||
public async Task<Result> 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();
|
||||
|
||||
|
|
|
|||
|
|
@ -14,8 +14,7 @@
|
|||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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<IMessageUpdate>
|
||||
|
|
@ -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)
|
||||
)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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<IGuildRoleCreate>
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -14,8 +14,7 @@
|
|||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
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<IGuildRoleDelete>
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -14,8 +14,7 @@
|
|||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
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<IGuildRoleUpdate>
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dapper" Version="2.1.35" />
|
||||
<PackageReference Include="EFCore.NamingConventions" Version="8.0.3"/>
|
||||
<PackageReference Include="EntityFrameworkCore.Exceptions.PostgreSQL" Version="8.1.3"/>
|
||||
<PackageReference Include="Humanizer.Core" Version="2.14.1"/>
|
||||
|
|
@ -22,8 +23,10 @@
|
|||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3"/>
|
||||
<PackageReference Include="NodaTime" Version="3.1.12"/>
|
||||
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.2.0"/>
|
||||
<PackageReference Include="Npgsql" Version="8.0.5" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.8"/>
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="8.0.8"/>
|
||||
<PackageReference Include="Npgsql.NodaTime" Version="8.0.5" />
|
||||
<PackageReference Include="Polly.Core" Version="8.4.2"/>
|
||||
<PackageReference Include="Polly.RateLimiting" Version="8.4.2"/>
|
||||
<PackageReference Include="prometheus-net" Version="8.2.1"/>
|
||||
|
|
|
|||
96
Catalogger.Backend/Database/Dapper/DatabaseConnection.cs
Normal file
96
Catalogger.Backend/Database/Dapper/DatabaseConnection.cs
Normal file
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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<DatabaseConnection>();
|
||||
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<DbTransaction> 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();
|
||||
}
|
||||
157
Catalogger.Backend/Database/Dapper/DatabasePool.cs
Normal file
157
Catalogger.Backend/Database/Dapper/DatabasePool.cs
Normal file
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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<DatabasePool>();
|
||||
|
||||
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<DatabaseConnection> 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<DatabaseConnection, Task> func,
|
||||
CancellationToken ct = default
|
||||
)
|
||||
{
|
||||
await using var conn = await AcquireAsync(ct);
|
||||
await func(conn);
|
||||
}
|
||||
|
||||
public async Task<T> ExecuteAsync<T>(
|
||||
Func<DatabaseConnection, Task<T>> func,
|
||||
CancellationToken ct = default
|
||||
)
|
||||
{
|
||||
await using var conn = await AcquireAsync(ct);
|
||||
return await func(conn);
|
||||
}
|
||||
|
||||
public async Task<IAsyncEnumerable<T>> ExecuteAsync<T>(
|
||||
Func<DatabaseConnection, Task<IAsyncEnumerable<T>>> 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);
|
||||
|
||||
/// <summary>
|
||||
/// Configures Dapper's SQL mapping, as it handles several types incorrectly by default.
|
||||
/// Most notably, ulongs and arrays of ulongs.
|
||||
/// </summary>
|
||||
public static void ConfigureDapper()
|
||||
{
|
||||
DefaultTypeMap.MatchNamesWithUnderscores = true;
|
||||
|
||||
SqlMapper.RemoveTypeMap(typeof(ulong));
|
||||
SqlMapper.AddTypeHandler(new UlongEncodeAsLongHandler());
|
||||
SqlMapper.AddTypeHandler(new UlongArrayHandler());
|
||||
|
||||
SqlMapper.AddTypeHandler(new PassthroughTypeHandler<Instant>());
|
||||
}
|
||||
|
||||
// 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<T> : SqlMapper.TypeHandler<T>
|
||||
{
|
||||
public override void SetValue(IDbDataParameter parameter, T? value) =>
|
||||
parameter.Value = value;
|
||||
|
||||
public override T Parse(object value) => (T)value;
|
||||
}
|
||||
|
||||
private class UlongEncodeAsLongHandler : SqlMapper.TypeHandler<ulong>
|
||||
{
|
||||
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<ulong[]>
|
||||
{
|
||||
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<DatabasePool>()
|
||||
.AddScoped<DatabaseConnection>(services =>
|
||||
services.GetRequiredService<DatabasePool>().Acquire()
|
||||
);
|
||||
}
|
||||
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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<DapperMessageRepository>();
|
||||
|
||||
public async Task<Message?> GetMessageAsync(ulong id, CancellationToken ct = default)
|
||||
{
|
||||
_logger.Debug("Retrieving message {MessageId}", id);
|
||||
|
||||
var dbMsg = await conn.QueryFirstOrDefaultAsync<Models.Message>(
|
||||
"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<Metadata>(
|
||||
await Task.Run(() => encryptionService.Decrypt(dbMsg.Metadata), ct)
|
||||
)
|
||||
: null,
|
||||
dbMsg.AttachmentSize
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new message. If the message is already in the database, updates the existing message instead.
|
||||
/// </summary>
|
||||
public async Task<bool> 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<bool>(
|
||||
"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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates a stored message with PluralKit information.
|
||||
/// </summary>
|
||||
/// <returns>True if the message exists and was updated, false if it doesn't exist.</returns>
|
||||
public async Task<bool> 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<bool> IsMessageIgnoredAsync(ulong id) =>
|
||||
await conn.ExecuteScalarAsync<bool>(
|
||||
"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<Attachment> 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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<GuildRepository>();
|
||||
|
||||
public async Task<Guild> GetAsync(Optional<Snowflake> id) => await GetAsync(id.Value.Value);
|
||||
|
||||
public async Task<Guild> GetAsync(Snowflake id) => await GetAsync(id.Value);
|
||||
|
||||
public async Task<Guild> GetAsync(ulong id)
|
||||
{
|
||||
_logger.Debug("Getting guild config for {GuildId}", id);
|
||||
|
||||
var guild = await conn.QueryFirstOrDefaultAsync<Guild>(
|
||||
"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<bool> IsGuildKnown(ulong id) =>
|
||||
await conn.ExecuteScalarAsync<bool>(
|
||||
"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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Metadata>(
|
||||
await Task.Run(() => encryptionService.Decrypt(dbMsg.EncryptedMetadata), ct)
|
||||
await Task.Run(() => encryptionService.Decrypt(dbMsg.Metadata), ct)
|
||||
)
|
||||
: null,
|
||||
dbMsg.AttachmentSize
|
||||
|
|
|
|||
|
|
@ -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<IClock>(SystemClock.Instance)
|
||||
.AddDatabasePool()
|
||||
.AddScoped<DapperMessageRepository>()
|
||||
.AddScoped<GuildRepository>()
|
||||
.AddSingleton<GuildCache>()
|
||||
.AddSingleton<RoleCache>()
|
||||
.AddSingleton<ChannelCache>()
|
||||
|
|
@ -113,7 +118,7 @@ public static class StartupExtensions
|
|||
.AddSingleton<NewsService>()
|
||||
.AddScoped<IEncryptionService, EncryptionService>()
|
||||
.AddSingleton<MetricsCollectionService>()
|
||||
.AddScoped<MessageRepository>()
|
||||
// .AddScoped<MessageRepository>()
|
||||
.AddSingleton<WebhookExecutorService>()
|
||||
.AddSingleton<PkMessageHandler>()
|
||||
.AddSingleton(InMemoryDataService<Snowflake, ChannelCommandData>.Instance)
|
||||
|
|
@ -189,6 +194,8 @@ public static class StartupExtensions
|
|||
.ServiceProvider.GetRequiredService<IClock>()
|
||||
.GetCurrentInstant();
|
||||
|
||||
DatabasePool.ConfigureDapper();
|
||||
|
||||
await using (var db = scope.ServiceProvider.GetRequiredService<DatabaseContext>())
|
||||
{
|
||||
var migrationCount = (await db.Database.GetPendingMigrationsAsync()).Count();
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
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<MessageRepository>();
|
||||
await using var messageRepository =
|
||||
scope.ServiceProvider.GetRequiredService<DapperMessageRepository>();
|
||||
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue