diff --git a/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs b/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs index dc2441f6..7bd9fabf 100644 --- a/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs @@ -31,7 +31,8 @@ namespace Discord /// The message components to be included with this message. Used for interactions. /// A collection of stickers to send with the message. /// A array of s to send with this response. Max 10. - /// A message flag to be applied to the sent message, only is permitted. + /// A message flag to be applied to the sent message, only + /// and is permitted. /// /// A task that represents an asynchronous send operation for delivering the message. The task result /// contains the sent message. @@ -72,7 +73,7 @@ namespace Discord /// The message components to be included with this message. Used for interactions. /// A collection of stickers to send with the file. /// A array of s to send with this response. Max 10. - /// A message flag to be applied to the sent message, only is permitted. + /// A message flag to be applied to the sent message, only and is permitted. /// /// A task that represents an asynchronous send operation for delivering the message. The task result /// contains the sent message. @@ -110,7 +111,7 @@ namespace Discord /// The message components to be included with this message. Used for interactions. /// A collection of stickers to send with the file. /// A array of s to send with this response. Max 10. - /// A message flag to be applied to the sent message, only is permitted. + /// A message flag to be applied to the sent message, only and is permitted. /// /// A task that represents an asynchronous send operation for delivering the message. The task result /// contains the sent message. @@ -140,7 +141,7 @@ namespace Discord /// The message components to be included with this message. Used for interactions. /// A collection of stickers to send with the file. /// A array of s to send with this response. Max 10. - /// A message flag to be applied to the sent message, only is permitted. + /// A message flag to be applied to the sent message, only and is permitted. /// /// A task that represents an asynchronous send operation for delivering the message. The task result /// contains the sent message. @@ -170,7 +171,7 @@ namespace Discord /// The message components to be included with this message. Used for interactions. /// A collection of stickers to send with the file. /// A array of s to send with this response. Max 10. - /// A message flag to be applied to the sent message, only is permitted. + /// A message flag to be applied to the sent message, only and is permitted. /// /// A task that represents an asynchronous send operation for delivering the message. The task result /// contains the sent message. diff --git a/src/Discord.Net.Core/Utils/Preconditions.cs b/src/Discord.Net.Core/Utils/Preconditions.cs index 37bd82df..c3f2785d 100644 --- a/src/Discord.Net.Core/Utils/Preconditions.cs +++ b/src/Discord.Net.Core/Utils/Preconditions.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; namespace Discord { @@ -69,6 +71,50 @@ namespace Discord private static ArgumentException CreateNotEmptyException(string name, string msg) => new ArgumentException(message: msg ?? "Argument cannot be blank.", paramName: name); + + #endregion + + #region Message Validation + + public static void WebhookMessageAtLeastOneOf(string text = null, MessageComponent components = null, ICollection embeds = null, + IEnumerable attachments = null) + { + if (!string.IsNullOrEmpty(text)) + return; + + if (components != null && components.Components.Count != 0) + return; + + if (attachments != null && attachments.Count() != 0) + return; + + if (embeds != null && embeds.Count != 0) + return; + + throw new ArgumentException($"At least one of 'Content', 'Embeds', 'Components' or 'Attachments' must be specified."); + } + + public static void MessageAtLeastOneOf(string text = null, MessageComponent components = null, ICollection embeds = null, + ICollection stickers = null, IEnumerable attachments = null) + { + if (!string.IsNullOrEmpty(text)) + return; + + if (components != null && components.Components.Count != 0) + return; + + if (stickers != null && stickers.Count != 0) + return; + + if (attachments != null && attachments.Count() != 0) + return; + + if (embeds != null && embeds.Count != 0) + return; + + throw new ArgumentException($"At least one of 'Content', 'Embeds', 'Components', 'Stickers' or 'Attachments' must be specified."); + } + #endregion #region Numerics diff --git a/src/Discord.Net.Rest/API/Rest/CreateMessageParams.cs b/src/Discord.Net.Rest/API/Rest/CreateMessageParams.cs index 7b0d842d..a79c7f70 100644 --- a/src/Discord.Net.Rest/API/Rest/CreateMessageParams.cs +++ b/src/Discord.Net.Rest/API/Rest/CreateMessageParams.cs @@ -6,7 +6,7 @@ namespace Discord.API.Rest internal class CreateMessageParams { [JsonProperty("content")] - public string Content { get; } + public Optional Content { get; set; } [JsonProperty("nonce")] public Optional Nonce { get; set; } @@ -31,10 +31,5 @@ namespace Discord.API.Rest [JsonProperty("flags")] public Optional Flags { get; set; } - - public CreateMessageParams(string content) - { - Content = content; - } } } diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index cd7d18a4..6c011676 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -816,16 +816,16 @@ namespace Discord.API endpoint = () => $"channels/{channelId}/messages?limit={limit}"; return await SendAsync>("GET", endpoint, ids, options: options).ConfigureAwait(false); } + /// Message content is too long, length must be less or equal to . public async Task CreateMessageAsync(ulong channelId, CreateMessageParams args, RequestOptions options = null) { Preconditions.NotNull(args, nameof(args)); Preconditions.NotEqual(channelId, 0, nameof(channelId)); - if ((!args.Embeds.IsSpecified || args.Embeds.Value == null || args.Embeds.Value.Length == 0) && (!args.Stickers.IsSpecified || args.Stickers.Value == null || args.Stickers.Value.Length == 0)) - Preconditions.NotNullOrEmpty(args.Content, nameof(args.Content)); - if (args.Content?.Length > DiscordConfig.MaxMessageSize) - throw new ArgumentException(message: $"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", paramName: nameof(args.Content)); + if (args.Content.IsSpecified && args.Content.Value is not null) + Preconditions.AtMost(args.Content.Value.Length, DiscordConfig.MaxMessageSize, nameof(args.Content), $"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}."); + options = RequestOptions.CreateOrClone(options); var ids = new BucketIds(channelId: channelId); @@ -840,13 +840,10 @@ namespace Discord.API if (AuthTokenType != TokenType.Webhook) throw new InvalidOperationException($"This operation may only be called with a {nameof(TokenType.Webhook)} token."); - Preconditions.NotNull(args, nameof(args)); - Preconditions.NotEqual(webhookId, 0, nameof(webhookId)); - if (!args.Embeds.IsSpecified || args.Embeds.Value == null || args.Embeds.Value.Length == 0) - Preconditions.NotNullOrEmpty(args.Content, nameof(args.Content)); - - if (args.Content.IsSpecified && args.Content.Value?.Length > DiscordConfig.MaxMessageSize) - throw new ArgumentException(message: $"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", paramName: nameof(args.Content)); + if (args.Embeds.IsSpecified) + Preconditions.AtMost(args.Embeds.Value.Length, DiscordConfig.MaxEmbedsPerMessage, nameof(args.Embeds), "A max of 10 Embeds are allowed."); + if (args.Content.IsSpecified && args.Content.Value is not null) + Preconditions.AtMost(args.Content.Value.Length, DiscordConfig.MaxMessageSize, nameof(args.Content), $"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}."); options = RequestOptions.CreateOrClone(options); @@ -866,15 +863,38 @@ namespace Discord.API Preconditions.NotEqual(messageId, 0, nameof(messageId)); if (args.Embeds.IsSpecified) - Preconditions.AtMost(args.Embeds.Value.Length, 10, nameof(args.Embeds), "A max of 10 Embeds are allowed."); - if (args.Content.IsSpecified && args.Content.Value.Length > DiscordConfig.MaxMessageSize) - throw new ArgumentException(message: $"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", paramName: nameof(args.Content)); + Preconditions.AtMost(args.Embeds.Value.Length, DiscordConfig.MaxEmbedsPerMessage, nameof(args.Embeds), $"A max of {DiscordConfig.MaxEmbedsPerMessage} Embeds are allowed."); + if (args.Content.IsSpecified && args.Content.Value is not null) + Preconditions.AtMost(args.Content.Value.Length, DiscordConfig.MaxMessageSize, nameof(args.Content), $"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}."); + options = RequestOptions.CreateOrClone(options); var ids = new BucketIds(webhookId: webhookId); await SendJsonAsync("PATCH", () => $"webhooks/{webhookId}/{AuthToken}/messages/{messageId}?{WebhookQuery(false, threadId)}", args, ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); } + /// Message content is too long, length must be less or equal to . + /// This operation may only be called with a token. + public async Task ModifyWebhookMessageAsync(ulong webhookId, ulong messageId, UploadWebhookFileParams args, RequestOptions options = null, ulong? threadId = null) + { + if (AuthTokenType != TokenType.Webhook) + throw new InvalidOperationException($"This operation may only be called with a {nameof(TokenType.Webhook)} token."); + + Preconditions.NotNull(args, nameof(args)); + Preconditions.NotEqual(webhookId, 0, nameof(webhookId)); + Preconditions.NotEqual(messageId, 0, nameof(messageId)); + + if (args.Embeds.IsSpecified) + Preconditions.AtMost(args.Embeds.Value.Length, DiscordConfig.MaxEmbedsPerMessage, nameof(args.Embeds), $"A max of {DiscordConfig.MaxEmbedsPerMessage} Embeds are allowed."); + if (args.Content.IsSpecified && args.Content.Value is not null) + Preconditions.AtMost(args.Content.Value.Length, DiscordConfig.MaxMessageSize, nameof(args.Content), $"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}."); + + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(webhookId: webhookId); + await SendMultipartAsync("PATCH", () => $"webhooks/{webhookId}/{AuthToken}/messages/{messageId}?{WebhookQuery(false, threadId)}", args.ToDictionary(), ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); + } + /// This operation may only be called with a token. public async Task DeleteWebhookMessageAsync(ulong webhookId, ulong messageId, RequestOptions options = null, ulong? threadId = null) { @@ -897,10 +917,10 @@ namespace Discord.API Preconditions.NotEqual(channelId, 0, nameof(channelId)); options = RequestOptions.CreateOrClone(options); - if (args.Content.GetValueOrDefault(null) == null) - args.Content = ""; - else if (args.Content.IsSpecified && args.Content.Value?.Length > DiscordConfig.MaxMessageSize) - throw new ArgumentOutOfRangeException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content)); + if (args.Embeds.IsSpecified) + Preconditions.AtMost(args.Embeds.Value.Length, DiscordConfig.MaxEmbedsPerMessage, nameof(args.Embeds), $"A max of {DiscordConfig.MaxEmbedsPerMessage} Embeds are allowed."); + if (args.Content.IsSpecified && args.Content.Value is not null) + Preconditions.AtMost(args.Content.Value.Length, DiscordConfig.MaxMessageSize, nameof(args.Content), $"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}."); var ids = new BucketIds(channelId: channelId); return await SendMultipartAsync("POST", () => $"channels/{channelId}/messages", args.ToDictionary(), ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); @@ -917,19 +937,15 @@ namespace Discord.API Preconditions.NotEqual(webhookId, 0, nameof(webhookId)); options = RequestOptions.CreateOrClone(options); - if (args.Content.GetValueOrDefault(null) == null) - args.Content = ""; - else if (args.Content.IsSpecified) - { - if (args.Content.Value == null) - args.Content = ""; - if (args.Content.Value?.Length > DiscordConfig.MaxMessageSize) - throw new ArgumentOutOfRangeException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content)); - } + if (args.Embeds.IsSpecified) + Preconditions.AtMost(args.Embeds.Value.Length, DiscordConfig.MaxEmbedsPerMessage, nameof(args.Embeds), $"A max of {DiscordConfig.MaxEmbedsPerMessage} Embeds are allowed."); + if (args.Content.IsSpecified && args.Content.Value is not null) + Preconditions.AtMost(args.Content.Value.Length, DiscordConfig.MaxMessageSize, nameof(args.Content), $"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}."); var ids = new BucketIds(webhookId: webhookId); return await SendMultipartAsync("POST", () => $"webhooks/{webhookId}/{AuthToken}?{WebhookQuery(true, threadId)}", args.ToDictionary(), ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); } + public async Task DeleteMessageAsync(ulong channelId, ulong messageId, RequestOptions options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); @@ -939,6 +955,7 @@ namespace Discord.API var ids = new BucketIds(channelId: channelId); await SendAsync("DELETE", () => $"channels/{channelId}/messages/{messageId}", ids, options: options).ConfigureAwait(false); } + public async Task DeleteMessagesAsync(ulong channelId, DeleteMessagesParams args, RequestOptions options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); @@ -967,8 +984,12 @@ namespace Discord.API Preconditions.NotEqual(channelId, 0, nameof(channelId)); Preconditions.NotEqual(messageId, 0, nameof(messageId)); Preconditions.NotNull(args, nameof(args)); - if (args.Content.IsSpecified && args.Content.Value?.Length > DiscordConfig.MaxMessageSize) - throw new ArgumentOutOfRangeException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content)); + + if (args.Embeds.IsSpecified) + Preconditions.AtMost(args.Embeds.Value.Length, DiscordConfig.MaxEmbedsPerMessage, nameof(args.Embeds), "A max of 10 Embeds are allowed."); + if (args.Content.IsSpecified && args.Content.Value is not null) + Preconditions.AtMost(args.Content.Value.Length, DiscordConfig.MaxMessageSize, nameof(args.Content), $"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}."); + options = RequestOptions.CreateOrClone(options); var ids = new BucketIds(channelId: channelId); @@ -980,8 +1001,12 @@ namespace Discord.API Preconditions.NotEqual(channelId, 0, nameof(channelId)); Preconditions.NotEqual(messageId, 0, nameof(messageId)); Preconditions.NotNull(args, nameof(args)); - if (args.Content.IsSpecified && args.Content.Value?.Length > DiscordConfig.MaxMessageSize) - throw new ArgumentOutOfRangeException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content)); + + if (args.Embeds.IsSpecified) + Preconditions.AtMost(args.Embeds.Value.Length, DiscordConfig.MaxEmbedsPerMessage, nameof(args.Embeds), "A max of 10 Embeds are allowed."); + if (args.Content.IsSpecified && args.Content.Value is not null) + Preconditions.AtMost(args.Content.Value.Length, DiscordConfig.MaxMessageSize, nameof(args.Content), $"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}."); + options = RequestOptions.CreateOrClone(options); var ids = new BucketIds(channelId: channelId); @@ -1444,8 +1469,15 @@ namespace Discord.API public async Task CreateInteractionFollowupMessageAsync(CreateWebhookMessageParams args, string token, RequestOptions options = null) { - if ((!args.Embeds.IsSpecified || args.Embeds.Value == null || args.Embeds.Value.Length == 0) && !args.File.IsSpecified) - Preconditions.NotNullOrEmpty(args.Content, nameof(args.Content)); + if ((!args.Embeds.IsSpecified || args.Embeds.Value == null || args.Embeds.Value.Length == 0) + && (!args.Content.IsSpecified || args.Content.Value is null || string.IsNullOrWhiteSpace(args.Content.Value)) + && (!args.Components.IsSpecified || args.Components.Value is null || args.Components.Value.Length == 0)) + { + throw new ArgumentException("At least one of 'Content', 'Embeds', 'File' or 'Components' must be specified.", nameof(args)); + } + + if (args.Content.IsSpecified && args.Content.Value is not null && args.Content.Value.Length > DiscordConfig.MaxMessageSize) + throw new ArgumentException(message: $"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", paramName: nameof(args.Content)); if (args.Content.IsSpecified && args.Content.Value?.Length > DiscordConfig.MaxMessageSize) throw new ArgumentException(message: $"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", paramName: nameof(args.Content)); @@ -1460,9 +1492,12 @@ namespace Discord.API public async Task CreateInteractionFollowupMessageAsync(UploadWebhookFileParams args, string token, RequestOptions options = null) { - if ((!args.Embeds.IsSpecified || args.Embeds.Value == null || args.Embeds.Value.Length == 0) && !args.Files.Any()) - Preconditions.NotNullOrEmpty(args.Content, nameof(args.Content)); - + if ((!args.Embeds.IsSpecified || args.Embeds.Value == null || args.Embeds.Value.Length == 0) + && (!args.Content.IsSpecified || args.Content.Value is null || string.IsNullOrWhiteSpace(args.Content.Value)) + && (!args.MessageComponents.IsSpecified || args.MessageComponents.Value is null || args.MessageComponents.Value.Length == 0)) + { + throw new ArgumentException("At least one of 'Content', 'Embeds', 'Files' or 'Components' must be specified.", nameof(args)); + } if (args.Content.IsSpecified && args.Content.Value?.Length > DiscordConfig.MaxMessageSize) throw new ArgumentException(message: $"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", paramName: nameof(args.Content)); diff --git a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs index f04cb3d3..1cf2bdc5 100644 --- a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs +++ b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs @@ -281,7 +281,9 @@ namespace Discord.Rest 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.Length, 10, nameof(embeds), "A max of 10 embeds are allowed."); + Preconditions.AtMost(embeds.Length, DiscordConfig.MaxEmbedsPerMessage, nameof(embeds), $"A max of {DiscordConfig.MaxEmbedsPerMessage} Embeds are allowed."); + + Preconditions.MessageAtLeastOneOf(text, components, embeds, stickers); // check that user flag and user Id list are exclusive, same with role flag and role Id list if (allowedMentions != null && allowedMentions.AllowedTypes.HasValue) @@ -304,12 +306,12 @@ namespace Discord.Rest Preconditions.AtMost(stickers.Length, 3, nameof(stickers), "A max of 3 stickers are allowed."); } - - if (flags is not MessageFlags.None and not MessageFlags.SuppressEmbeds) + if (flags is not MessageFlags.None and not MessageFlags.SuppressEmbeds and not MessageFlags.SuppressNotification) throw new ArgumentException("The only valid MessageFlags are SuppressEmbeds and none.", nameof(flags)); - var args = new CreateMessageParams(text) + var args = new CreateMessageParams { + Content = text, IsTTS = isTTS, Embeds = embeds.Any() ? embeds.Select(x => x.ToModel()).ToArray() : Optional.Unspecified, AllowedMentions = allowedMentions?.ToModel(), @@ -346,7 +348,7 @@ namespace Discord.Rest /// is in an invalid format. /// An I/O error occurred while opening the file. /// Message content is too long, length must be less or equal to . - /// The only valid are and . + /// The only valid are , and . public static async Task SendFileAsync(IMessageChannel channel, BaseDiscordClient client, string filePath, string text, bool isTTS, Embed embed, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, RequestOptions options, @@ -379,7 +381,7 @@ namespace Discord.Rest => SendFilesAsync(channel, client, new[] { attachment }, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds, flags); - /// The only valid are and . + /// The only valid are , and . public static async Task SendFilesAsync(IMessageChannel channel, BaseDiscordClient client, IEnumerable attachments, string text, bool isTTS, Embed embed, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, RequestOptions options, @@ -391,7 +393,9 @@ namespace Discord.Rest 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.Length, 10, nameof(embeds), "A max of 10 embeds are allowed."); + Preconditions.AtMost(embeds.Length, DiscordConfig.MaxEmbedsPerMessage, nameof(embeds), $"A max of {DiscordConfig.MaxEmbedsPerMessage} Embeds are allowed."); + + Preconditions.MessageAtLeastOneOf(text, components, embeds, stickers, attachments); foreach (var attachment in attachments) { @@ -424,8 +428,8 @@ namespace Discord.Rest } } - if (flags is not MessageFlags.None and not MessageFlags.SuppressEmbeds) - throw new ArgumentException("The only valid MessageFlags are SuppressEmbeds and none.", nameof(flags)); + if (flags is not MessageFlags.None and not MessageFlags.SuppressEmbeds and not MessageFlags.SuppressNotification) + throw new ArgumentException("The only valid MessageFlags are SuppressEmbeds, SuppressNotification and none.", nameof(flags)); if (stickers != null) { diff --git a/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs index 35c2411b..59c502f1 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs @@ -127,7 +127,7 @@ namespace Discord.Rest /// An I/O error occurred while opening the file. /// Message content is too long, length must be less or equal to . /// The only valid are and . - public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, + public Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) @@ -135,7 +135,7 @@ namespace Discord.Rest components, stickers, options, isSpoiler, embeds, flags); /// /// Message content is too long, length must be less or equal to . - public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, + public Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) @@ -144,7 +144,7 @@ namespace Discord.Rest /// /// Message content is too long, length must be less or equal to . - public Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, + public Task SendFileAsync(FileAttachment attachment, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) @@ -153,7 +153,7 @@ namespace Discord.Rest /// /// Message content is too long, length must be less or equal to . - public Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, + public Task SendFilesAsync(IEnumerable attachments, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) diff --git a/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs index ba4cbdb2..38e20e8f 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs @@ -137,7 +137,7 @@ namespace Discord.Rest /// An I/O error occurred while opening the file. /// Message content is too long, length must be less or equal to . /// The only valid are and . - public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, + public Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) @@ -146,7 +146,7 @@ namespace Discord.Rest /// /// Message content is too long, length must be less or equal to . /// The only valid are and . - public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, + public Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) @@ -155,7 +155,7 @@ namespace Discord.Rest /// /// Message content is too long, length must be less or equal to . /// The only valid are and . - public Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, + public Task SendFileAsync(FileAttachment attachment, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) @@ -164,7 +164,7 @@ namespace Discord.Rest /// /// Message content is too long, length must be less or equal to . /// The only valid are and . - public Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, + public Task SendFilesAsync(IEnumerable attachments, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) diff --git a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs index 14f531de..798d15ef 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs @@ -114,7 +114,7 @@ namespace Discord.Rest /// /// Message content is too long, length must be less or equal to . - /// The only valid are and . + /// The only valid are , and . public virtual Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) @@ -146,8 +146,8 @@ namespace Discord.Rest /// is in an invalid format. /// An I/O error occurred while opening the file. /// Message content is too long, length must be less or equal to . - /// The only valid are and . - public virtual Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, + /// The only valid are , and . + public virtual Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) @@ -156,8 +156,8 @@ namespace Discord.Rest /// /// Message content is too long, length must be less or equal to . - /// The only valid are and . - public virtual Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, + /// The only valid are , and . + public virtual Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) @@ -166,8 +166,8 @@ namespace Discord.Rest /// /// Message content is too long, length must be less or equal to . - /// The only valid are and . - public virtual Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, + /// The only valid are , and . + public virtual Task SendFileAsync(FileAttachment attachment, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) @@ -176,8 +176,8 @@ namespace Discord.Rest /// /// Message content is too long, length must be less or equal to . - /// The only valid are and . - public virtual Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, + /// The only valid are , and . + public virtual Task SendFilesAsync(IEnumerable attachments, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs index 47e92626..e2031fef 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs @@ -148,7 +148,7 @@ namespace Discord.WebSocket /// /// The only valid are and . - public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, + public Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) @@ -157,7 +157,7 @@ namespace Discord.WebSocket /// /// Message content is too long, length must be less or equal to . /// The only valid are and . - public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, + public Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) @@ -166,7 +166,7 @@ namespace Discord.WebSocket /// /// Message content is too long, length must be less or equal to . /// The only valid are and . - public Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, + public Task SendFileAsync(FileAttachment attachment, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) @@ -175,7 +175,7 @@ namespace Discord.WebSocket /// /// Message content is too long, length must be less or equal to . /// The only valid are and . - public Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, + public Task SendFilesAsync(IEnumerable attachments, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs index 90dfafb0..7502a4e2 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs @@ -187,7 +187,7 @@ namespace Discord.WebSocket /// /// The only valid are and . - public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, + public Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) @@ -196,7 +196,7 @@ namespace Discord.WebSocket /// /// Message content is too long, length must be less or equal to . /// The only valid are and . - public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, + public Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) @@ -205,7 +205,7 @@ namespace Discord.WebSocket /// /// Message content is too long, length must be less or equal to . /// The only valid are and . - public Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, + public Task SendFileAsync(FileAttachment attachment, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) @@ -214,7 +214,7 @@ namespace Discord.WebSocket /// /// Message content is too long, length must be less or equal to . /// The only valid are and . - public Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, + public Task SendFilesAsync(IEnumerable attachments, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs index 7f7ebc2b..5545048c 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs @@ -222,7 +222,7 @@ namespace Discord.WebSocket /// /// Message content is too long, length must be less or equal to . - /// The only valid are and . + /// The only valid are , and . public virtual Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) @@ -230,8 +230,8 @@ namespace Discord.WebSocket components, stickers, options, embeds, flags); /// - /// The only valid are and . - public virtual Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, + /// The only valid are , and . + public virtual Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) @@ -239,8 +239,8 @@ namespace Discord.WebSocket components, stickers, options, isSpoiler, embeds, flags); /// /// Message content is too long, length must be less or equal to . - /// The only valid are and . - public virtual Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, + /// The only valid are , and . + public virtual Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) @@ -248,8 +248,8 @@ namespace Discord.WebSocket messageReference, components, stickers, options, isSpoiler, embeds, flags); /// /// Message content is too long, length must be less or equal to . - /// The only valid are and . - public virtual Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, + /// The only valid are , and . + public virtual Task SendFileAsync(FileAttachment attachment, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) @@ -257,8 +257,8 @@ namespace Discord.WebSocket messageReference, components, stickers, options, embeds, flags); /// /// Message content is too long, length must be less or equal to . - /// The only valid are and . - public virtual Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, + /// The only valid are , and . + public virtual Task SendFilesAsync(IEnumerable attachments, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) diff --git a/src/Discord.Net.Webhook/DiscordWebhookClient.cs b/src/Discord.Net.Webhook/DiscordWebhookClient.cs index 1b05fb04..833dcebb 100644 --- a/src/Discord.Net.Webhook/DiscordWebhookClient.cs +++ b/src/Discord.Net.Webhook/DiscordWebhookClient.cs @@ -1,5 +1,6 @@ using Discord.Logging; using Discord.Rest; + using System; using System.Collections.Generic; using System.Globalization; @@ -7,195 +8,235 @@ using System.IO; using System.Text.RegularExpressions; using System.Threading.Tasks; -namespace Discord.Webhook +namespace Discord.Webhook; + +/// +/// A client responsible for connecting as a Webhook. +/// +public class DiscordWebhookClient : IDisposable { - /// A client responsible for connecting as a Webhook. - public class DiscordWebhookClient : IDisposable + public event Func Log { - public event Func Log { add { _logEvent.Add(value); } remove { _logEvent.Remove(value); } } - internal readonly AsyncEvent> _logEvent = new AsyncEvent>(); + add => _logEvent.Add(value); + remove => _logEvent.Remove(value); + } - private readonly ulong _webhookId; - internal IWebhook Webhook; - internal readonly Logger _restLogger; + internal readonly AsyncEvent> _logEvent = new AsyncEvent>(); - internal API.DiscordRestApiClient ApiClient { get; } - internal LogManager LogManager { get; } + private readonly ulong _webhookId; + internal IWebhook Webhook; + internal readonly Logger _restLogger; - /// Creates a new Webhook Discord client. - public DiscordWebhookClient(IWebhook webhook) - : this(webhook.Id, webhook.Token, new DiscordRestConfig()) { } - /// Creates a new Webhook Discord client. - public DiscordWebhookClient(ulong webhookId, string webhookToken) - : this(webhookId, webhookToken, new DiscordRestConfig()) { } - /// Creates a new Webhook Discord client. - public DiscordWebhookClient(string webhookUrl) - : this(webhookUrl, new DiscordRestConfig()) { } + internal API.DiscordRestApiClient ApiClient { get; } + internal LogManager LogManager { get; } - // regex pattern to match webhook urls - private static Regex WebhookUrlRegex = new Regex(@"^.*(discord|discordapp)\.com\/api\/webhooks\/([\d]+)\/([a-z0-9_-]+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + /// + /// Creates a new Webhook Discord client. + /// + public DiscordWebhookClient(IWebhook webhook) + : this(webhook.Id, webhook.Token, new DiscordRestConfig()) { } - /// Creates a new Webhook Discord client. - public DiscordWebhookClient(ulong webhookId, string webhookToken, DiscordRestConfig config) - : this(config) + /// + /// Creates a new Webhook Discord client. + /// + public DiscordWebhookClient(ulong webhookId, string webhookToken) + : this(webhookId, webhookToken, new DiscordRestConfig()) { } + + /// + /// Creates a new Webhook Discord client. + /// + public DiscordWebhookClient(string webhookUrl) + : this(webhookUrl, new DiscordRestConfig()) { } + + // regex pattern to match webhook urls + private static Regex WebhookUrlRegex = new Regex(@"^.*(discord|discordapp)\.com\/api\/webhooks\/([\d]+)\/([a-z0-9_-]+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + + /// + /// Creates a new Webhook Discord client. + /// + public DiscordWebhookClient(ulong webhookId, string webhookToken, DiscordRestConfig config) + : this(config) + { + _webhookId = webhookId; + ApiClient.LoginAsync(TokenType.Webhook, webhookToken).GetAwaiter().GetResult(); + Webhook = WebhookClientHelper.GetWebhookAsync(this, webhookId).GetAwaiter().GetResult(); + } + + /// + /// Creates a new Webhook Discord client. + /// + public DiscordWebhookClient(IWebhook webhook, DiscordRestConfig config) + : this(config) + { + Webhook = webhook; + _webhookId = Webhook.Id; + } + + /// + /// Creates a new Webhook Discord client. + /// + /// The url of the webhook. + /// The configuration options to use for this client. + /// Thrown if the is an invalid format. + /// Thrown if the is null or whitespace. + public DiscordWebhookClient(string webhookUrl, DiscordRestConfig config) : this(config) + { + ParseWebhookUrl(webhookUrl, out _webhookId, out string token); + ApiClient.LoginAsync(TokenType.Webhook, token).GetAwaiter().GetResult(); + Webhook = WebhookClientHelper.GetWebhookAsync(this, _webhookId).GetAwaiter().GetResult(); + } + + private DiscordWebhookClient(DiscordRestConfig config) + { + ApiClient = CreateApiClient(config); + LogManager = new LogManager(config.LogLevel); + LogManager.Message += async msg => await _logEvent.InvokeAsync(msg).ConfigureAwait(false); + + _restLogger = LogManager.CreateLogger("Rest"); + + ApiClient.RequestQueue.RateLimitTriggered += async (id, info, endpoint) => { - _webhookId = webhookId; - ApiClient.LoginAsync(TokenType.Webhook, webhookToken).GetAwaiter().GetResult(); - Webhook = WebhookClientHelper.GetWebhookAsync(this, webhookId).GetAwaiter().GetResult(); - } - /// Creates a new Webhook Discord client. - public DiscordWebhookClient(IWebhook webhook, DiscordRestConfig config) - : this(config) - { - Webhook = webhook; - _webhookId = Webhook.Id; - } - - /// - /// Creates a new Webhook Discord client. - /// - /// The url of the webhook. - /// The configuration options to use for this client. - /// Thrown if the is an invalid format. - /// Thrown if the is null or whitespace. - public DiscordWebhookClient(string webhookUrl, DiscordRestConfig config) : this(config) - { - ParseWebhookUrl(webhookUrl, out _webhookId, out string token); - ApiClient.LoginAsync(TokenType.Webhook, token).GetAwaiter().GetResult(); - Webhook = WebhookClientHelper.GetWebhookAsync(this, _webhookId).GetAwaiter().GetResult(); - } - - private DiscordWebhookClient(DiscordRestConfig config) - { - ApiClient = CreateApiClient(config); - LogManager = new LogManager(config.LogLevel); - LogManager.Message += async msg => await _logEvent.InvokeAsync(msg).ConfigureAwait(false); - - _restLogger = LogManager.CreateLogger("Rest"); - - ApiClient.RequestQueue.RateLimitTriggered += async (id, info, endpoint) => - { - if (info == null) - await _restLogger.VerboseAsync($"Preemptive Rate limit triggered: {endpoint} {(id.IsHashBucket ? $"(Bucket: {id.BucketHash})" : "")}").ConfigureAwait(false); - else - await _restLogger.WarningAsync($"Rate limit triggered: {endpoint} {(id.IsHashBucket ? $"(Bucket: {id.BucketHash})" : "")}").ConfigureAwait(false); - }; - ApiClient.SentRequest += async (method, endpoint, millis) => await _restLogger.VerboseAsync($"{method} {endpoint}: {millis} ms").ConfigureAwait(false); - } - private static API.DiscordRestApiClient CreateApiClient(DiscordRestConfig config) - => new API.DiscordRestApiClient(config.RestClientProvider, DiscordRestConfig.UserAgent, useSystemClock: config.UseSystemClock, defaultRatelimitCallback: config.DefaultRatelimitCallback); - /// Sends a message to the channel for this webhook. - /// Returns the ID of the created message. - public Task SendMessageAsync(string text = null, bool isTTS = false, IEnumerable embeds = null, - string username = null, string avatarUrl = null, RequestOptions options = null, AllowedMentions allowedMentions = null, - MessageComponent components = null, MessageFlags flags = MessageFlags.None, ulong? threadId = null, string threadName = null) - => WebhookClientHelper.SendMessageAsync(this, text, isTTS, embeds, username, avatarUrl, allowedMentions, options, components, flags, threadId, threadName); - - /// - /// Modifies a message posted using this webhook. - /// - /// - /// This method can only modify messages that were sent using the same webhook. - /// - /// ID of the modified message. - /// A delegate containing the properties to modify the message with. - /// The options to be used when sending the request. - /// - /// A task that represents the asynchronous modification operation. - /// - public Task ModifyMessageAsync(ulong messageId, Action func, RequestOptions options = null, ulong? threadId = null) - => WebhookClientHelper.ModifyMessageAsync(this, messageId, func, options, threadId); - - /// - /// Deletes a message posted using this webhook. - /// - /// - /// This method can only delete messages that were sent using the same webhook. - /// - /// ID of the deleted message. - /// The options to be used when sending the request. - /// - /// A task that represents the asynchronous deletion operation. - /// - public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null, ulong? threadId = null) - => WebhookClientHelper.DeleteMessageAsync(this, messageId, options, threadId); - - /// Sends a message to the channel for this webhook with an attachment. - /// 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, - MessageComponent components = null, MessageFlags flags = MessageFlags.None, ulong? threadId = null, string threadName = null) - => WebhookClientHelper.SendFileAsync(this, filePath, text, isTTS, embeds, username, avatarUrl, - allowedMentions, options, isSpoiler, components, flags, threadId, threadName); - /// 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, - MessageComponent components = null, MessageFlags flags = MessageFlags.None, ulong? threadId = null, string threadName = null) - => WebhookClientHelper.SendFileAsync(this, stream, filename, text, isTTS, embeds, username, - avatarUrl, allowedMentions, options, isSpoiler, components, flags, threadId, threadName); - - /// 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, - MessageFlags flags = MessageFlags.None, ulong? threadId = null, string threadName = null) - => WebhookClientHelper.SendFileAsync(this, attachment, text, isTTS, embeds, username, - avatarUrl, allowedMentions, components, options, flags, threadId, threadName); - - /// 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, - MessageFlags flags = MessageFlags.None, ulong? threadId = null, string threadName = null) - => WebhookClientHelper.SendFilesAsync(this, attachments, text, isTTS, embeds, username, avatarUrl, - allowedMentions, components, options, flags, threadId, threadName); - - - /// Modifies the properties of this webhook. - public Task ModifyWebhookAsync(Action func, RequestOptions options = null) - => Webhook.ModifyAsync(func, options); - - /// Deletes this webhook from Discord and disposes the client. - public async Task DeleteWebhookAsync(RequestOptions options = null) - { - await Webhook.DeleteAsync(options).ConfigureAwait(false); - Dispose(); - } - - public void Dispose() - { - ApiClient?.Dispose(); - } - - internal static void ParseWebhookUrl(string webhookUrl, out ulong webhookId, out string webhookToken) - { - if (string.IsNullOrWhiteSpace(webhookUrl)) - throw new ArgumentNullException(paramName: nameof(webhookUrl), message: - "The given webhook Url cannot be null or whitespace."); - - // thrown when groups are not populated/valid, or when there is no match - ArgumentException ex(string reason = null) - => new ArgumentException(paramName: nameof(webhookUrl), message: - $"The given webhook Url was not in a valid format. {reason}"); - var match = WebhookUrlRegex.Match(webhookUrl); - if (match != null) - { - // ensure that the first group is a ulong, set the _webhookId - // 0th group is always the entire match, and 1 is the domain; so start at index 2 - if (!(match.Groups[2].Success && ulong.TryParse(match.Groups[2].Value, NumberStyles.None, CultureInfo.InvariantCulture, out webhookId))) - throw ex("The webhook Id could not be parsed."); - - if (!match.Groups[3].Success) - throw ex("The webhook token could not be parsed."); - webhookToken = match.Groups[3].Value; - } + if (info == null) + await _restLogger.VerboseAsync($"Preemptive Rate limit triggered: {endpoint} {(id.IsHashBucket ? $"(Bucket: {id.BucketHash})" : "")}").ConfigureAwait(false); else - throw ex(); + await _restLogger.WarningAsync($"Rate limit triggered: {endpoint} {(id.IsHashBucket ? $"(Bucket: {id.BucketHash})" : "")}").ConfigureAwait(false); + }; + ApiClient.SentRequest += async (method, endpoint, millis) => await _restLogger.VerboseAsync($"{method} {endpoint}: {millis} ms").ConfigureAwait(false); + } + private static API.DiscordRestApiClient CreateApiClient(DiscordRestConfig config) + => new API.DiscordRestApiClient(config.RestClientProvider, DiscordRestConfig.UserAgent, useSystemClock: config.UseSystemClock, defaultRatelimitCallback: config.DefaultRatelimitCallback); + + /// + /// Sends a message to the channel for this webhook. + /// + /// + /// Returns the ID of the created message. + /// + public Task SendMessageAsync(string text = null, bool isTTS = false, IEnumerable embeds = null, + string username = null, string avatarUrl = null, RequestOptions options = null, AllowedMentions allowedMentions = null, + MessageComponent components = null, MessageFlags flags = MessageFlags.None, ulong? threadId = null, string threadName = null) + => WebhookClientHelper.SendMessageAsync(this, text, isTTS, embeds, username, avatarUrl, allowedMentions, options, components, flags, threadId, threadName); + + /// + /// Modifies a message posted using this webhook. + /// + /// + /// This method can only modify messages that were sent using the same webhook. + /// + /// ID of the modified message. + /// A delegate containing the properties to modify the message with. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous modification operation. + /// + public Task ModifyMessageAsync(ulong messageId, Action func, RequestOptions options = null, ulong? threadId = null) + => WebhookClientHelper.ModifyMessageAsync(this, messageId, func, options, threadId); + + /// + /// Deletes a message posted using this webhook. + /// + /// + /// This method can only delete messages that were sent using the same webhook. + /// + /// ID of the deleted message. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous deletion operation. + /// + public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null, ulong? threadId = null) + => WebhookClientHelper.DeleteMessageAsync(this, messageId, options, threadId); + + /// + /// Sends a message to the channel for this webhook with an attachment. + /// + /// + /// 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, + MessageComponent components = null, MessageFlags flags = MessageFlags.None, ulong? threadId = null, string threadName = null) + => WebhookClientHelper.SendFileAsync(this, filePath, text, isTTS, embeds, username, avatarUrl, + allowedMentions, options, isSpoiler, components, flags, threadId, threadName); + + /// + /// 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, + MessageComponent components = null, MessageFlags flags = MessageFlags.None, ulong? threadId = null, string threadName = null) + => WebhookClientHelper.SendFileAsync(this, stream, filename, text, isTTS, embeds, username, + avatarUrl, allowedMentions, options, isSpoiler, components, flags, threadId, threadName); + + /// 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, + MessageFlags flags = MessageFlags.None, ulong? threadId = null, string threadName = null) + => WebhookClientHelper.SendFileAsync(this, attachment, text, isTTS, embeds, username, + avatarUrl, allowedMentions, components, options, flags, threadId, threadName); + + /// + /// 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, + MessageFlags flags = MessageFlags.None, ulong? threadId = null, string threadName = null) + => WebhookClientHelper.SendFilesAsync(this, attachments, text, isTTS, embeds, username, avatarUrl, + allowedMentions, components, options, flags, threadId, threadName); + + /// + /// Modifies the properties of this webhook. + /// + public Task ModifyWebhookAsync(Action func, RequestOptions options = null) + => Webhook.ModifyAsync(func, options); + + /// + /// Deletes this webhook from Discord and disposes the client. + /// + public async Task DeleteWebhookAsync(RequestOptions options = null) + { + await Webhook.DeleteAsync(options).ConfigureAwait(false); + Dispose(); + } + + public void Dispose() + { + ApiClient?.Dispose(); + } + + internal static void ParseWebhookUrl(string webhookUrl, out ulong webhookId, out string webhookToken) + { + if (string.IsNullOrWhiteSpace(webhookUrl)) + throw new ArgumentNullException(nameof(webhookUrl), "The given webhook Url cannot be null or whitespace."); + + // thrown when groups are not populated/valid, or when there is no match + ArgumentException ex(string reason = null) + => new ($"The given webhook Url was not in a valid format. {reason}", nameof(webhookUrl)); + + var match = WebhookUrlRegex.Match(webhookUrl); + + if (match != null) + { + // ensure that the first group is a ulong, set the _webhookId + // 0th group is always the entire match, and 1 is the domain; so start at index 2 + if (!(match.Groups[2].Success && ulong.TryParse(match.Groups[2].Value, NumberStyles.None, CultureInfo.InvariantCulture, out webhookId))) + throw ex("The webhook Id could not be parsed."); + + if (!match.Groups[3].Success) + throw ex("The webhook token could not be parsed."); + webhookToken = match.Groups[3].Value; } + else + throw ex(); } } diff --git a/src/Discord.Net.Webhook/Entities/Messages/WebhookMessageProperties.cs b/src/Discord.Net.Webhook/Entities/Messages/WebhookMessageProperties.cs index ca2ff10a..6cb15bc9 100644 --- a/src/Discord.Net.Webhook/Entities/Messages/WebhookMessageProperties.cs +++ b/src/Discord.Net.Webhook/Entities/Messages/WebhookMessageProperties.cs @@ -26,5 +26,9 @@ namespace Discord.Webhook /// Gets or sets the components that the message should display. /// public Optional Components { get; set; } + /// + /// Gets or sets the attachments for the message. + /// + public Optional> Attachments { get; set; } } } diff --git a/src/Discord.Net.Webhook/WebhookClientHelper.cs b/src/Discord.Net.Webhook/WebhookClientHelper.cs index 3f9a6acb..535d0fb8 100644 --- a/src/Discord.Net.Webhook/WebhookClientHelper.cs +++ b/src/Discord.Net.Webhook/WebhookClientHelper.cs @@ -22,6 +22,7 @@ namespace Discord.Webhook throw new InvalidOperationException("Could not find a webhook with the supplied credentials."); return RestInternalWebhook.Create(client, model); } + public static async Task SendMessageAsync(DiscordWebhookClient client, string text, bool isTTS, IEnumerable embeds, string username, string avatarUrl, AllowedMentions allowedMentions, RequestOptions options, MessageComponent components, MessageFlags flags, ulong? threadId = null, string threadName = null) @@ -33,6 +34,8 @@ namespace Discord.Webhook Flags = flags }; + Preconditions.WebhookMessageAtLeastOneOf(text, components, embeds?.ToArray()); + if (embeds != null) args.Embeds = embeds.Select(x => x.ToModel()).ToArray(); if (username != null) @@ -86,21 +89,42 @@ namespace Discord.Webhook } } - var apiArgs = new ModifyWebhookMessageParams + if (!args.Attachments.IsSpecified) { - Content = args.Content.IsSpecified ? args.Content.Value : Optional.Create(), - Embeds = - args.Embeds.IsSpecified - ? args.Embeds.Value.Select(embed => embed.ToModel()).ToArray() - : Optional.Create(), - AllowedMentions = args.AllowedMentions.IsSpecified - ? args.AllowedMentions.Value.ToModel() - : Optional.Create(), - Components = args.Components.IsSpecified ? args.Components.Value?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() : Optional.Unspecified, - }; + var apiArgs = new ModifyWebhookMessageParams + { + Content = args.Content.IsSpecified ? args.Content.Value : Optional.Create(), + Embeds = + args.Embeds.IsSpecified + ? args.Embeds.Value.Select(embed => embed.ToModel()).ToArray() + : Optional.Create(), + AllowedMentions = args.AllowedMentions.IsSpecified + ? args.AllowedMentions.Value.ToModel() + : Optional.Create(), + Components = args.Components.IsSpecified ? args.Components.Value?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() : Optional.Unspecified, + }; - await client.ApiClient.ModifyWebhookMessageAsync(client.Webhook.Id, messageId, apiArgs, options, threadId) - .ConfigureAwait(false); + await client.ApiClient.ModifyWebhookMessageAsync(client.Webhook.Id, messageId, apiArgs, options, threadId) + .ConfigureAwait(false); + } + else + { + var apiArgs = new UploadWebhookFileParams(args.Attachments.Value.ToArray()) + { + Content = args.Content.IsSpecified ? args.Content.Value : Optional.Create(), + Embeds = + args.Embeds.IsSpecified + ? args.Embeds.Value.Select(embed => embed.ToModel()).ToArray() + : Optional.Create(), + AllowedMentions = args.AllowedMentions.IsSpecified + ? args.AllowedMentions.Value.ToModel() + : Optional.Create(), + MessageComponents = args.Components.IsSpecified ? args.Components.Value?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() : Optional.Unspecified, + }; + + await client.ApiClient.ModifyWebhookMessageAsync(client.Webhook.Id, messageId, apiArgs, options, threadId) + .ConfigureAwait(false); + } } public static async Task DeleteMessageAsync(DiscordWebhookClient client, ulong messageId, RequestOptions options, ulong? threadId) @@ -136,7 +160,9 @@ namespace Discord.Webhook 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."); + Preconditions.AtMost(embeds.Count(), DiscordConfig.MaxEmbedsPerMessage, nameof(embeds), $"A max of {DiscordConfig.MaxEmbedsPerMessage} Embeds are allowed."); + + Preconditions.WebhookMessageAtLeastOneOf(text, components, embeds.ToArray(), attachments); foreach (var attachment in attachments) { @@ -159,8 +185,8 @@ namespace Discord.Webhook } } - if (flags is not MessageFlags.None and not MessageFlags.SuppressEmbeds) - throw new ArgumentException("The only valid MessageFlags are SuppressEmbeds and none.", nameof(flags)); + if (flags is not MessageFlags.None and not MessageFlags.SuppressEmbeds and not MessageFlags.SuppressNotification) + throw new ArgumentException("The only valid MessageFlags are SuppressEmbeds, SuppressNotification and none.", nameof(flags)); var args = new UploadWebhookFileParams(attachments.ToArray()) { @@ -178,8 +204,7 @@ namespace Discord.Webhook return msg.Id; } - public static async Task ModifyAsync(DiscordWebhookClient client, - Action func, RequestOptions options) + public static async Task ModifyAsync(DiscordWebhookClient client, Action func, RequestOptions options) { var args = new WebhookProperties(); func(args);