Port some things, still does not compile
This commit is contained in:
@@ -8,7 +8,8 @@ using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using App.Metrics;
|
||||
|
||||
using Discord;
|
||||
using DSharpPlus.Entities;
|
||||
using DSharpPlus.Exceptions;
|
||||
|
||||
using Humanizer;
|
||||
|
||||
@@ -44,13 +45,13 @@ namespace PluralKit.Bot
|
||||
_logger = logger.ForContext<WebhookExecutorService>();
|
||||
}
|
||||
|
||||
public async Task<ulong> ExecuteWebhook(ITextChannel channel, string name, string avatarUrl, string content, IReadOnlyCollection<IAttachment> attachments)
|
||||
public async Task<ulong> ExecuteWebhook(DiscordChannel channel, string name, string avatarUrl, string content, IReadOnlyList<DiscordAttachment> attachments)
|
||||
{
|
||||
_logger.Verbose("Invoking webhook in channel {Channel}", channel.Id);
|
||||
|
||||
// Get a webhook, execute it
|
||||
var webhook = await _webhookCache.GetWebhook(channel);
|
||||
var id = await ExecuteWebhookInner(webhook, name, avatarUrl, content, attachments);
|
||||
var id = await ExecuteWebhookInner(channel, webhook, name, avatarUrl, content, attachments);
|
||||
|
||||
// Log the relevant metrics
|
||||
_metrics.Measure.Meter.Mark(BotMetrics.MessagesProxied);
|
||||
@@ -60,112 +61,93 @@ namespace PluralKit.Bot
|
||||
return id;
|
||||
}
|
||||
|
||||
private async Task<ulong> ExecuteWebhookInner(IWebhook webhook, string name, string avatarUrl, string content,
|
||||
IReadOnlyCollection<IAttachment> attachments, bool hasRetried = false)
|
||||
private async Task<ulong> ExecuteWebhookInner(DiscordChannel channel, DiscordWebhook webhook, string name, string avatarUrl, string content,
|
||||
IReadOnlyList<DiscordAttachment> attachments, bool hasRetried = false)
|
||||
{
|
||||
using var mfd = new MultipartFormDataContent
|
||||
{
|
||||
{new StringContent(content.Truncate(2000)), "content"},
|
||||
{new StringContent(FixClyde(name).Truncate(80)), "username"}
|
||||
};
|
||||
if (avatarUrl != null) mfd.Add(new StringContent(avatarUrl), "avatar_url");
|
||||
|
||||
var dwb = new DiscordWebhookBuilder();
|
||||
dwb.WithUsername(FixClyde(name).Truncate(80));
|
||||
dwb.WithContent(content.Truncate(2000));
|
||||
if (avatarUrl != null) 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.Size).Sum() / 1024 / 1024, attachmentChunks.Count);
|
||||
await AddAttachmentsToMultipart(mfd, attachmentChunks.First());
|
||||
_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]);
|
||||
}
|
||||
|
||||
mfd.Headers.Add("X-RateLimit-Precision", "millisecond"); // Need this for better rate limit support
|
||||
|
||||
// Adding this check as close to the actual send call as possible to prevent potential race conditions (unlikely, but y'know)
|
||||
if (!_rateLimit.TryExecuteWebhook(webhook))
|
||||
throw new WebhookRateLimited();
|
||||
|
||||
var timerCtx = _metrics.Measure.Timer.Time(BotMetrics.WebhookResponseTime);
|
||||
using var response = await _client.PostAsync($"{DiscordConfig.APIUrl}webhooks/{webhook.Id}/{webhook.Token}?wait=true", mfd);
|
||||
timerCtx.Dispose();
|
||||
|
||||
_rateLimit.UpdateRateLimitInfo(webhook, response);
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.TooManyRequests)
|
||||
// Rate limits should be respected, we bail early (already updated the limit info so we hopefully won't hit this again)
|
||||
throw new WebhookRateLimited();
|
||||
|
||||
var responseString = await response.Content.ReadAsStringAsync();
|
||||
|
||||
JObject responseJson;
|
||||
DiscordMessage response;
|
||||
try
|
||||
{
|
||||
responseJson = JsonConvert.DeserializeObject<JObject>(responseString);
|
||||
response = await webhook.ExecuteAsync(dwb);
|
||||
}
|
||||
catch (JsonReaderException)
|
||||
catch (NotFoundException e)
|
||||
{
|
||||
// Sometimes we get invalid JSON from the server, just ignore all of it
|
||||
throw new WebhookExecutionErrorOnDiscordsEnd();
|
||||
}
|
||||
|
||||
if (responseJson.ContainsKey("code"))
|
||||
{
|
||||
var errorCode = responseJson["code"].Value<int>();
|
||||
if (errorCode == 10015 && !hasRetried)
|
||||
if (e.JsonMessage.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);
|
||||
return await ExecuteWebhookInner(await _webhookCache.InvalidateAndRefreshWebhook(webhook), name, avatarUrl, content, attachments, hasRetried: true);
|
||||
}
|
||||
|
||||
if (errorCode == 40005)
|
||||
throw Errors.AttachmentTooLarge; // should be caught by the check above but just makin' sure
|
||||
|
||||
// TODO: look into what this actually throws, and if this is the correct handling
|
||||
if ((int) response.StatusCode >= 500)
|
||||
// If it's a 5xx error code, this is on Discord's end, so we throw an execution exception
|
||||
throw new WebhookExecutionErrorOnDiscordsEnd();
|
||||
|
||||
// Otherwise, this is going to throw on 4xx, and bubble up to our Sentry handler
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
// If we have any leftover attachment chunks, send those
|
||||
if (attachmentChunks.Count > 1)
|
||||
{
|
||||
// Deliberately not adding a content, just the remaining files
|
||||
foreach (var chunk in attachmentChunks.Skip(1))
|
||||
{
|
||||
using var mfd2 = new MultipartFormDataContent();
|
||||
mfd2.Add(new StringContent(FixClyde(name).Truncate(80)), "username");
|
||||
if (avatarUrl != null) mfd2.Add(new StringContent(avatarUrl), "avatar_url");
|
||||
await AddAttachmentsToMultipart(mfd2, chunk);
|
||||
|
||||
// Don't bother with ?wait, we're just kinda firehosing this stuff
|
||||
// also don't error check, the real message itself is already sent
|
||||
await _client.PostAsync($"{DiscordConfig.APIUrl}webhooks/{webhook.Id}/{webhook.Token}", mfd2);
|
||||
var newWebhook = await _webhookCache.InvalidateAndRefreshWebhook(channel, webhook);
|
||||
return await ExecuteWebhookInner(channel, newWebhook, name, avatarUrl, content, attachments, hasRetried: true);
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
|
||||
timerCtx.Dispose();
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
private async Task TrySendRemainingAttachments(DiscordWebhook webhook, string name, string avatarUrl, IReadOnlyList<IReadOnlyCollection<DiscordAttachment>> attachmentChunks)
|
||||
{
|
||||
if (attachmentChunks.Count <= 1) return;
|
||||
|
||||
for (var i = 1; i < attachmentChunks.Count; i++)
|
||||
{
|
||||
var dwb = new DiscordWebhookBuilder();
|
||||
if (avatarUrl != null) dwb.WithAvatarUrl(avatarUrl);
|
||||
dwb.WithUsername(name);
|
||||
await AddAttachmentsToBuilder(dwb, attachmentChunks[i]);
|
||||
await webhook.ExecuteAsync(dwb);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AddAttachmentsToBuilder(DiscordWebhookBuilder dwb, IReadOnlyCollection<DiscordAttachment> attachments)
|
||||
{
|
||||
async Task<(DiscordAttachment, Stream)> GetStream(DiscordAttachment attachment)
|
||||
{
|
||||
var attachmentResponse = await _client.GetAsync(attachment.Url, HttpCompletionOption.ResponseHeadersRead);
|
||||
return (attachment, await attachmentResponse.Content.ReadAsStreamAsync());
|
||||
}
|
||||
|
||||
// At this point we're sure we have a 2xx status code, so just assume success
|
||||
// TODO: can we do this without a round-trip to a string?
|
||||
return responseJson["id"].Value<ulong>();
|
||||
foreach (var (attachment, attachmentStream) in await Task.WhenAll(attachments.Select(GetStream)))
|
||||
dwb.AddFile(attachment.FileName, attachmentStream);
|
||||
}
|
||||
private IReadOnlyCollection<IReadOnlyCollection<IAttachment>> ChunkAttachmentsOrThrow(
|
||||
IReadOnlyCollection<IAttachment> attachments, int sizeThreshold)
|
||||
|
||||
private IReadOnlyList<IReadOnlyCollection<DiscordAttachment>> ChunkAttachmentsOrThrow(
|
||||
IReadOnlyList<DiscordAttachment> attachments, int sizeThreshold)
|
||||
{
|
||||
// Splits a list of attachments into "chunks" of at most 8MB each
|
||||
// If any individual attachment is larger than 8MB, will throw an error
|
||||
var chunks = new List<List<IAttachment>>();
|
||||
var list = new List<IAttachment>();
|
||||
var chunks = new List<List<DiscordAttachment>>();
|
||||
var list = new List<DiscordAttachment>();
|
||||
|
||||
foreach (var attachment in attachments)
|
||||
{
|
||||
if (attachment.Size >= sizeThreshold) throw Errors.AttachmentTooLarge;
|
||||
if (attachment.FileSize >= sizeThreshold) throw Errors.AttachmentTooLarge;
|
||||
|
||||
if (list.Sum(a => a.Size) + attachment.Size >= sizeThreshold)
|
||||
if (list.Sum(a => a.FileSize) + attachment.FileSize >= sizeThreshold)
|
||||
{
|
||||
chunks.Add(list);
|
||||
list = new List<IAttachment>();
|
||||
list = new List<DiscordAttachment>();
|
||||
}
|
||||
|
||||
list.Add(attachment);
|
||||
@@ -175,20 +157,6 @@ namespace PluralKit.Bot
|
||||
return chunks;
|
||||
}
|
||||
|
||||
private async Task AddAttachmentsToMultipart(MultipartFormDataContent content,
|
||||
IReadOnlyCollection<IAttachment> attachments)
|
||||
{
|
||||
async Task<(IAttachment, Stream)> GetStream(IAttachment attachment)
|
||||
{
|
||||
var attachmentResponse = await _client.GetAsync(attachment.Url, HttpCompletionOption.ResponseHeadersRead);
|
||||
return (attachment, await attachmentResponse.Content.ReadAsStreamAsync());
|
||||
}
|
||||
|
||||
var attachmentId = 0;
|
||||
foreach (var (attachment, attachmentStream) in await Task.WhenAll(attachments.Select(GetStream)))
|
||||
content.Add(new StreamContent(attachmentStream), $"file{attachmentId++}", attachment.Filename);
|
||||
}
|
||||
|
||||
private string FixClyde(string name)
|
||||
{
|
||||
// Check if the name contains "Clyde" - if not, do nothing
|
||||
|
Reference in New Issue
Block a user