Catalogger.NET/Catalogger.Backend/Services/WebhookExecutorService.cs

223 lines
9.3 KiB
C#
Raw Normal View History

2024-08-13 13:08:50 +02:00
using System.Collections.Concurrent;
using Catalogger.Backend.Cache;
using Catalogger.Backend.Cache.InMemoryCache;
2024-08-13 13:08:50 +02:00
using Catalogger.Backend.Extensions;
using Remora.Discord.API;
using Remora.Discord.API.Abstractions.Objects;
using Remora.Discord.API.Abstractions.Rest;
using Remora.Rest.Core;
2024-08-13 16:48:54 +02:00
using Guild = Catalogger.Backend.Database.Models.Guild;
2024-08-13 13:08:50 +02:00
namespace Catalogger.Backend.Services;
2024-08-13 16:48:54 +02:00
// TODO: this entire class is a mess, clean it up
2024-08-13 13:08:50 +02:00
public class WebhookExecutorService(
Config config,
ILogger logger,
IWebhookCache webhookCache,
ChannelCache channelCache,
2024-08-13 13:08:50 +02:00
IDiscordRestWebhookAPI webhookApi)
{
private readonly ILogger _logger = logger.ForContext<WebhookExecutorService>();
private readonly Snowflake _applicationId = DiscordSnowflake.New(config.Discord.ApplicationId);
private readonly ConcurrentDictionary<ulong, ConcurrentQueue<IEmbed>> _cache = new();
2024-08-13 16:48:54 +02:00
private readonly ConcurrentDictionary<ulong, object> _locks = new();
2024-08-13 13:08:50 +02:00
private readonly ConcurrentDictionary<ulong, Timer> _timers = new();
private IUser? _selfUser;
public void SetSelfUser(IUser user) => _selfUser = user;
public async Task QueueLogAsync(Guild guild, LogChannelType logChannelType, IEmbed embed)
{
var logChannel = GetLogChannel(guild, logChannelType, channelId: null, userId: null);
if (logChannel == null) return;
await QueueLogAsync(logChannel.Value, embed);
}
2024-08-13 16:48:54 +02:00
private List<IEmbed> TakeFromQueue(ulong channelId)
2024-08-13 13:08:50 +02:00
{
2024-08-13 16:48:54 +02:00
var queue = _cache.GetOrAdd(channelId, []);
var channelLock = _locks.GetOrAdd(channelId, channelId);
lock (channelLock)
{
var embeds = new List<IEmbed>();
for (var i = 0; i < 5; i++)
{
if (!queue.TryDequeue(out var embed)) break;
embeds.Add(embed);
}
return embeds;
}
}
2024-08-13 13:08:50 +02:00
2024-08-13 16:48:54 +02:00
public async Task QueueLogAsync(ulong channelId, IEmbed embed)
{
2024-08-13 13:08:50 +02:00
var queue = _cache.GetOrAdd(channelId, []);
queue.Enqueue(embed);
2024-08-13 16:48:54 +02:00
_cache[channelId] = queue;
2024-08-13 13:08:50 +02:00
2024-08-13 16:48:54 +02:00
await SetTimer(channelId, queue);
}
2024-08-13 13:08:50 +02:00
2024-08-13 16:48:54 +02:00
private async Task SetTimer(ulong channelId, ConcurrentQueue<IEmbed> queue)
{
if (_timers.TryGetValue(channelId, out var existingTimer)) await existingTimer.DisposeAsync();
2024-08-13 13:08:50 +02:00
_timers[channelId] = new Timer(_ =>
{
2024-08-13 16:48:54 +02:00
_logger.Debug("Sending 5 queued embeds");
var __ = SendLogsAsync(channelId, TakeFromQueue(channelId));
if (!queue.IsEmpty)
{
if (_timers.TryGetValue(channelId, out var timer)) timer.Dispose();
var ___ = SetTimer(channelId, queue);
}
2024-08-13 13:08:50 +02:00
}, null, 3000, Timeout.Infinite);
}
2024-08-13 16:48:54 +02:00
private async Task SendLogsAsync(ulong channelId, List<IEmbed> embeds)
2024-08-13 13:08:50 +02:00
{
2024-08-13 16:48:54 +02:00
_logger.Debug("Sending {Count} embeds to channel {ChannelId}", embeds.Count, channelId);
if (embeds.Count == 0) return;
2024-08-13 13:08:50 +02:00
var webhook = await webhookCache.GetOrFetchWebhookAsync(channelId, id => FetchWebhookAsync(id));
await webhookApi.ExecuteWebhookAsync(DiscordSnowflake.New(webhook.Id), webhook.Token, shouldWait: false,
embeds: embeds, username: _selfUser!.Username, avatarUrl: _selfUser.AvatarUrl());
}
2024-08-13 16:48:54 +02:00
public async Task SendLogWithAttachmentsAsync(ulong channelId, IEmbed embed, IEnumerable<FileData> files)
{
var attachments = files
.Select<FileData, OneOf.OneOf<FileData, IPartialAttachment>>(f => f)
.ToList();
var webhook = await webhookCache.GetOrFetchWebhookAsync(channelId, id => FetchWebhookAsync(id));
await webhookApi.ExecuteWebhookAsync(DiscordSnowflake.New(webhook.Id), webhook.Token, shouldWait: false,
embeds: new List<IEmbed>([embed]), attachments: attachments, username: _selfUser!.Username,
avatarUrl: _selfUser.AvatarUrl());
}
2024-08-13 13:08:50 +02:00
private async Task<IWebhook> FetchWebhookAsync(Snowflake channelId, CancellationToken ct = default)
{
var channelWebhooks =
await webhookApi.GetChannelWebhooksAsync(channelId, ct).GetOrThrow();
var webhook = channelWebhooks.FirstOrDefault(w => w.ApplicationID == _applicationId && w.Token.IsDefined());
if (webhook != null) return webhook;
return await webhookApi.CreateWebhookAsync(channelId, "Catalogger", default, reason: "Creating logging webhook",
ct: ct).GetOrThrow();
}
public ulong? GetLogChannel(Guild guild, LogChannelType logChannelType, Snowflake? channelId = null,
ulong? userId = null)
{
if (channelId == null) return GetDefaultLogChannel(guild, logChannelType);
2024-08-13 16:48:54 +02:00
if (!channelCache.TryGet(channelId.Value, out var channel)) return null;
2024-08-13 13:08:50 +02:00
Snowflake? categoryId;
if (channel.Type is ChannelType.AnnouncementThread or ChannelType.PrivateThread or ChannelType.PublicThread)
{
// parent_id should always have a value for threads
channelId = channel.ParentID.Value!.Value;
2024-08-13 16:48:54 +02:00
if (!channelCache.TryGet(channelId.Value, out var parentChannel))
2024-08-13 13:08:50 +02:00
return GetDefaultLogChannel(guild, logChannelType);
categoryId = parentChannel.ParentID.Value;
}
else
{
channelId = channel.ID;
categoryId = channel.ParentID.Value;
}
// Check if the channel, or its category, or the user is ignored
if (guild.Channels.IgnoredChannels.Contains(channelId.Value.Value) ||
categoryId != null && guild.Channels.IgnoredChannels.Contains(categoryId.Value.Value)) return null;
if (userId != null)
{
if (guild.Channels.IgnoredUsers.Contains(userId.Value)) return null;
// Check the channel-local and category-local ignored users
var channelIgnoredUsers =
guild.Channels.IgnoredUsersPerChannel.GetValueOrDefault(channelId.Value.Value) ?? [];
var categoryIgnoredUsers = (categoryId != null
? guild.Channels.IgnoredUsersPerChannel.GetValueOrDefault(categoryId.Value.Value)
: []) ?? [];
if (channelIgnoredUsers.Concat(categoryIgnoredUsers).Contains(userId.Value)) return null;
}
// These three events can be redirected to other channels. Redirects can be on a channel or category level.
// Obviously, the events are only redirected if they're supposed to be logged in the first place.
if (logChannelType is LogChannelType.MessageUpdate or LogChannelType.MessageDelete
or LogChannelType.MessageDeleteBulk)
{
if (GetDefaultLogChannel(guild, logChannelType) == null) return null;
ulong categoryRedirect = categoryId != null
? guild.Channels.Redirects.GetValueOrDefault(categoryId.Value.Value)
: 0;
if (guild.Channels.Redirects.TryGetValue(channelId.Value.Value, out var channelRedirect))
return channelRedirect;
if (categoryRedirect != 0) return categoryRedirect;
return GetDefaultLogChannel(guild, logChannelType);
}
return GetDefaultLogChannel(guild, logChannelType);
}
2024-08-14 16:05:43 +02:00
public static ulong? GetDefaultLogChannel(Guild guild, LogChannelType channelType) => channelType switch
2024-08-13 13:08:50 +02:00
{
LogChannelType.GuildUpdate => guild.Channels.GuildUpdate,
LogChannelType.GuildEmojisUpdate => guild.Channels.GuildEmojisUpdate,
LogChannelType.GuildRoleCreate => guild.Channels.GuildRoleCreate,
LogChannelType.GuildRoleUpdate => guild.Channels.GuildRoleUpdate,
LogChannelType.GuildRoleDelete => guild.Channels.GuildRoleDelete,
LogChannelType.ChannelCreate => guild.Channels.ChannelCreate,
LogChannelType.ChannelUpdate => guild.Channels.ChannelUpdate,
LogChannelType.ChannelDelete => guild.Channels.ChannelDelete,
LogChannelType.GuildMemberAdd => guild.Channels.GuildMemberAdd,
LogChannelType.GuildMemberUpdate => guild.Channels.GuildMemberUpdate,
LogChannelType.GuildKeyRoleUpdate => guild.Channels.GuildKeyRoleUpdate,
LogChannelType.GuildMemberNickUpdate => guild.Channels.GuildMemberNickUpdate,
LogChannelType.GuildMemberAvatarUpdate => guild.Channels.GuildMemberAvatarUpdate,
LogChannelType.GuildMemberRemove => guild.Channels.GuildMemberRemove,
LogChannelType.GuildMemberKick => guild.Channels.GuildMemberKick,
LogChannelType.GuildBanAdd => guild.Channels.GuildBanAdd,
LogChannelType.GuildBanRemove => guild.Channels.GuildBanRemove,
LogChannelType.InviteCreate => guild.Channels.InviteCreate,
LogChannelType.InviteDelete => guild.Channels.InviteDelete,
LogChannelType.MessageUpdate => guild.Channels.MessageUpdate,
LogChannelType.MessageDelete => guild.Channels.MessageDelete,
LogChannelType.MessageDeleteBulk => guild.Channels.MessageDeleteBulk,
_ => throw new ArgumentOutOfRangeException(nameof(channelType))
};
}
public enum LogChannelType
{
GuildUpdate,
GuildEmojisUpdate,
GuildRoleCreate,
GuildRoleUpdate,
GuildRoleDelete,
ChannelCreate,
ChannelUpdate,
ChannelDelete,
GuildMemberAdd,
GuildMemberUpdate,
GuildKeyRoleUpdate,
GuildMemberNickUpdate,
GuildMemberAvatarUpdate,
GuildMemberRemove,
GuildMemberKick,
GuildBanAdd,
GuildBanRemove,
InviteCreate,
InviteDelete,
MessageUpdate,
MessageDelete,
MessageDeleteBulk
}