using System; using System.Collections.Concurrent; using System.Threading.Tasks; using App.Metrics; using Myriad.Builders; using Myriad.Rest; using NodaTime; using Serilog; namespace PluralKit.Bot { public class ErrorMessageService { private static readonly Duration MinErrorInterval = Duration.FromSeconds(10); private readonly ConcurrentDictionary _lastErrorInChannel = new ConcurrentDictionary(); private readonly IMetrics _metrics; private readonly ILogger _logger; private readonly DiscordApiClient _rest; public ErrorMessageService(IMetrics metrics, ILogger logger, DiscordApiClient rest) { _metrics = metrics; _logger = logger; _rest = rest; } public async Task SendErrorMessage(ulong channelId, string errorId) { var now = SystemClock.Instance.GetCurrentInstant(); if (!ShouldSendErrorMessage(channelId, now)) { _logger.Warning("Rate limited sending error message to {ChannelId} with error code {ErrorId}", channelId, errorId); _metrics.Measure.Meter.Mark(BotMetrics.ErrorMessagesSent, "throttled"); return; } var embed = new EmbedBuilder() .Color(0xE74C3C) .Title("Internal error occurred") .Description("For support, please send the error code above in **#bug-reports-and-errors** on **[the support server *(click to join)*](https://discord.gg/PczBt78)** with a description of what you were doing at the time.") .Footer(new(errorId)) .Timestamp(now.ToDateTimeOffset().ToString("O")); try { await _rest.CreateMessage(channelId, new() { Content = $"> **Error code:** `{errorId}`", Embed = embed.Build() }); _logger.Information("Sent error message to {ChannelId} with error code {ErrorId}", channelId, errorId); _metrics.Measure.Meter.Mark(BotMetrics.ErrorMessagesSent, "sent"); } catch (Exception e) { _logger.Error(e, "Error sending error message to {ChannelId}", channelId); _metrics.Measure.Meter.Mark(BotMetrics.ErrorMessagesSent, "failed"); throw; } } private bool ShouldSendErrorMessage(ulong channelId, Instant now) { if (_lastErrorInChannel.TryGetValue(channelId, out var lastErrorTime)) { var interval = now - lastErrorTime; if (interval < MinErrorInterval) return false; } _lastErrorInChannel[channelId] = now; return true; } } }