private async Task TrySendRemainingAttachments(Webhook webhook, string name, string avatarUrl,
                                                   IReadOnlyList <IReadOnlyCollection <Message.Attachment> >
                                                   attachmentChunks, ulong?threadId)
    {
        if (attachmentChunks.Count <= 1)
        {
            return;
        }

        for (var i = 1; i < attachmentChunks.Count; i++)
        {
            var files = await GetAttachmentFiles(attachmentChunks[i]);

            var req = new ExecuteWebhookRequest
            {
                Username    = name,
                AvatarUrl   = avatarUrl,
                Attachments = files.Select(f => new Message.Attachment
                {
                    Id          = (ulong)Array.IndexOf(files, f),
                    Description = f.Description,
                    Filename    = f.Filename
                }).ToArray()
            };
            await _rest.ExecuteWebhook(webhook.Id, webhook.Token !, req, files, threadId);
        }
    }
    private async Task <Message> ExecuteWebhookInner(Webhook webhook, ProxyRequest req, bool hasRetried = false)
    {
        var guild = await _cache.GetGuild(req.GuildId);

        var content = req.Content.Truncate(2000);

        var allowedMentions = content.ParseMentions();

        if (!req.AllowEveryone)
        {
            allowedMentions = allowedMentions.RemoveUnmentionableRoles(guild) with
            {
                // also clear @everyones
                Parse = Array.Empty <AllowedMentions.ParseType>()
            }
        }
        ;

        var webhookReq = new ExecuteWebhookRequest
        {
            Username        = FixProxyName(req.Name).Truncate(80),
            Content         = content,
            AllowedMentions = allowedMentions,
            AvatarUrl       = !string.IsNullOrWhiteSpace(req.AvatarUrl) ? req.AvatarUrl : null,
            Embeds          = req.Embeds,
            Stickers        = req.Stickers,
        };

        MultipartFile[] files            = null;
        var             attachmentChunks = ChunkAttachmentsOrThrow(req.Attachments, req.FileSizeLimit);

        if (attachmentChunks.Count > 0)
        {
            _logger.Information(
                "Invoking webhook with {AttachmentCount} attachments totalling {AttachmentSize} MiB in {AttachmentChunks} chunks",
                req.Attachments.Length, req.Attachments.Select(a => a.Size).Sum() / 1024 / 1024,
                attachmentChunks.Count);
            files = await GetAttachmentFiles(attachmentChunks[0]);

            webhookReq.Attachments = files.Select(f => new Message.Attachment
            {
                Id          = (ulong)Array.IndexOf(files, f),
                Description = f.Description,
                Filename    = f.Filename
            }).ToArray();
        }

        Message webhookMessage;

        using (_metrics.Measure.Timer.Time(BotMetrics.WebhookResponseTime))
        {
            try
            {
                webhookMessage =
                    await _rest.ExecuteWebhook(webhook.Id, webhook.Token, webhookReq, files, req.ThreadId);
            }
            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)
            {
                if (e.ErrorCode == 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} (thread {ThreadId})",
                                    webhook.Id, webhook.ChannelId, req.ThreadId);

                    var newWebhook = await _webhookCache.InvalidateAndRefreshWebhook(req.ChannelId, webhook);

                    return(await ExecuteWebhookInner(newWebhook, req, 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, req.Name, req.AvatarUrl, attachmentChunks, req.ThreadId);

        return(webhookMessage);
    }