private async Task <ulong> ExecuteWebhookInner(DiscordChannel channel, DiscordWebhook webhook, string name, string avatarUrl, string content, IReadOnlyList <DiscordAttachment> attachments, bool allowEveryone, bool hasRetried = false) { content = content.Truncate(2000); var dwb = new DiscordWebhookBuilder(); dwb.WithUsername(FixClyde(name).Truncate(80)); dwb.WithContent(content); dwb.AddMentions(content.ParseAllMentions(allowEveryone, channel.Guild)); if (!string.IsNullOrWhiteSpace(avatarUrl)) { dwb.WithAvatarUrl(avatarUrl); } var attachmentChunks = ChunkAttachmentsOrThrow(attachments, 8 * 1024 * 1024); if (attachmentChunks.Count > 0) { _logger.Information("Invoking webhook with {AttachmentCount} attachments totalling {AttachmentSize} MiB in {AttachmentChunks} chunks", attachments.Count, attachments.Select(a => a.FileSize).Sum() / 1024 / 1024, attachmentChunks.Count); await AddAttachmentsToBuilder(dwb, attachmentChunks[0]); } DiscordMessage response; using (_metrics.Measure.Timer.Time(BotMetrics.WebhookResponseTime)) { try { response = await webhook.ExecuteAsync(dwb); } catch (JsonReaderException) { // This happens sometimes when we hit a CloudFlare error (or similar) on Discord's end // Nothing we can do about this - happens sometimes under server load, so just drop the message and give up throw new WebhookExecutionErrorOnDiscordsEnd(); } catch (NotFoundException e) { var errorText = e.WebResponse?.Response; if (errorText != null && errorText.Contains("10015") && !hasRetried) { // Error 10015 = "Unknown Webhook" - this likely means the webhook was deleted // but is still in our cache. Invalidate, refresh, try again _logger.Warning("Error invoking webhook {Webhook} in channel {Channel}", webhook.Id, webhook.ChannelId); var newWebhook = await _webhookCache.InvalidateAndRefreshWebhook(channel, webhook); return(await ExecuteWebhookInner(channel, newWebhook, name, avatarUrl, content, attachments, allowEveryone, hasRetried : true)); } throw; } } // We don't care about whether the sending succeeds, and we don't want to *wait* for it, so we just fork it off var _ = TrySendRemainingAttachments(webhook, name, avatarUrl, attachmentChunks); return(response.Id); }