Catalogger.NET/Catalogger.Backend/Services/TimeoutService.cs
sam e6d68338db
feat: store timeouts in database and log them ending
we have to do this because discord doesn't notify us when a timeout
ends naturally, only when a moderator removes it early.
2024-11-05 22:22:12 +01:00

119 lines
3.9 KiB
C#

using System.Collections.Concurrent;
using Catalogger.Backend.Bot;
using Catalogger.Backend.Cache.InMemoryCache;
using Catalogger.Backend.Database.Models;
using Catalogger.Backend.Database.Repositories;
using Catalogger.Backend.Extensions;
using Remora.Discord.API;
using Remora.Discord.Extensions.Embeds;
using Remora.Rest.Core;
namespace Catalogger.Backend.Services;
public class TimeoutService(
IServiceProvider serviceProvider,
ILogger logger,
WebhookExecutorService webhookExecutor,
UserCache userCache
)
{
private readonly ILogger _logger = logger.ForContext<TimeoutService>();
private readonly ConcurrentDictionary<int, Timer> _timers = new();
public async Task InitializeAsync()
{
_logger.Information("Populating timeout service with existing database timeouts");
await using var scope = serviceProvider.CreateAsyncScope();
var timeoutRepository = scope.ServiceProvider.GetRequiredService<TimeoutRepository>();
var timeouts = await timeoutRepository.GetAllAsync();
foreach (var timeout in timeouts)
AddTimer(timeout);
}
public void AddTimer(DiscordTimeout timeout)
{
_logger.Debug("Adding timeout {TimeoutId} to queue", timeout.Id);
RemoveTimer(timeout.Id);
_timers[timeout.Id] = new Timer(
_ =>
{
var __ = SendTimeoutLogAsync(timeout.Id);
},
null,
timeout.Until.ToDateTimeOffset() - DateTimeOffset.UtcNow,
Timeout.InfiniteTimeSpan
);
}
private async Task SendTimeoutLogAsync(int timeoutId)
{
_logger.Information("Sending timeout log for {TimeoutId}", timeoutId);
await using var scope = serviceProvider.CreateAsyncScope();
var guildRepository = scope.ServiceProvider.GetRequiredService<GuildRepository>();
var timeoutRepository = scope.ServiceProvider.GetRequiredService<TimeoutRepository>();
var timeout = await timeoutRepository.RemoveAsync(timeoutId);
if (timeout == null)
{
_logger.Warning("Timeout {TimeoutId} not found, can't log anything", timeoutId);
return;
}
var userId = DiscordSnowflake.New(timeout.UserId);
var moderatorId =
timeout.ModeratorId != null
? DiscordSnowflake.New(timeout.ModeratorId.Value)
: (Snowflake?)null;
var user = await userCache.GetUserAsync(userId);
if (user == null)
{
_logger.Warning("Could not get user {UserId} from cache, can't log timeout", userId);
return;
}
var embed = new EmbedBuilder()
.WithAuthor(user.Tag(), null, user.AvatarUrl())
.WithTitle("Member timeout ended")
.WithDescription($"<@{user.ID}>")
.WithColour(DiscordUtils.Green)
.WithFooter($"User ID: {user.ID}")
.WithCurrentTimestamp();
if (moderatorId != null)
{
var moderator = await userCache.TryFormatUserAsync(moderatorId.Value);
embed.AddField("Originally timed out by", moderator);
}
else
{
embed.AddField("Originally timed out by", "*(unknown)*");
}
try
{
var guildConfig = await guildRepository.GetAsync(DiscordSnowflake.New(timeout.GuildId));
webhookExecutor.QueueLog(
guildConfig,
LogChannelType.GuildMemberTimeout,
embed.Build().GetOrThrow()
);
}
catch (Exception e)
{
_logger.Error(e, "Could not log timeout {TimeoutId} expiring", timeout.Id);
}
}
public void RemoveTimer(int timeoutId)
{
if (!_timers.TryRemove(timeoutId, out var timer))
return;
_logger.Debug("Removing timeout {TimeoutId} from queue", timeoutId);
timer.Dispose();
}
}