add invite repository to replace ef core
This commit is contained in:
parent
5891f28f7c
commit
64b4c26d93
12 changed files with 112 additions and 301 deletions
|
|
@ -15,12 +15,10 @@
|
||||||
|
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using Catalogger.Backend.Cache;
|
using Catalogger.Backend.Cache;
|
||||||
using Catalogger.Backend.Cache.InMemoryCache;
|
using Catalogger.Backend.Cache.InMemoryCache;
|
||||||
using Catalogger.Backend.Database;
|
using Catalogger.Backend.Database.Dapper.Repositories;
|
||||||
using Catalogger.Backend.Extensions;
|
using Catalogger.Backend.Extensions;
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Remora.Commands.Attributes;
|
using Remora.Commands.Attributes;
|
||||||
using Remora.Commands.Groups;
|
using Remora.Commands.Groups;
|
||||||
using Remora.Discord.API.Abstractions.Objects;
|
using Remora.Discord.API.Abstractions.Objects;
|
||||||
|
|
@ -33,7 +31,6 @@ using Remora.Discord.Commands.Feedback.Services;
|
||||||
using Remora.Discord.Commands.Services;
|
using Remora.Discord.Commands.Services;
|
||||||
using Remora.Discord.Pagination.Extensions;
|
using Remora.Discord.Pagination.Extensions;
|
||||||
using Remora.Rest.Core;
|
using Remora.Rest.Core;
|
||||||
using Invite = Catalogger.Backend.Database.Models.Invite;
|
|
||||||
using IResult = Remora.Results.IResult;
|
using IResult = Remora.Results.IResult;
|
||||||
|
|
||||||
namespace Catalogger.Backend.Bot.Commands;
|
namespace Catalogger.Backend.Bot.Commands;
|
||||||
|
|
@ -43,7 +40,7 @@ namespace Catalogger.Backend.Bot.Commands;
|
||||||
[DiscordDefaultMemberPermissions(DiscordPermission.ManageGuild)]
|
[DiscordDefaultMemberPermissions(DiscordPermission.ManageGuild)]
|
||||||
public class InviteCommands(
|
public class InviteCommands(
|
||||||
ILogger logger,
|
ILogger logger,
|
||||||
DatabaseContext db,
|
InviteRepository inviteRepository,
|
||||||
GuildCache guildCache,
|
GuildCache guildCache,
|
||||||
IInviteCache inviteCache,
|
IInviteCache inviteCache,
|
||||||
IDiscordRestChannelAPI channelApi,
|
IDiscordRestChannelAPI channelApi,
|
||||||
|
|
@ -63,7 +60,7 @@ public class InviteCommands(
|
||||||
if (!guildCache.TryGet(guildId, out var guild))
|
if (!guildCache.TryGet(guildId, out var guild))
|
||||||
throw new CataloggerError("Guild not in cache");
|
throw new CataloggerError("Guild not in cache");
|
||||||
|
|
||||||
var dbInvites = await db.Invites.Where(i => i.GuildId == guildId.Value).ToListAsync();
|
var dbInvites = await inviteRepository.GetGuildInvitesAsync(guildId);
|
||||||
|
|
||||||
var fields = guildInvites
|
var fields = guildInvites
|
||||||
.Select(i => new PartialNamedInvite(
|
.Select(i => new PartialNamedInvite(
|
||||||
|
|
@ -153,14 +150,7 @@ public class InviteCommands(
|
||||||
+ $"\nLink: https://discord.gg/{inviteResult.Entity.Code}"
|
+ $"\nLink: https://discord.gg/{inviteResult.Entity.Code}"
|
||||||
);
|
);
|
||||||
|
|
||||||
var dbInvite = new Invite
|
await inviteRepository.SetInviteNameAsync(guildId, inviteResult.Entity.Code, name);
|
||||||
{
|
|
||||||
GuildId = guildId.Value,
|
|
||||||
Code = inviteResult.Entity.Code,
|
|
||||||
Name = name,
|
|
||||||
};
|
|
||||||
db.Add(dbInvite);
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
|
|
||||||
return await feedbackService.ReplyAsync(
|
return await feedbackService.ReplyAsync(
|
||||||
$"Created a new invite in <#{channel.ID}> with the name **{name}**!"
|
$"Created a new invite in <#{channel.ID}> with the name **{name}**!"
|
||||||
|
|
@ -188,39 +178,18 @@ public class InviteCommands(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
var namedInvite = await db
|
var namedInvite = await inviteRepository.GetInviteAsync(guildId, invite);
|
||||||
.Invites.Where(i => i.GuildId == guildId.Value && i.Code == invite)
|
if (namedInvite == null && name == null)
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
if (namedInvite == null)
|
|
||||||
{
|
|
||||||
if (name == null)
|
|
||||||
return await feedbackService.ReplyAsync($"Invite `{invite}` already has no name.");
|
return await feedbackService.ReplyAsync($"Invite `{invite}` already has no name.");
|
||||||
|
|
||||||
namedInvite = new Invite
|
|
||||||
{
|
|
||||||
GuildId = guildId.Value,
|
|
||||||
Code = invite,
|
|
||||||
Name = name,
|
|
||||||
};
|
|
||||||
db.Add(namedInvite);
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
|
|
||||||
return await feedbackService.ReplyAsync(
|
|
||||||
$"New name set! The invite `{invite}` will now show up as **{name}** in logs."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name == null)
|
if (name == null)
|
||||||
{
|
{
|
||||||
db.Invites.Remove(namedInvite);
|
await inviteRepository.DeleteInviteAsync(guildId, invite);
|
||||||
await db.SaveChangesAsync();
|
|
||||||
|
|
||||||
return await feedbackService.ReplyAsync($"Removed the name for `{invite}`.");
|
return await feedbackService.ReplyAsync($"Removed the name for `{invite}`.");
|
||||||
}
|
}
|
||||||
|
|
||||||
namedInvite.Name = name;
|
await inviteRepository.SetInviteNameAsync(guildId, invite, name);
|
||||||
db.Update(namedInvite);
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
|
|
||||||
return await feedbackService.ReplyAsync(
|
return await feedbackService.ReplyAsync(
|
||||||
$"New name set! The invite `{invite}` will now show up as **{name}** in logs."
|
$"New name set! The invite `{invite}` will now show up as **{name}** in logs."
|
||||||
|
|
@ -230,7 +199,7 @@ public class InviteCommands(
|
||||||
|
|
||||||
public class InviteAutocompleteProvider(
|
public class InviteAutocompleteProvider(
|
||||||
ILogger logger,
|
ILogger logger,
|
||||||
DatabaseContext db,
|
InviteRepository inviteRepository,
|
||||||
IInviteCache inviteCache,
|
IInviteCache inviteCache,
|
||||||
ContextInjectionService contextInjection
|
ContextInjectionService contextInjection
|
||||||
) : IAutocompleteProvider
|
) : IAutocompleteProvider
|
||||||
|
|
@ -262,13 +231,13 @@ public class InviteAutocompleteProvider(
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
var namedInvites = await db
|
// We're filtering and ordering on the client side because a guild won't have infinite invites
|
||||||
.Invites.Where(i =>
|
// (the maximum on Discord's end is 1500-ish)
|
||||||
i.GuildId == guildId.Value && i.Name.ToLower().StartsWith(userInput.ToLower())
|
// and this way we don't need an index on (guild_id, name) for this *one* case.
|
||||||
)
|
var namedInvites = (await inviteRepository.GetGuildInvitesAsync(guildId))
|
||||||
|
.Where(i => i.Name.StartsWith(userInput, StringComparison.InvariantCultureIgnoreCase))
|
||||||
.OrderBy(i => i.Name)
|
.OrderBy(i => i.Name)
|
||||||
.Take(25)
|
.ToList();
|
||||||
.ToListAsync(ct);
|
|
||||||
|
|
||||||
if (namedInvites.Count != 0)
|
if (namedInvites.Count != 0)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ namespace Catalogger.Backend.Bot.Responders.Invites;
|
||||||
public class InviteDeleteResponder(
|
public class InviteDeleteResponder(
|
||||||
ILogger logger,
|
ILogger logger,
|
||||||
GuildRepository guildRepository,
|
GuildRepository guildRepository,
|
||||||
DatabaseContext db,
|
InviteRepository inviteRepository,
|
||||||
IInviteCache inviteCache,
|
IInviteCache inviteCache,
|
||||||
WebhookExecutorService webhookExecutor,
|
WebhookExecutorService webhookExecutor,
|
||||||
IDiscordRestGuildAPI guildApi
|
IDiscordRestGuildAPI guildApi
|
||||||
|
|
@ -44,9 +44,7 @@ public class InviteDeleteResponder(
|
||||||
{
|
{
|
||||||
var guildId = evt.GuildID.Value;
|
var guildId = evt.GuildID.Value;
|
||||||
|
|
||||||
var dbDeleteCount = await db
|
var dbDeleteCount = await inviteRepository.DeleteInviteAsync(guildId, evt.Code);
|
||||||
.Invites.Where(i => i.GuildId == guildId.Value && i.Code == evt.Code)
|
|
||||||
.ExecuteDeleteAsync(ct);
|
|
||||||
if (dbDeleteCount != 0)
|
if (dbDeleteCount != 0)
|
||||||
_logger.Information(
|
_logger.Information(
|
||||||
"Deleted named invite {Invite} for guild {Guild}",
|
"Deleted named invite {Invite} for guild {Guild}",
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ namespace Catalogger.Backend.Bot.Responders.Members;
|
||||||
public class GuildMemberAddResponder(
|
public class GuildMemberAddResponder(
|
||||||
ILogger logger,
|
ILogger logger,
|
||||||
DatabaseContext db,
|
DatabaseContext db,
|
||||||
|
InviteRepository inviteRepository,
|
||||||
GuildRepository guildRepository,
|
GuildRepository guildRepository,
|
||||||
IMemberCache memberCache,
|
IMemberCache memberCache,
|
||||||
IInviteCache inviteCache,
|
IInviteCache inviteCache,
|
||||||
|
|
@ -128,11 +129,7 @@ public class GuildMemberAddResponder(
|
||||||
goto afterInvite;
|
goto afterInvite;
|
||||||
}
|
}
|
||||||
|
|
||||||
var inviteName =
|
var inviteName = inviteRepository.GetInviteNameAsync(member.GuildID, usedInvite.Code);
|
||||||
await db
|
|
||||||
.Invites.Where(i => i.Code == usedInvite.Code && i.GuildId == member.GuildID.Value)
|
|
||||||
.Select(i => i.Name)
|
|
||||||
.FirstOrDefaultAsync(ct) ?? "*(unnamed)*";
|
|
||||||
|
|
||||||
var inviteDescription = $"""
|
var inviteDescription = $"""
|
||||||
**Code:** {usedInvite.Code}
|
**Code:** {usedInvite.Code}
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ public class MessageCreateResponder(
|
||||||
ILogger logger,
|
ILogger logger,
|
||||||
Config config,
|
Config config,
|
||||||
GuildRepository guildRepository,
|
GuildRepository guildRepository,
|
||||||
DapperMessageRepository messageRepository,
|
MessageRepository messageRepository,
|
||||||
UserCache userCache,
|
UserCache userCache,
|
||||||
PkMessageHandler pkMessageHandler
|
PkMessageHandler pkMessageHandler
|
||||||
) : IResponder<IMessageCreate>
|
) : IResponder<IMessageCreate>
|
||||||
|
|
@ -146,7 +146,7 @@ public partial class PkMessageHandler(ILogger logger, IServiceProvider services)
|
||||||
|
|
||||||
await using var scope = services.CreateAsyncScope();
|
await using var scope = services.CreateAsyncScope();
|
||||||
await using var messageRepository =
|
await using var messageRepository =
|
||||||
scope.ServiceProvider.GetRequiredService<DapperMessageRepository>();
|
scope.ServiceProvider.GetRequiredService<MessageRepository>();
|
||||||
|
|
||||||
await Task.WhenAll(
|
await Task.WhenAll(
|
||||||
messageRepository.SetProxiedMessageDataAsync(
|
messageRepository.SetProxiedMessageDataAsync(
|
||||||
|
|
@ -166,7 +166,7 @@ public partial class PkMessageHandler(ILogger logger, IServiceProvider services)
|
||||||
|
|
||||||
await using var scope = services.CreateAsyncScope();
|
await using var scope = services.CreateAsyncScope();
|
||||||
await using var messageRepository =
|
await using var messageRepository =
|
||||||
scope.ServiceProvider.GetRequiredService<DapperMessageRepository>();
|
scope.ServiceProvider.GetRequiredService<MessageRepository>();
|
||||||
var pluralkitApi = scope.ServiceProvider.GetRequiredService<PluralkitApiService>();
|
var pluralkitApi = scope.ServiceProvider.GetRequiredService<PluralkitApiService>();
|
||||||
|
|
||||||
var (isStored, hasProxyInfo) = await messageRepository.HasProxyInfoAsync(msgId);
|
var (isStored, hasProxyInfo) = await messageRepository.HasProxyInfoAsync(msgId);
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ namespace Catalogger.Backend.Bot.Responders.Messages;
|
||||||
public class MessageDeleteBulkResponder(
|
public class MessageDeleteBulkResponder(
|
||||||
ILogger logger,
|
ILogger logger,
|
||||||
GuildRepository guildRepository,
|
GuildRepository guildRepository,
|
||||||
DapperMessageRepository messageRepository,
|
MessageRepository messageRepository,
|
||||||
WebhookExecutorService webhookExecutor,
|
WebhookExecutorService webhookExecutor,
|
||||||
ChannelCache channelCache
|
ChannelCache channelCache
|
||||||
) : IResponder<IMessageDeleteBulk>
|
) : IResponder<IMessageDeleteBulk>
|
||||||
|
|
@ -128,7 +128,7 @@ public class MessageDeleteBulkResponder(
|
||||||
return Result.Success;
|
return Result.Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
private string RenderMessage(Snowflake messageId, DapperMessageRepository.Message? message)
|
private string RenderMessage(Snowflake messageId, MessageRepository.Message? message)
|
||||||
{
|
{
|
||||||
var timestamp = messageId.Timestamp.ToOffsetDateTime().ToString();
|
var timestamp = messageId.Timestamp.ToOffsetDateTime().ToString();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ namespace Catalogger.Backend.Bot.Responders.Messages;
|
||||||
public class MessageDeleteResponder(
|
public class MessageDeleteResponder(
|
||||||
ILogger logger,
|
ILogger logger,
|
||||||
GuildRepository guildRepository,
|
GuildRepository guildRepository,
|
||||||
DapperMessageRepository messageRepository,
|
MessageRepository messageRepository,
|
||||||
WebhookExecutorService webhookExecutor,
|
WebhookExecutorService webhookExecutor,
|
||||||
ChannelCache channelCache,
|
ChannelCache channelCache,
|
||||||
UserCache userCache,
|
UserCache userCache,
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ public class MessageUpdateResponder(
|
||||||
DatabaseContext db,
|
DatabaseContext db,
|
||||||
ChannelCache channelCache,
|
ChannelCache channelCache,
|
||||||
UserCache userCache,
|
UserCache userCache,
|
||||||
DapperMessageRepository messageRepository,
|
MessageRepository messageRepository,
|
||||||
WebhookExecutorService webhookExecutor,
|
WebhookExecutorService webhookExecutor,
|
||||||
PluralkitApiService pluralkitApi
|
PluralkitApiService pluralkitApi
|
||||||
) : IResponder<IMessageUpdate>
|
) : IResponder<IMessageUpdate>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
// 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 InviteRepository(ILogger logger, DatabaseConnection conn)
|
||||||
|
: IDisposable,
|
||||||
|
IAsyncDisposable
|
||||||
|
{
|
||||||
|
private readonly ILogger _logger = logger.ForContext<InviteRepository>();
|
||||||
|
|
||||||
|
public async Task<List<Invite>> GetGuildInvitesAsync(Snowflake guildId) =>
|
||||||
|
(
|
||||||
|
await conn.QueryAsync<Invite>(
|
||||||
|
"select * from invites where guild_id = @GuildId",
|
||||||
|
new { GuildId = guildId.Value }
|
||||||
|
)
|
||||||
|
).ToList();
|
||||||
|
|
||||||
|
public async Task SetInviteNameAsync(Snowflake guildId, string code, string name) =>
|
||||||
|
await conn.ExecuteAsync(
|
||||||
|
"""
|
||||||
|
insert into invites (code, guild_id, name) values
|
||||||
|
(@Code, @GuildId, @Name) on conflict (code, guild_id) do update set name = @Name
|
||||||
|
""",
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Code = code,
|
||||||
|
GuildId = guildId.Value,
|
||||||
|
Name = name,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
public async Task<Invite?> GetInviteAsync(Snowflake guildId, string code) =>
|
||||||
|
await conn.QueryFirstOrDefaultAsync<Invite>(
|
||||||
|
"select * from invites where guild_id = @GuildId and code = @Code",
|
||||||
|
new { GuildId = guildId.Value, Code = code }
|
||||||
|
);
|
||||||
|
|
||||||
|
public async Task<string> GetInviteNameAsync(Snowflake guildId, string code) =>
|
||||||
|
await conn.ExecuteScalarAsync<string>(
|
||||||
|
"select name from invites where guild_id = @GuildId and code = @Code",
|
||||||
|
new { GuildId = guildId.Value, Code = code }
|
||||||
|
) ?? "(unnamed)";
|
||||||
|
|
||||||
|
public async Task<int> DeleteInviteAsync(Snowflake guildId, string code) =>
|
||||||
|
await conn.ExecuteAsync(
|
||||||
|
"delete from invites where guild_id = @GuildId and code = @Code",
|
||||||
|
new { GuildId = guildId.Value, Code = code }
|
||||||
|
);
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
conn.Dispose();
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
await conn.DisposeAsync();
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -22,13 +22,13 @@ using Remora.Rest.Core;
|
||||||
|
|
||||||
namespace Catalogger.Backend.Database.Dapper.Repositories;
|
namespace Catalogger.Backend.Database.Dapper.Repositories;
|
||||||
|
|
||||||
public class DapperMessageRepository(
|
public class MessageRepository(
|
||||||
ILogger logger,
|
ILogger logger,
|
||||||
DatabaseConnection conn,
|
DatabaseConnection conn,
|
||||||
IEncryptionService encryptionService
|
IEncryptionService encryptionService
|
||||||
) : IDisposable, IAsyncDisposable
|
) : IDisposable, IAsyncDisposable
|
||||||
{
|
{
|
||||||
private readonly ILogger _logger = logger.ForContext<DapperMessageRepository>();
|
private readonly ILogger _logger = logger.ForContext<MessageRepository>();
|
||||||
|
|
||||||
public async Task<Message?> GetMessageAsync(ulong id, CancellationToken ct = default)
|
public async Task<Message?> GetMessageAsync(ulong id, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
|
|
@ -1,232 +0,0 @@
|
||||||
// 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 Microsoft.EntityFrameworkCore;
|
|
||||||
using Remora.Discord.API;
|
|
||||||
using Remora.Discord.API.Abstractions.Gateway.Events;
|
|
||||||
using Remora.Rest.Core;
|
|
||||||
using DbMessage = Catalogger.Backend.Database.Models.Message;
|
|
||||||
|
|
||||||
namespace Catalogger.Backend.Database.Queries;
|
|
||||||
|
|
||||||
public class MessageRepository(
|
|
||||||
ILogger logger,
|
|
||||||
DatabaseContext db,
|
|
||||||
IEncryptionService encryptionService
|
|
||||||
)
|
|
||||||
{
|
|
||||||
private readonly ILogger _logger = logger.ForContext<MessageRepository>();
|
|
||||||
|
|
||||||
public async Task SaveMessageAsync(IMessageCreate msg, CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
_logger.Debug("Saving message {MessageId}", msg.ID);
|
|
||||||
|
|
||||||
var metadata = new Metadata(
|
|
||||||
IsWebhook: msg.WebhookID.HasValue,
|
|
||||||
msg.Attachments.Select(a => new Attachment(a.Filename, a.Size, a.ContentType.Value))
|
|
||||||
);
|
|
||||||
|
|
||||||
var dbMessage = new DbMessage
|
|
||||||
{
|
|
||||||
Id = msg.ID.ToUlong(),
|
|
||||||
UserId = msg.Author.ID.ToUlong(),
|
|
||||||
ChannelId = msg.ChannelID.ToUlong(),
|
|
||||||
GuildId = msg.GuildID.ToUlong(),
|
|
||||||
|
|
||||||
Content = await Task.Run(
|
|
||||||
() =>
|
|
||||||
encryptionService.Encrypt(
|
|
||||||
string.IsNullOrWhiteSpace(msg.Content) ? "None" : msg.Content
|
|
||||||
),
|
|
||||||
ct
|
|
||||||
),
|
|
||||||
Username = await Task.Run(() => encryptionService.Encrypt(msg.Author.Tag()), ct),
|
|
||||||
Metadata = await Task.Run(
|
|
||||||
() => encryptionService.Encrypt(JsonSerializer.Serialize(metadata)),
|
|
||||||
ct
|
|
||||||
),
|
|
||||||
AttachmentSize = msg.Attachments.Select(a => a.Size).Sum(),
|
|
||||||
};
|
|
||||||
|
|
||||||
db.Add(dbMessage);
|
|
||||||
await db.SaveChangesAsync(ct);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Updates an edited message.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>true if the message was already stored and got updated,
|
|
||||||
/// false if the message wasn't stored and was newly inserted.</returns>
|
|
||||||
public async Task<bool> UpdateMessageAsync(IMessageCreate msg, CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
_logger.Debug("Updating message {MessageId}", msg.ID);
|
|
||||||
|
|
||||||
var tx = await db.Database.BeginTransactionAsync(ct);
|
|
||||||
var (isStored, _) = await HasProxyInfoAsync(msg.ID.Value);
|
|
||||||
if (!isStored)
|
|
||||||
{
|
|
||||||
_logger.Debug("Edited message {MessageId} is not stored yet, storing it", msg.ID);
|
|
||||||
await SaveMessageAsync(msg, ct);
|
|
||||||
await tx.CommitAsync(ct);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var metadata = new Metadata(
|
|
||||||
IsWebhook: msg.WebhookID.HasValue,
|
|
||||||
msg.Attachments.Select(a => new Attachment(a.Filename, a.Size, a.ContentType.Value))
|
|
||||||
);
|
|
||||||
|
|
||||||
var dbMsg = await db.Messages.FindAsync(
|
|
||||||
new object?[] { msg.ID.Value },
|
|
||||||
cancellationToken: ct
|
|
||||||
);
|
|
||||||
if (dbMsg == null)
|
|
||||||
throw new CataloggerError(
|
|
||||||
"Message was null despite HasProxyInfoAsync returning true"
|
|
||||||
);
|
|
||||||
|
|
||||||
dbMsg.Content = await Task.Run(
|
|
||||||
() =>
|
|
||||||
encryptionService.Encrypt(
|
|
||||||
string.IsNullOrWhiteSpace(msg.Content) ? "None" : msg.Content
|
|
||||||
),
|
|
||||||
ct
|
|
||||||
);
|
|
||||||
dbMsg.Username = await Task.Run(() => encryptionService.Encrypt(msg.Author.Tag()), ct);
|
|
||||||
dbMsg.Metadata = await Task.Run(
|
|
||||||
() => encryptionService.Encrypt(JsonSerializer.Serialize(metadata)),
|
|
||||||
ct
|
|
||||||
);
|
|
||||||
|
|
||||||
db.Update(dbMsg);
|
|
||||||
await db.SaveChangesAsync(ct);
|
|
||||||
await tx.CommitAsync(ct);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Message?> GetMessageAsync(ulong id, CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
_logger.Debug("Retrieving message {MessageId}", id);
|
|
||||||
|
|
||||||
var dbMsg = await db.Messages.AsNoTracking().FirstOrDefaultAsync(m => m.Id == id, ct);
|
|
||||||
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>
|
|
||||||
/// Checks if a message has proxy information.
|
|
||||||
/// If yes, returns (true, true). If no, returns (true, false). If the message isn't saved at all, returns (false, false).
|
|
||||||
/// </summary>
|
|
||||||
public async Task<(bool, bool)> HasProxyInfoAsync(ulong id)
|
|
||||||
{
|
|
||||||
_logger.Debug("Checking if message {MessageId} has proxy information", id);
|
|
||||||
|
|
||||||
var msg = await db
|
|
||||||
.Messages.AsNoTracking()
|
|
||||||
.Select(m => new { m.Id, m.OriginalId })
|
|
||||||
.FirstOrDefaultAsync(m => m.Id == id);
|
|
||||||
return (msg != null, msg?.OriginalId != null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task SetProxiedMessageDataAsync(
|
|
||||||
ulong id,
|
|
||||||
ulong originalId,
|
|
||||||
ulong authorId,
|
|
||||||
string? systemId,
|
|
||||||
string? memberId
|
|
||||||
)
|
|
||||||
{
|
|
||||||
_logger.Debug("Setting proxy information for message {MessageId}", id);
|
|
||||||
|
|
||||||
var message = await db.Messages.FirstOrDefaultAsync(m => m.Id == id);
|
|
||||||
if (message == null)
|
|
||||||
{
|
|
||||||
_logger.Debug("Message {MessageId} not found", id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.Debug("Updating message {MessageId}", id);
|
|
||||||
|
|
||||||
message.OriginalId = originalId;
|
|
||||||
message.UserId = authorId;
|
|
||||||
message.System = systemId;
|
|
||||||
message.Member = memberId;
|
|
||||||
|
|
||||||
db.Update(message);
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> IsMessageIgnoredAsync(ulong id, CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
_logger.Debug("Checking if message {MessageId} is ignored", id);
|
|
||||||
return await db.IgnoredMessages.AsNoTracking().FirstOrDefaultAsync(m => m.Id == id, ct)
|
|
||||||
!= null;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
var msgCount = await db.Messages.Where(m => m.Id < cutoffId.Value).ExecuteDeleteAsync();
|
|
||||||
var ignoredMsgCount = await db
|
|
||||||
.IgnoredMessages.Where(m => m.Id < cutoffId.Value)
|
|
||||||
.ExecuteDeleteAsync();
|
|
||||||
|
|
||||||
return (msgCount, ignoredMsgCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
@ -106,8 +106,9 @@ public static class StartupExtensions
|
||||||
services
|
services
|
||||||
.AddSingleton<IClock>(SystemClock.Instance)
|
.AddSingleton<IClock>(SystemClock.Instance)
|
||||||
.AddDatabasePool()
|
.AddDatabasePool()
|
||||||
.AddScoped<DapperMessageRepository>()
|
.AddScoped<MessageRepository>()
|
||||||
.AddScoped<GuildRepository>()
|
.AddScoped<GuildRepository>()
|
||||||
|
.AddScoped<InviteRepository>()
|
||||||
.AddSingleton<GuildCache>()
|
.AddSingleton<GuildCache>()
|
||||||
.AddSingleton<RoleCache>()
|
.AddSingleton<RoleCache>()
|
||||||
.AddSingleton<ChannelCache>()
|
.AddSingleton<ChannelCache>()
|
||||||
|
|
@ -118,7 +119,6 @@ public static class StartupExtensions
|
||||||
.AddSingleton<NewsService>()
|
.AddSingleton<NewsService>()
|
||||||
.AddScoped<IEncryptionService, EncryptionService>()
|
.AddScoped<IEncryptionService, EncryptionService>()
|
||||||
.AddSingleton<MetricsCollectionService>()
|
.AddSingleton<MetricsCollectionService>()
|
||||||
// .AddScoped<MessageRepository>()
|
|
||||||
.AddSingleton<WebhookExecutorService>()
|
.AddSingleton<WebhookExecutorService>()
|
||||||
.AddSingleton<PkMessageHandler>()
|
.AddSingleton<PkMessageHandler>()
|
||||||
.AddSingleton(InMemoryDataService<Snowflake, ChannelCommandData>.Instance)
|
.AddSingleton(InMemoryDataService<Snowflake, ChannelCommandData>.Instance)
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ public class BackgroundTasksService(ILogger logger, IServiceProvider services) :
|
||||||
|
|
||||||
await using var scope = services.CreateAsyncScope();
|
await using var scope = services.CreateAsyncScope();
|
||||||
await using var messageRepository =
|
await using var messageRepository =
|
||||||
scope.ServiceProvider.GetRequiredService<DapperMessageRepository>();
|
scope.ServiceProvider.GetRequiredService<MessageRepository>();
|
||||||
|
|
||||||
var (msgCount, ignoredCount) = await messageRepository.DeleteExpiredMessagesAsync();
|
var (msgCount, ignoredCount) = await messageRepository.DeleteExpiredMessagesAsync();
|
||||||
if (msgCount != 0 || ignoredCount != 0)
|
if (msgCount != 0 || ignoredCount != 0)
|
||||||
|
|
@ -44,7 +44,7 @@ public class BackgroundTasksService(ILogger logger, IServiceProvider services) :
|
||||||
"Deleted {Count} messages and {IgnoredCount} ignored message IDs older than {MaxDays} days old",
|
"Deleted {Count} messages and {IgnoredCount} ignored message IDs older than {MaxDays} days old",
|
||||||
msgCount,
|
msgCount,
|
||||||
ignoredCount,
|
ignoredCount,
|
||||||
DapperMessageRepository.MaxMessageAgeDays
|
MessageRepository.MaxMessageAgeDays
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue