using System; using System.Collections.Concurrent; using System.Threading; using System.Threading.Tasks; using NodaTime; using Serilog; namespace PluralKit.Bot { public class InteractionDispatchService: IDisposable { private static readonly Duration DefaultExpiry = Duration.FromMinutes(15); private readonly ConcurrentDictionary _handlers = new(); private readonly CancellationTokenSource _cts = new(); private readonly IClock _clock; private readonly ILogger _logger; private readonly Task _cleanupWorker; public InteractionDispatchService(IClock clock, ILogger logger) { _clock = clock; _logger = logger.ForContext(); _cleanupWorker = CleanupLoop(_cts.Token); } public async ValueTask Dispatch(string customId, InteractionContext context) { if (!Guid.TryParse(customId, out var customIdGuid)) return false; if (!_handlers.TryGetValue(customIdGuid, out var handler)) return false; await handler.Callback.Invoke(context); return true; } public string Register(Func callback, Duration? expiry = null) { var key = Guid.NewGuid(); var handler = new RegisteredInteraction { Callback = callback, Expiry = _clock.GetCurrentInstant() + (expiry ?? DefaultExpiry) }; _handlers[key] = handler; return key.ToString(); } private async Task CleanupLoop(CancellationToken ct) { while (true) { DoCleanup(); await Task.Delay(TimeSpan.FromMinutes(1), ct); } } private void DoCleanup() { var now = _clock.GetCurrentInstant(); var removedCount = 0; foreach (var (key, value) in _handlers.ToArray()) { if (value.Expiry < now) { _handlers.TryRemove(key, out _); removedCount++; } } _logger.Debug("Removed {ExpiredInteractions} expired interactions", removedCount); } private struct RegisteredInteraction { public Instant Expiry; public Func Callback; } public void Dispose() { _cts.Cancel(); _cts.Dispose(); } } }