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(); private readonly ConcurrentDictionary _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(); 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(); var timeoutRepository = scope.ServiceProvider.GetRequiredService(); 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(); } }