From bc440abd445d58aa5bbe2c8f551c43d54aecbdc8 Mon Sep 17 00:00:00 2001 From: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Wed, 24 Nov 2021 12:52:55 -0400 Subject: [PATCH] Implement multi-file upload to webhooks (#1942) --- .../API/Rest/UploadWebhookFileParams.cs | 38 +++++++++---- .../DiscordWebhookClient.cs | 29 ++++++++-- .../WebhookClientHelper.cs | 53 ++++++++++++++----- 3 files changed, 92 insertions(+), 28 deletions(-) diff --git a/src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs b/src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs index d925e0108..3d09ad145 100644 --- a/src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs +++ b/src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs @@ -11,9 +11,8 @@ namespace Discord.API.Rest { private static JsonSerializer _serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() }; - public Stream File { get; } + public FileAttachment[] Files { get; } - public Optional Filename { get; set; } public Optional Content { get; set; } public Optional Nonce { get; set; } public Optional IsTTS { get; set; } @@ -21,22 +20,16 @@ namespace Discord.API.Rest public Optional AvatarUrl { get; set; } public Optional Embeds { get; set; } public Optional AllowedMentions { get; set; } + public Optional MessageComponents { get; set; } - public bool IsSpoiler { get; set; } = false; - - public UploadWebhookFileParams(Stream file) + public UploadWebhookFileParams(params FileAttachment[] files) { - File = file; + Files = files; } public IReadOnlyDictionary ToDictionary() { var d = new Dictionary(); - var filename = Filename.GetValueOrDefault("unknown.dat"); - if (IsSpoiler && !filename.StartsWith(AttachmentExtensions.SpoilerPrefix)) - filename = filename.Insert(0, AttachmentExtensions.SpoilerPrefix); - - d["file"] = new MultipartFile(File, filename); var payload = new Dictionary(); if (Content.IsSpecified) @@ -49,11 +42,34 @@ namespace Discord.API.Rest payload["username"] = Username.Value; if (AvatarUrl.IsSpecified) payload["avatar_url"] = AvatarUrl.Value; + if (MessageComponents.IsSpecified) + payload["components"] = MessageComponents.Value; if (Embeds.IsSpecified) payload["embeds"] = Embeds.Value; if (AllowedMentions.IsSpecified) payload["allowed_mentions"] = AllowedMentions.Value; + List attachments = new(); + + for (int n = 0; n != Files.Length; n++) + { + var attachment = Files[n]; + + var filename = attachment.FileName ?? "unknown.dat"; + if (attachment.IsSpoiler && !filename.StartsWith(AttachmentExtensions.SpoilerPrefix)) + filename = filename.Insert(0, AttachmentExtensions.SpoilerPrefix); + d[$"files[{n}]"] = new MultipartFile(attachment.Stream, filename); + + attachments.Add(new + { + id = (ulong)n, + filename = filename, + description = attachment.Description ?? Optional.Unspecified + }); + } + + payload["attachments"] = attachments; + var json = new StringBuilder(); using (var text = new StringWriter(json)) using (var writer = new JsonTextWriter(text)) diff --git a/src/Discord.Net.Webhook/DiscordWebhookClient.cs b/src/Discord.Net.Webhook/DiscordWebhookClient.cs index a4fdf9179..f7ad7301c 100644 --- a/src/Discord.Net.Webhook/DiscordWebhookClient.cs +++ b/src/Discord.Net.Webhook/DiscordWebhookClient.cs @@ -123,14 +123,35 @@ namespace Discord.Webhook /// Returns the ID of the created message. public Task SendFileAsync(string filePath, string text, bool isTTS = false, IEnumerable embeds = null, string username = null, string avatarUrl = null, - RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null) - => WebhookClientHelper.SendFileAsync(this, filePath, text, isTTS, embeds, username, avatarUrl, allowedMentions, options, isSpoiler); + RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, + MessageComponent components = null) + => WebhookClientHelper.SendFileAsync(this, filePath, text, isTTS, embeds, username, avatarUrl, + allowedMentions, options, isSpoiler, components); /// Sends a message to the channel for this webhook with an attachment. /// Returns the ID of the created message. public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, IEnumerable embeds = null, string username = null, string avatarUrl = null, - RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null) - => WebhookClientHelper.SendFileAsync(this, stream, filename, text, isTTS, embeds, username, avatarUrl, allowedMentions, options, isSpoiler); + RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, + MessageComponent components = null) + => WebhookClientHelper.SendFileAsync(this, stream, filename, text, isTTS, embeds, username, + avatarUrl, allowedMentions, options, isSpoiler, components); + + /// Sends a message to the channel for this webhook with an attachment. + /// Returns the ID of the created message. + public Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, + IEnumerable embeds = null, string username = null, string avatarUrl = null, + RequestOptions options = null, AllowedMentions allowedMentions = null, MessageComponent components = null) + => WebhookClientHelper.SendFileAsync(this, attachment, text, isTTS, embeds, username, + avatarUrl, allowedMentions, components, options); + + /// Sends a message to the channel for this webhook with an attachment. + /// Returns the ID of the created message. + public Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, + IEnumerable embeds = null, string username = null, string avatarUrl = null, + RequestOptions options = null, AllowedMentions allowedMentions = null, MessageComponent components = null) + => WebhookClientHelper.SendFilesAsync(this, attachments, text, isTTS, embeds, username, avatarUrl, + allowedMentions, components, options); + /// Modifies the properties of this webhook. public Task ModifyWebhookAsync(Action func, RequestOptions options = null) diff --git a/src/Discord.Net.Webhook/WebhookClientHelper.cs b/src/Discord.Net.Webhook/WebhookClientHelper.cs index 6e3651323..8b4bb5d2a 100644 --- a/src/Discord.Net.Webhook/WebhookClientHelper.cs +++ b/src/Discord.Net.Webhook/WebhookClientHelper.cs @@ -97,24 +97,51 @@ namespace Discord.Webhook await client.ApiClient.DeleteWebhookMessageAsync(client.Webhook.Id, messageId, options).ConfigureAwait(false); } public static async Task SendFileAsync(DiscordWebhookClient client, string filePath, string text, bool isTTS, - IEnumerable embeds, string username, string avatarUrl, AllowedMentions allowedMentions, RequestOptions options, bool isSpoiler) + IEnumerable embeds, string username, string avatarUrl, AllowedMentions allowedMentions, RequestOptions options, bool isSpoiler, MessageComponent components) { string filename = Path.GetFileName(filePath); using (var file = File.OpenRead(filePath)) - return await SendFileAsync(client, file, filename, text, isTTS, embeds, username, avatarUrl, allowedMentions, options, isSpoiler).ConfigureAwait(false); + return await SendFileAsync(client, file, filename, text, isTTS, embeds, username, avatarUrl, allowedMentions, options, isSpoiler, components).ConfigureAwait(false); } - public static async Task SendFileAsync(DiscordWebhookClient client, Stream stream, string filename, string text, bool isTTS, - IEnumerable embeds, string username, string avatarUrl, AllowedMentions allowedMentions, RequestOptions options, bool isSpoiler) + public static Task SendFileAsync(DiscordWebhookClient client, Stream stream, string filename, string text, bool isTTS, + IEnumerable embeds, string username, string avatarUrl, AllowedMentions allowedMentions, RequestOptions options, bool isSpoiler, + MessageComponent components) + => SendFileAsync(client, new FileAttachment(stream, filename, isSpoiler: isSpoiler), text, isTTS, embeds, username, avatarUrl, allowedMentions, components, options); + + public static Task SendFileAsync(DiscordWebhookClient client, FileAttachment attachment, string text, bool isTTS, IEnumerable embeds, string username, string avatarUrl, AllowedMentions allowedMentions, MessageComponent components, RequestOptions options) + => SendFilesAsync(client, new FileAttachment[] { attachment }, text, isTTS, embeds, username, avatarUrl, allowedMentions, components, options); + + public static async Task SendFilesAsync(DiscordWebhookClient client, + IEnumerable attachments, string text, bool isTTS, IEnumerable embeds, string username, string avatarUrl, AllowedMentions allowedMentions, MessageComponent components, RequestOptions options) { - var args = new UploadWebhookFileParams(stream) { Filename = filename, Content = text, IsTTS = isTTS, IsSpoiler = isSpoiler }; - if (username != null) - args.Username = username; - if (avatarUrl != null) - args.AvatarUrl = avatarUrl; - if (embeds != null) - args.Embeds = embeds.Select(x => x.ToModel()).ToArray(); - if(allowedMentions != null) - args.AllowedMentions = allowedMentions.ToModel(); + embeds ??= Array.Empty(); + + Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed."); + Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed."); + Preconditions.AtMost(embeds.Count(), 10, nameof(embeds), "A max of 10 embeds are allowed."); + + foreach (var attachment in attachments) + { + Preconditions.NotNullOrEmpty(attachment.FileName, nameof(attachment.FileName), "File Name must not be empty or null"); + } + + // check that user flag and user Id list are exclusive, same with role flag and role Id list + if (allowedMentions != null && allowedMentions.AllowedTypes.HasValue) + { + if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Users) && + allowedMentions.UserIds != null && allowedMentions.UserIds.Count > 0) + { + throw new ArgumentException("The Users flag is mutually exclusive with the list of User Ids.", nameof(allowedMentions)); + } + + if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Roles) && + allowedMentions.RoleIds != null && allowedMentions.RoleIds.Count > 0) + { + throw new ArgumentException("The Roles flag is mutually exclusive with the list of Role Ids.", nameof(allowedMentions)); + } + } + + var args = new UploadWebhookFileParams(attachments.ToArray()) {AvatarUrl = avatarUrl, Username = username, Content = text, IsTTS = isTTS, Embeds = embeds.Any() ? embeds.Select(x => x.ToModel()).ToArray() : Optional.Unspecified, AllowedMentions = allowedMentions?.ToModel() ?? Optional.Unspecified, MessageComponents = components?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified }; var msg = await client.ApiClient.UploadWebhookFileAsync(client.Webhook.Id, args, options).ConfigureAwait(false); return msg.Id; }