From 6039378c52b38a2ae53bd012643d94e84091cebd Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Wed, 16 Feb 2022 12:51:15 +0100 Subject: [PATCH 01/35] Fixes unused creation of REST clients for DiscordShardedClient shards. (#2109) * Init * Remove unnecessary length check * Swap out for any check * Final; Check if parentclient was passed --- src/Discord.Net.WebSocket/DiscordSocketClient.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index b0215d9ef..ecd53cf65 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -152,7 +152,8 @@ namespace Discord.WebSocket LogGatewayIntentWarnings = config.LogGatewayIntentWarnings; HandlerTimeout = config.HandlerTimeout; State = new ClientState(0, 0); - Rest = new DiscordSocketRestClient(config, ApiClient); + if (shardedClient is null || parentClient is null) + Rest = new DiscordSocketRestClient(config, ApiClient); _heartbeatTimes = new ConcurrentQueue(); _gatewayIntents = config.GatewayIntents; _defaultStickers = ImmutableArray.Create>(); From 169d54f1dfe9f3579295b5b00b4b855108565453 Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Wed, 16 Feb 2022 12:51:40 +0100 Subject: [PATCH 02/35] Fix being unable to modify AllowedMentions with no embeds set.(#2108) --- src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs b/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs index cd2a34f11..4d9ef008d 100644 --- a/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs +++ b/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs @@ -53,7 +53,6 @@ namespace Discord.Rest AllowedMentions allowedMentions = args.AllowedMentions.Value; 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(args.Embeds.Value?.Length ?? 0, 10, nameof(args.Embeds), "A max of 10 embeds are allowed."); // check that user flag and user Id list are exclusive, same with role flag and role Id list if (allowedMentions != null && allowedMentions.AllowedTypes.HasValue) From abfba3c4bb07796f40f53c8573a047061d5950f3 Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Wed, 16 Feb 2022 12:52:08 +0100 Subject: [PATCH 03/35] Add DisplayName property to IGuildUser. (#2107) --- src/Discord.Net.Core/Entities/Users/IGuildUser.cs | 7 +++++++ src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs | 3 ++- src/Discord.Net.Rest/Entities/Users/RestWebhookUser.cs | 2 ++ .../Entities/Users/SocketGuildUser.cs | 2 ++ .../Entities/Users/SocketThreadUser.cs | 4 ++++ .../Entities/Users/SocketWebhookUser.cs | 2 ++ 6 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net.Core/Entities/Users/IGuildUser.cs b/src/Discord.Net.Core/Entities/Users/IGuildUser.cs index 95896eef0..21f6cf093 100644 --- a/src/Discord.Net.Core/Entities/Users/IGuildUser.cs +++ b/src/Discord.Net.Core/Entities/Users/IGuildUser.cs @@ -18,6 +18,13 @@ namespace Discord /// DateTimeOffset? JoinedAt { get; } /// + /// Gets the displayed name for this user. + /// + /// + /// A string representing the display name of the user; If the nickname is null, this will be the username. + /// + string DisplayName { get; } + /// /// Gets the nickname for this user. /// /// diff --git a/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs b/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs index 09e7ec03a..1415b5825 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs @@ -19,7 +19,8 @@ namespace Discord.Rest private long? _timedOutTicks; private long? _joinedAtTicks; private ImmutableArray _roleIds; - + /// + public string DisplayName => Nickname ?? Username; /// public string Nickname { get; private set; } /// diff --git a/src/Discord.Net.Rest/Entities/Users/RestWebhookUser.cs b/src/Discord.Net.Rest/Entities/Users/RestWebhookUser.cs index 4ef84c508..b887f8df6 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestWebhookUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestWebhookUser.cs @@ -52,6 +52,8 @@ namespace Discord.Rest /// DateTimeOffset? IGuildUser.JoinedAt => null; /// + string IGuildUser.DisplayName => null; + /// string IGuildUser.Nickname => null; /// string IGuildUser.GuildAvatarId => null; diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs index 8c2825bc4..52d55561f 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs @@ -30,6 +30,8 @@ namespace Discord.WebSocket /// public SocketGuild Guild { get; } /// + public string DisplayName => Nickname ?? Username; + /// public string Nickname { get; private set; } /// public string GuildAvatarId { get; private set; } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs index 24d570692..f3fdbff3e 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs @@ -29,6 +29,10 @@ namespace Discord.WebSocket public DateTimeOffset? JoinedAt => GuildUser.JoinedAt; + /// + public string DisplayName + => GuildUser.Nickname ?? GuildUser.Username; + /// public string Nickname => GuildUser.Nickname; diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs index 7d63e4e36..b6a00bcb0 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs @@ -63,6 +63,8 @@ namespace Discord.WebSocket /// DateTimeOffset? IGuildUser.JoinedAt => null; /// + string IGuildUser.DisplayName => null; + /// string IGuildUser.Nickname => null; /// string IGuildUser.GuildAvatarId => null; From b2598d37b6debbba972aaeb240980ce691f0671e Mon Sep 17 00:00:00 2001 From: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Wed, 16 Feb 2022 08:20:57 -0400 Subject: [PATCH 04/35] fix: Implement correct ratelimit handles for 429's (#2110) * init * fix errors --- .../Net/Queue/RequestQueueBucket.cs | 16 +++++++++++++--- src/Discord.Net.Rest/Net/RateLimitInfo.cs | 18 +++++++----------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs b/src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs index 8130e6fac..3bdfb2ffe 100644 --- a/src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs +++ b/src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs @@ -88,7 +88,7 @@ namespace Discord.Net.Queue #if DEBUG_LIMITS Debug.WriteLine($"[{id}] (!) 429"); #endif - UpdateRateLimit(id, request, info, true); + UpdateRateLimit(id, request, info, true, body:response.Stream); } await _queue.RaiseRateLimitTriggered(Id, info, $"{request.Method} {request.Endpoint}").ConfigureAwait(false); continue; //Retry @@ -316,7 +316,7 @@ namespace Discord.Net.Queue } } - private void UpdateRateLimit(int id, IRequest request, RateLimitInfo info, bool is429, bool redirected = false) + private void UpdateRateLimit(int id, IRequest request, RateLimitInfo info, bool is429, bool redirected = false, Stream body = null) { if (WindowCount == 0) return; @@ -373,7 +373,17 @@ namespace Discord.Net.Queue Debug.WriteLine($"[{id}] X-RateLimit-Remaining: " + info.Remaining.Value); _semaphore = info.Remaining.Value; }*/ - if (info.RetryAfter.HasValue) + if (is429) + { + // use the payload reset after value + var payload = info.ReadRatelimitPayload(body); + + resetTick = DateTimeOffset.UtcNow.Add(TimeSpan.FromSeconds(payload.RetryAfter)); +#if DEBUG_LIMITS + Debug.WriteLine($"[{id}] Reset-After: {info.ResetAfter.Value} ({info.ResetAfter?.TotalMilliseconds} ms)"); +#endif + } + else if (info.RetryAfter.HasValue) { //RetryAfter is more accurate than Reset, where available resetTick = DateTimeOffset.UtcNow.AddSeconds(info.RetryAfter.Value); diff --git a/src/Discord.Net.Rest/Net/RateLimitInfo.cs b/src/Discord.Net.Rest/Net/RateLimitInfo.cs index 253343311..9d0b9a426 100644 --- a/src/Discord.Net.Rest/Net/RateLimitInfo.cs +++ b/src/Discord.Net.Rest/Net/RateLimitInfo.cs @@ -61,22 +61,18 @@ namespace Discord.Net DateTimeOffset.TryParse(temp, CultureInfo.InvariantCulture, DateTimeStyles.None, out var date) ? DateTimeOffset.UtcNow - date : (TimeSpan?)null; } - internal void ReadRatelimitPayload(Stream response) + internal Ratelimit ReadRatelimitPayload(Stream response) { - try + if (response != null && response.Length != 0) { - if (response != null && response.Length != 0) + using (TextReader text = new StreamReader(response)) + using (JsonReader reader = new JsonTextReader(text)) { - using (TextReader text = new StreamReader(response)) - using (JsonReader reader = new JsonTextReader(text)) - { - var ratelimit = Discord.Rest.DiscordRestClient.Serializer.Deserialize(reader); - - ResetAfter = TimeSpan.FromSeconds(ratelimit.RetryAfter); - } + return Discord.Rest.DiscordRestClient.Serializer.Deserialize(reader); } } - catch { } + + return null; } } } From 62d4598fc9ab4f05e4d35295184b11d7f376002d Mon Sep 17 00:00:00 2001 From: Quin Lynch Date: Wed, 16 Feb 2022 08:40:56 -0400 Subject: [PATCH 05/35] meta: bump version --- CHANGELOG.md | 19 +++++++++ Discord.Net.targets | 2 +- docs/docfx.json | 2 +- src/Discord.Net/Discord.Net.nuspec | 62 +++++++++++++++--------------- 4 files changed, 52 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f2fe6286..678338af7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # Changelog +## [3.3.1] - 2022-02-16 + +### Added + +- #2107 Add DisplayName property to IGuildUser. (abfba3c) + +### Fixed + +- #2110 Fix incorrect ratelimit handles for 429's (b2598d3) +- #2094 Fix ToString() on CommandInfo (01735c8) +- #2098 Fix channel being null in DMs on Interactions (7e1b8c9) +- #2100 Fix crosspost ratelimits (fad217e) +- #2108 Fix being unable to modify AllowedMentions with no embeds set. (169d54f) +- #2109 Fix unused creation of REST clients for DiscordShardedClient shards. (6039378) + +### Misc + +- #2099 Update interaction summaries (503d32a) + ## [3.3.0] - 2022-02-09 ### Added diff --git a/Discord.Net.targets b/Discord.Net.targets index 3de30825f..de4c57b8e 100644 --- a/Discord.Net.targets +++ b/Discord.Net.targets @@ -1,6 +1,6 @@ - 3.3.0 + 3.3.1 latest Discord.Net Contributors discord;discordapp diff --git a/docs/docfx.json b/docs/docfx.json index 01ba9a7d9..d111fa85c 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -60,7 +60,7 @@ "overwrite": "_overwrites/**/**.md", "globalMetadata": { "_appTitle": "Discord.Net Documentation", - "_appFooter": "Discord.Net (c) 2015-2022 3.3.0", + "_appFooter": "Discord.Net (c) 2015-2022 3.3.1", "_enableSearch": true, "_appLogoPath": "marketing/logo/SVG/Logomark Purple.svg", "_appFaviconPath": "favicon.ico" diff --git a/src/Discord.Net/Discord.Net.nuspec b/src/Discord.Net/Discord.Net.nuspec index 206eb349d..875f0ec69 100644 --- a/src/Discord.Net/Discord.Net.nuspec +++ b/src/Discord.Net/Discord.Net.nuspec @@ -2,7 +2,7 @@ Discord.Net - 3.3.0$suffix$ + 3.3.1$suffix$ Discord.Net Discord.Net Contributors foxbot @@ -14,44 +14,44 @@ https://github.com/RogueException/Discord.Net/raw/dev/docs/marketing/logo/PackageLogo.png - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + From 9a33aa4371717b0884db7b56ded98519b0360999 Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Wed, 16 Feb 2022 14:11:40 +0100 Subject: [PATCH 06/35] Update bugreport.yml (#2112) Updates the bugreport format to include heading to the discussions for non-bug related issues --- .github/ISSUE_TEMPLATE/bugreport.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bugreport.yml b/.github/ISSUE_TEMPLATE/bugreport.yml index 81cac4af7..d130991bf 100644 --- a/.github/ISSUE_TEMPLATE/bugreport.yml +++ b/.github/ISSUE_TEMPLATE/bugreport.yml @@ -18,7 +18,8 @@ body: attributes: label: Verify Issue Source description: If your issue is related to an exception make sure the error was thrown by Discord.Net, and not your code or another library. - If you get an `HttpException` with the error code `401`, then the error is caused by your bot's permissions, not dnet. + If you get an `HttpException` with the error code `401`, then the error is caused by your bot's permissions, not dnet. + If you have a issue that does directly relate to an API bug, feel free to open a [Q&A Discussion](https://github.com/discord-net/Discord.Net/discussions) options: - label: I verified the issue was caused by Discord.Net. required: true From 1ffe9eeca9570ae97e4b50af1378ca848bb4b9e2 Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Wed, 16 Feb 2022 14:49:55 +0100 Subject: [PATCH 07/35] Add DisplayAvatar to IGuildUser (#2115) --- .../Entities/Users/IGuildUser.cs | 27 ++++++++++++++++--- .../Entities/Users/RestGuildUser.cs | 9 +++++++ .../Entities/Users/RestWebhookUser.cs | 4 +++ .../Entities/Users/SocketGuildUser.cs | 10 +++++++ .../Entities/Users/SocketThreadUser.cs | 7 +++++ .../Entities/Users/SocketWebhookUser.cs | 4 +++ 6 files changed, 58 insertions(+), 3 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Users/IGuildUser.cs b/src/Discord.Net.Core/Entities/Users/IGuildUser.cs index 21f6cf093..96de06ed8 100644 --- a/src/Discord.Net.Core/Entities/Users/IGuildUser.cs +++ b/src/Discord.Net.Core/Entities/Users/IGuildUser.cs @@ -32,7 +32,15 @@ namespace Discord /// string Nickname { get; } /// - /// Gets the guild specific avatar for this users. + /// Gets the displayed avatar for this user. + /// + /// + /// The users displayed avatar hash. If the user does not have a guild avatar, this will be the regular avatar. + /// If the user also does not have a regular avatar, this will be . + /// + string DisplayAvatarId { get; } + /// + /// Gets the guild specific avatar for this user. /// /// /// The users guild avatar hash if they have one; otherwise . @@ -126,16 +134,29 @@ namespace Discord /// /// /// This property retrieves a URL for this guild user's guild specific avatar. In event that the user does not have a valid guild avatar - /// (i.e. their avatar identifier is not set), this method will return null. + /// (i.e. their avatar identifier is not set), this method will return . /// /// The format to return. /// The size of the image to return in. This can be any power of two between 16 and 2048. /// /// - /// A string representing the user's avatar URL; null if the user does not have an avatar in place. + /// A string representing the user's avatar URL; if the user does not have an avatar in place. /// string GetGuildAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128); /// + /// Gets the display avatar URL for this user. + /// + /// + /// This property retrieves an URL for this guild user's displayed avatar. + /// If the user does not have a guild avatar, this will be the user's regular avatar. + /// + /// The format to return. + /// The size of the image to return in. This can be any power of two between 16 and 2048. + /// + /// A string representing the URL of the displayed avatar for this user. if the user does not have an avatar in place. + /// + string GetDisplayAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128); + /// /// Kicks this user from this guild. /// /// The reason for the kick which will be recorded in the audit log. diff --git a/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs b/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs index 1415b5825..d6c7b5d7c 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs @@ -24,6 +24,8 @@ namespace Discord.Rest /// public string Nickname { get; private set; } /// + public string DisplayAvatarId => GuildAvatarId ?? AvatarId; + /// public string GuildAvatarId { get; private set; } internal IGuild Guild { get; private set; } /// @@ -183,6 +185,13 @@ namespace Discord.Rest return new ChannelPermissions(Permissions.ResolveChannel(Guild, this, channel, guildPerms.RawValue)); } + /// + public string GetDisplayAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) + => GuildAvatarId is not null + ? GetGuildAvatarUrl(format, size) + : GetAvatarUrl(format, size); + + /// public string GetGuildAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) => CDN.GetGuildUserAvatarUrl(Id, GuildId, GuildAvatarId, size, format); #endregion diff --git a/src/Discord.Net.Rest/Entities/Users/RestWebhookUser.cs b/src/Discord.Net.Rest/Entities/Users/RestWebhookUser.cs index b887f8df6..d03800676 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestWebhookUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestWebhookUser.cs @@ -55,9 +55,13 @@ namespace Discord.Rest string IGuildUser.DisplayName => null; /// string IGuildUser.Nickname => null; + /// + string IGuildUser.DisplayAvatarId => null; /// string IGuildUser.GuildAvatarId => null; /// + string IGuildUser.GetDisplayAvatarUrl(ImageFormat format, ushort size) => null; + /// string IGuildUser.GetGuildAvatarUrl(ImageFormat format, ushort size) => null; /// bool? IGuildUser.IsPending => null; diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs index 52d55561f..ac3a53f17 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs @@ -34,6 +34,8 @@ namespace Discord.WebSocket /// public string Nickname { get; private set; } /// + public string DisplayAvatarId => GuildAvatarId ?? AvatarId; + /// public string GuildAvatarId { get; private set; } /// public override bool IsBot { get { return GlobalUser.IsBot; } internal set { GlobalUser.IsBot = value; } } @@ -246,6 +248,14 @@ namespace Discord.WebSocket /// public ChannelPermissions GetPermissions(IGuildChannel channel) => new ChannelPermissions(Permissions.ResolveChannel(Guild, this, channel, GuildPermissions.RawValue)); + + /// + public string GetDisplayAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) + => GuildAvatarId is not null + ? GetGuildAvatarUrl(format, size) + : GetAvatarUrl(format, size); + + /// public string GetGuildAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) => CDN.GetGuildUserAvatarUrl(Id, Guild.Id, GuildAvatarId, size, format); diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs index f3fdbff3e..08a1cbab4 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs @@ -58,6 +58,9 @@ namespace Discord.WebSocket get => GuildUser.AvatarId; internal set => GuildUser.AvatarId = value; } + /// + public string DisplayAvatarId => GuildAvatarId ?? AvatarId; + /// public string GuildAvatarId => GuildUser.GuildAvatarId; @@ -201,6 +204,10 @@ namespace Discord.WebSocket /// IReadOnlyCollection IGuildUser.RoleIds => GuildUser.Roles.Select(x => x.Id).ToImmutableArray(); + /// + string IGuildUser.GetDisplayAvatarUrl(ImageFormat format, ushort size) => GuildUser.GetDisplayAvatarUrl(format, size); + + /// string IGuildUser.GetGuildAvatarUrl(ImageFormat format, ushort size) => GuildUser.GetGuildAvatarUrl(format, size); internal override SocketGlobalUser GlobalUser { get => GuildUser.GlobalUser; set => GuildUser.GlobalUser = value; } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs index b6a00bcb0..df5fe786d 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs @@ -67,8 +67,12 @@ namespace Discord.WebSocket /// string IGuildUser.Nickname => null; /// + string IGuildUser.DisplayAvatarId => null; + /// string IGuildUser.GuildAvatarId => null; /// + string IGuildUser.GetDisplayAvatarUrl(ImageFormat format, ushort size) => null; + /// string IGuildUser.GetGuildAvatarUrl(ImageFormat format, ushort size) => null; /// DateTimeOffset? IGuildUser.PremiumSince => null; From 84eeb780d115a354c9e5a7afbc0ded41d607663e Mon Sep 17 00:00:00 2001 From: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Wed, 16 Feb 2022 10:43:04 -0400 Subject: [PATCH 08/35] Revert "Fixes unused creation of REST clients for DiscordShardedClient shards. (#2109)" (#2116) This reverts commit 6039378c52b38a2ae53bd012643d94e84091cebd. --- src/Discord.Net.WebSocket/DiscordSocketClient.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index ecd53cf65..b0215d9ef 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -152,8 +152,7 @@ namespace Discord.WebSocket LogGatewayIntentWarnings = config.LogGatewayIntentWarnings; HandlerTimeout = config.HandlerTimeout; State = new ClientState(0, 0); - if (shardedClient is null || parentClient is null) - Rest = new DiscordSocketRestClient(config, ApiClient); + Rest = new DiscordSocketRestClient(config, ApiClient); _heartbeatTimes = new ConcurrentQueue(); _gatewayIntents = config.GatewayIntents; _defaultStickers = ImmutableArray.Create>(); From 92445fb247e37e78f6fe79058f11e3d6903ed276 Mon Sep 17 00:00:00 2001 From: Quin Lynch Date: Wed, 16 Feb 2022 10:54:21 -0400 Subject: [PATCH 09/35] meta: bump version --- CHANGELOG.md | 6 +++ Discord.Net.targets | 2 +- docs/docfx.json | 2 +- src/Discord.Net/Discord.Net.nuspec | 62 +++++++++++++++--------------- 4 files changed, 39 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 678338af7..7ba61ed83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [3.3.2] - 2022-02-16 + +### Fixed + +- #2116 Fix null rest client in shards + ## [3.3.1] - 2022-02-16 ### Added diff --git a/Discord.Net.targets b/Discord.Net.targets index de4c57b8e..1b6a19c72 100644 --- a/Discord.Net.targets +++ b/Discord.Net.targets @@ -1,6 +1,6 @@ - 3.3.1 + 3.3.2 latest Discord.Net Contributors discord;discordapp diff --git a/docs/docfx.json b/docs/docfx.json index d111fa85c..c0821ce5d 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -60,7 +60,7 @@ "overwrite": "_overwrites/**/**.md", "globalMetadata": { "_appTitle": "Discord.Net Documentation", - "_appFooter": "Discord.Net (c) 2015-2022 3.3.1", + "_appFooter": "Discord.Net (c) 2015-2022 3.3.2", "_enableSearch": true, "_appLogoPath": "marketing/logo/SVG/Logomark Purple.svg", "_appFaviconPath": "favicon.ico" diff --git a/src/Discord.Net/Discord.Net.nuspec b/src/Discord.Net/Discord.Net.nuspec index 875f0ec69..dec25413c 100644 --- a/src/Discord.Net/Discord.Net.nuspec +++ b/src/Discord.Net/Discord.Net.nuspec @@ -2,7 +2,7 @@ Discord.Net - 3.3.1$suffix$ + 3.3.2$suffix$ Discord.Net Discord.Net Contributors foxbot @@ -14,44 +14,44 @@ https://github.com/RogueException/Discord.Net/raw/dev/docs/marketing/logo/PackageLogo.png - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + From a1cfa41953e9f83cafaba453d750c688bcad3d24 Mon Sep 17 00:00:00 2001 From: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Wed, 16 Feb 2022 11:13:36 -0400 Subject: [PATCH 10/35] Fix stream access exception when ratelimited (#2117) --- src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs b/src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs index 3bdfb2ffe..3eb656745 100644 --- a/src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs +++ b/src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs @@ -75,7 +75,6 @@ namespace Discord.Net.Queue switch (response.StatusCode) { case (HttpStatusCode)429: - info.ReadRatelimitPayload(response.Stream); if (info.IsGlobal) { #if DEBUG_LIMITS From 553055b724176d812e2b2c2ba9ae3955b7c341fb Mon Sep 17 00:00:00 2001 From: sabihoshi <25006819+sabihoshi@users.noreply.github.com> Date: Thu, 3 Mar 2022 02:46:51 +0800 Subject: [PATCH 11/35] feat: Add FromDateTimeOffset in TimestampTag (#2146) --- .../Entities/Messages/TimestampTag.cs | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Messages/TimestampTag.cs b/src/Discord.Net.Core/Entities/Messages/TimestampTag.cs index 347b0daaa..3beffdbb6 100644 --- a/src/Discord.Net.Core/Entities/Messages/TimestampTag.cs +++ b/src/Discord.Net.Core/Entities/Messages/TimestampTag.cs @@ -15,7 +15,7 @@ namespace Discord /// /// Gets or sets the time for this timestamp tag. /// - public DateTime Time { get; set; } + public DateTimeOffset Time { get; set; } /// /// Converts the current timestamp tag to the string representation supported by discord. @@ -26,11 +26,11 @@ namespace Discord /// A string that is compatible in a discord message, ex: <t:1625944201:f> public override string ToString() { - return $""; + return $""; } /// - /// Creates a new timestamp tag with the specified datetime object. + /// Creates a new timestamp tag with the specified object. /// /// The time of this timestamp tag. /// The style for this timestamp tag. @@ -43,5 +43,20 @@ namespace Discord Time = time }; } + + /// + /// Creates a new timestamp tag with the specified object. + /// + /// The time of this timestamp tag. + /// The style for this timestamp tag. + /// The newly create timestamp tag. + public static TimestampTag FromDateTimeOffset(DateTimeOffset time, TimestampTagStyles style = TimestampTagStyles.ShortDateTime) + { + return new TimestampTag + { + Style = style, + Time = time + }; + } } -} +} \ No newline at end of file From b95b94231cf9f0774b615e5c19ef59e7f59ccb1f Mon Sep 17 00:00:00 2001 From: CottageDwellingCat <80918250+CottageDwellingCat@users.noreply.github.com> Date: Wed, 2 Mar 2022 13:03:10 -0600 Subject: [PATCH 12/35] Fix NRE when ratelimmited requests don't return a body (#2135) --- src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs b/src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs index 3eb656745..c596f112b 100644 --- a/src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs +++ b/src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs @@ -377,7 +377,8 @@ namespace Discord.Net.Queue // use the payload reset after value var payload = info.ReadRatelimitPayload(body); - resetTick = DateTimeOffset.UtcNow.Add(TimeSpan.FromSeconds(payload.RetryAfter)); + // fallback on stored ratelimit info when payload is null, https://github.com/discord-net/Discord.Net/issues/2123 + resetTick = DateTimeOffset.UtcNow.Add(TimeSpan.FromSeconds(payload?.RetryAfter ?? info.ResetAfter?.TotalSeconds ?? 0)); #if DEBUG_LIMITS Debug.WriteLine($"[{id}] Reset-After: {info.ResetAfter.Value} ({info.ResetAfter?.TotalMilliseconds} ms)"); #endif From f601e9bc345e0b511b0038b2906b3d1b4d5f30d4 Mon Sep 17 00:00:00 2001 From: d4n Date: Wed, 2 Mar 2022 14:06:29 -0500 Subject: [PATCH 13/35] Fix context menu comand message type (#2128) --- src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs index 4be9f4c5a..6668426e1 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs @@ -134,7 +134,8 @@ namespace Discord.WebSocket if (model.Type == MessageType.Default || model.Type == MessageType.Reply || model.Type == MessageType.ApplicationCommand || - model.Type == MessageType.ThreadStarterMessage) + model.Type == MessageType.ThreadStarterMessage || + model.Type == MessageType.ContextMenuCommand) return SocketUserMessage.Create(discord, state, author, channel, model); else return SocketSystemMessage.Create(discord, state, author, channel, model); From 7d8911bfed9b8488055bb4c6b928bf01913d7100 Mon Sep 17 00:00:00 2001 From: Almighty-Shogun <96011415+Almighty-Shogun@users.noreply.github.com> Date: Wed, 2 Mar 2022 20:07:15 +0100 Subject: [PATCH 14/35] Fixed documentation typo's (#2127) * Fixed typo at line 39 On this code example for the documentation there was a typo on the README.md file at line 39. There was an `;` where there should not be one. The code that had the typo: ```cs var tb = new TextInputBuilder() .WithLabel("Labeled") .WithCustomId("text_input") .WithStyle(TextInputStyle.Paragraph) .WithMinLength(6); // This ";" does not belong here. .WithMaxLength(42) .WithRequired(true) .WithPlaceholder("Consider this place held."); ``` * Changed `ExecuteAsync` to `ExecuteCommandAsync` `_interactionService.ExecuteAsync(ctx, serviceProvider);` cannot be executed because the method `ExecuteAsync` does not exists. * Changed `componBuild()` to `components.Build()` --- docs/guides/int_basics/message-components/advanced.md | 2 +- .../guides/int_basics/message-components/text-input.md | 10 +++++----- docs/guides/int_framework/samples/intro/context.cs | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/guides/int_basics/message-components/advanced.md b/docs/guides/int_basics/message-components/advanced.md index 49b3f31a6..14dc94e40 100644 --- a/docs/guides/int_basics/message-components/advanced.md +++ b/docs/guides/int_basics/message-components/advanced.md @@ -43,7 +43,7 @@ var components = new ComponentBuilder() .WithSelectMenu(menu); -await arg.RespondAsync("On a scale of one to five, how gaming is this?", component: componBuild(), ephemeral: true); +await arg.RespondAsync("On a scale of one to five, how gaming is this?", component: components.Build(), ephemeral: true); break; ``` diff --git a/docs/guides/int_basics/message-components/text-input.md b/docs/guides/int_basics/message-components/text-input.md index 37f5b4937..92679ae41 100644 --- a/docs/guides/int_basics/message-components/text-input.md +++ b/docs/guides/int_basics/message-components/text-input.md @@ -35,11 +35,11 @@ and min/max length of the input: var tb = new TextInputBuilder() .WithLabel("Labeled") .WithCustomId("text_input") - .WithStyle(TextInputStyle.Paragraph) - .WithMinLength(6); - .WithMaxLength(42) - .WithRequired(true) - .WithPlaceholder("Consider this place held."); + .WithStyle(TextInputStyle.Paragraph) + .WithMinLength(6) + .WithMaxLength(42) + .WithRequired(true) + .WithPlaceholder("Consider this place held."); ``` ![more advanced text input](images/image9.png) diff --git a/docs/guides/int_framework/samples/intro/context.cs b/docs/guides/int_framework/samples/intro/context.cs index 5976ffc5c..1bd164d3f 100644 --- a/docs/guides/int_framework/samples/intro/context.cs +++ b/docs/guides/int_framework/samples/intro/context.cs @@ -1,7 +1,7 @@ discordClient.ButtonExecuted += async (interaction) => { var ctx = new SocketInteractionContext(discordClient, interaction); - await _interactionService.ExecuteAsync(ctx, serviceProvider); + await _interactionService.ExecuteCommandAsync(ctx, serviceProvider); }; public class MessageComponentModule : InteractionModuleBase> From 1fc07e742fa7322be55f6a10b502de44de32b39a Mon Sep 17 00:00:00 2001 From: Duke <40759437+dukesteen@users.noreply.github.com> Date: Wed, 2 Mar 2022 20:08:51 +0100 Subject: [PATCH 15/35] Guides for Serilog and EFCore (#2134) * Add serilog guide * added suggestions from Rozen * Add efcore guide * Fix review changes * Fix grammatical errors & review points --- docs/guides/other_libs/efcore.md | 61 ++++++++++++++++++ .../other_libs/images/serilog_output.png | Bin 0 -> 40492 bytes .../other_libs/samples/ConfiguringSerilog.cs | 36 +++++++++++ .../samples/DbContextDepInjection.cs | 9 +++ .../other_libs/samples/DbContextSample.cs | 19 ++++++ .../samples/InteractionModuleDISample.cs | 20 ++++++ .../other_libs/samples/LogDebugSample.cs | 1 + .../other_libs/samples/ModifyLogMethod.cs | 15 +++++ docs/guides/other_libs/serilog.md | 45 +++++++++++++ docs/guides/toc.yml | 8 ++- 10 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 docs/guides/other_libs/efcore.md create mode 100644 docs/guides/other_libs/images/serilog_output.png create mode 100644 docs/guides/other_libs/samples/ConfiguringSerilog.cs create mode 100644 docs/guides/other_libs/samples/DbContextDepInjection.cs create mode 100644 docs/guides/other_libs/samples/DbContextSample.cs create mode 100644 docs/guides/other_libs/samples/InteractionModuleDISample.cs create mode 100644 docs/guides/other_libs/samples/LogDebugSample.cs create mode 100644 docs/guides/other_libs/samples/ModifyLogMethod.cs create mode 100644 docs/guides/other_libs/serilog.md diff --git a/docs/guides/other_libs/efcore.md b/docs/guides/other_libs/efcore.md new file mode 100644 index 000000000..ffdea42aa --- /dev/null +++ b/docs/guides/other_libs/efcore.md @@ -0,0 +1,61 @@ +--- +uid: Guides.OtherLibs.EFCore +title: EFCore +--- + +# Entity Framework Core + +In this guide we will set up EFCore with a PostgreSQL database. Information on other databases will be at the bottom of this page. + +## Prerequisites + +- A simple bot with dependency injection configured +- A running PostgreSQL instance +- [EFCore CLI tools](https://docs.microsoft.com/en-us/ef/core/cli/dotnet#installing-the-tools) + +## Downloading the required packages + +You can install the following packages through your IDE or go to the nuget link to grab the dotnet cli command. + +|Name|Link| +|--|--| +| `Microsoft.EntityFrameworkCore` | [link](https://www.nuget.org/packages/Microsoft.EntityFrameworkCore) | +| `Npgsql.EntityFrameworkCore.PostgreSQL` | [link](https://www.nuget.org/packages/Npgsql.EntityFrameworkCore.PostgreSQL)| + +## Configuring the DbContext + +To use EFCore, you need a DbContext to access everything in your database. The DbContext will look like this. Here is an example entity to show you how you can add more entities yourself later on. + +[!code-csharp[DBContext Sample](samples/DbContextSample.cs)] + +> [!NOTE] +> To learn more about creating the EFCore model, visit the following [link](https://docs.microsoft.com/en-us/ef/core/get-started/overview/first-app?tabs=netcore-cli#create-the-model) + +## Adding the DbContext to your Dependency Injection container + +To add your newly created DbContext to your Dependency Injection container, simply use the extension method provided by EFCore to add the context to your container. It should look something like this + +[!code-csharp[DBContext Dependency Injection](samples/DbContextDepInjection.cs)] + +> [!NOTE] +> You can find out how to get your connection string [here](https://www.connectionstrings.com/npgsql/standard/) + +## Migrations + +Before you can start using your DbContext, you have to migrate the changes you've made in your code to your actual database. +To learn more about migrations, visit the official Microsoft documentation [here](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/?tabs=dotnet-core-cli) + +## Using the DbContext + +You can now use the DbContext wherever you can inject it. Here's an example on injecting it into an interaction command module. + +[!code-csharp[DBContext injected into interaction module](samples/InteractionModuleDISample.cs)] + +## Using a different database provider + +Here's a couple of popular database providers for EFCore and links to tutorials on how to set them up. The only thing that usually changes is the provider inside of your `DbContextOptions` + +| Provider | Link | +|--|--| +| MySQL | [link](https://dev.mysql.com/doc/connector-net/en/connector-net-entityframework-core-example.html) | +| SQLite | [link](https://docs.microsoft.com/en-us/ef/core/get-started/overview/first-app?tabs=netcore-cli) | diff --git a/docs/guides/other_libs/images/serilog_output.png b/docs/guides/other_libs/images/serilog_output.png new file mode 100644 index 0000000000000000000000000000000000000000..67de0fa34627a4854bb9107d6367d4074c2aa844 GIT binary patch literal 40492 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 literal 0 HcmV?d00001 diff --git a/docs/guides/other_libs/samples/ConfiguringSerilog.cs b/docs/guides/other_libs/samples/ConfiguringSerilog.cs new file mode 100644 index 000000000..0d4706424 --- /dev/null +++ b/docs/guides/other_libs/samples/ConfiguringSerilog.cs @@ -0,0 +1,36 @@ +using Discord; +using Serilog; +using Serilog.Events; + +public class Program +{ + static void Main(string[] args) => new Program().MainAsync().GetAwaiter().GetResult(); + + public async Task MainAsync() + { + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Verbose() + .Enrich.FromLogContext() + .WriteTo.Console() + .CreateLogger(); + + _client = new DiscordSocketClient(); + + _client.Log += LogAsync; + + // You can assign your bot token to a string, and pass that in to connect. + // This is, however, insecure, particularly if you plan to have your code hosted in a public repository. + var token = "token"; + + // Some alternative options would be to keep your token in an Environment Variable or a standalone file. + // var token = Environment.GetEnvironmentVariable("NameOfYourEnvironmentVariable"); + // var token = File.ReadAllText("token.txt"); + // var token = JsonConvert.DeserializeObject(File.ReadAllText("config.json")).Token; + + await _client.LoginAsync(TokenType.Bot, token); + await _client.StartAsync(); + + // Block this task until the program is closed. + await Task.Delay(Timeout.Infinite); + } +} diff --git a/docs/guides/other_libs/samples/DbContextDepInjection.cs b/docs/guides/other_libs/samples/DbContextDepInjection.cs new file mode 100644 index 000000000..5d989995b --- /dev/null +++ b/docs/guides/other_libs/samples/DbContextDepInjection.cs @@ -0,0 +1,9 @@ +private static ServiceProvider ConfigureServices() +{ + return new ServiceCollection() + .AddDbContext( + options => options.UseNpgsql("Your connection string") + ) + [...] + .BuildServiceProvider(); +} diff --git a/docs/guides/other_libs/samples/DbContextSample.cs b/docs/guides/other_libs/samples/DbContextSample.cs new file mode 100644 index 000000000..96104ae53 --- /dev/null +++ b/docs/guides/other_libs/samples/DbContextSample.cs @@ -0,0 +1,19 @@ +// ApplicationDbContext.cs +using Microsoft.EntityFrameworkCore; + +public class ApplicationDbContext : DbContext +{ + public ApplicationDbContext(DbContextOptions options) : base(options) + { + + } + + public DbSet Users { get; set; } = null!; +} + +// UserEntity.cs +public class UserEntity +{ + public ulong Id { get; set; } + public string Name { get; set; } +} diff --git a/docs/guides/other_libs/samples/InteractionModuleDISample.cs b/docs/guides/other_libs/samples/InteractionModuleDISample.cs new file mode 100644 index 000000000..777d6aef0 --- /dev/null +++ b/docs/guides/other_libs/samples/InteractionModuleDISample.cs @@ -0,0 +1,20 @@ +using Discord; + +public class SampleModule : InteractionModuleBase +{ + private readonly ApplicationDbContext _db; + + public SampleModule(ApplicationDbContext db) + { + _db = db; + } + + [SlashCommand("sample", "sample")] + public async Task Sample() + { + // Do stuff with your injected DbContext + var user = _db.Users.FirstOrDefault(x => x.Id == Context.User.Id); + + ... + } +} diff --git a/docs/guides/other_libs/samples/LogDebugSample.cs b/docs/guides/other_libs/samples/LogDebugSample.cs new file mode 100644 index 000000000..e796e207a --- /dev/null +++ b/docs/guides/other_libs/samples/LogDebugSample.cs @@ -0,0 +1 @@ +Log.Debug("Your log message, with {Variables}!", 10); // This will output "[21:51:00 DBG] Your log message, with 10!" diff --git a/docs/guides/other_libs/samples/ModifyLogMethod.cs b/docs/guides/other_libs/samples/ModifyLogMethod.cs new file mode 100644 index 000000000..0f7c11daf --- /dev/null +++ b/docs/guides/other_libs/samples/ModifyLogMethod.cs @@ -0,0 +1,15 @@ +private static async Task LogAsync(LogMessage message) +{ + var severity = message.Severity switch + { + LogSeverity.Critical => LogEventLevel.Fatal, + LogSeverity.Error => LogEventLevel.Error, + LogSeverity.Warning => LogEventLevel.Warning, + LogSeverity.Info => LogEventLevel.Information, + LogSeverity.Verbose => LogEventLevel.Verbose, + LogSeverity.Debug => LogEventLevel.Debug, + _ => LogEventLevel.Information + }; + Log.Write(severity, message.Exception, "[{Source}] {Message}", message.Source, message.Message); + await Task.CompletedTask; +} diff --git a/docs/guides/other_libs/serilog.md b/docs/guides/other_libs/serilog.md new file mode 100644 index 000000000..5086b4b85 --- /dev/null +++ b/docs/guides/other_libs/serilog.md @@ -0,0 +1,45 @@ +--- +uid: Guides.OtherLibs.Serilog +title: Serilog +--- + +# Configuring serilog + +## Prerequisites + +- A basic working bot with a logging method as described in [Creating your first bot](xref:Guides.GettingStarted.FirstBot) + +## Installing the Serilog package + +You can install the following packages through your IDE or go to the nuget link to grab the dotnet cli command. + +|Name|Link| +|--|--| +|`Serilog.Extensions.Logging`| [link](https://www.nuget.org/packages/Serilog.Extensions.Logging)| +|`Serilog.Sinks.Console`| [link](https://www.nuget.org/packages/Serilog.Sinks.Console)| + +## Configuring Serilog + +Serilog will be configured at the top of your async Main method, it looks like this + +[!code-csharp[Configuring serilog](samples/ConfiguringSerilog.cs)] + +## Modifying your logging method + +For Serilog to log Discord events correctly, we have to map the Discord `LogSeverity` to the Serilog `LogEventLevel`. You can modify your log method to look like this. + +[!code-csharp[Modifying your log method](samples/ModifyLogMethod.cs)] + +## Testing + +If you run your application now, you should see something similar to this +![Serilog output](images/serilog_output.png) + +## Using your new logger in other places + +Now that you have set up Serilog, you can use it everywhere in your application by simply calling + +[!code-csharp[Log debug sample](samples/LogDebugSample.cs)] + +> [!NOTE] +> Depending on your configured log level, the log messages may or may not show up in your console. Refer to [Serilog's github page](https://github.com/serilog/serilog/wiki/Configuration-Basics#minimum-level) for more information about log levels. diff --git a/docs/guides/toc.yml b/docs/guides/toc.yml index 1616363b7..b1a6b4721 100644 --- a/docs/guides/toc.yml +++ b/docs/guides/toc.yml @@ -95,7 +95,7 @@ topicUid: Guides.MessageComponents.TextInputs - name: Advanced Concepts topicUid: Guides.MessageComponents.Advanced -- name: Modal Basics +- name: Modal Basics items: - name: Introduction topicUid: Guides.Modals.Intro @@ -109,6 +109,12 @@ topicUid: Guides.GuildEvents.GettingUsers - name: Modifying Events topicUid: Guides.GuildEvents.Modifying +- name: Working with other libraries + items: + - name: Serilog + topicUid: Guides.OtherLibs.Serilog + - name: EFCore + topicUid: Guides.OtherLibs.EFCore - name: Emoji topicUid: Guides.Emoji - name: Voice From b7f6db96efbc8c7df6618e301902d772af0b7eb9 Mon Sep 17 00:00:00 2001 From: Ilay Nahman Date: Wed, 2 Mar 2022 21:09:57 +0200 Subject: [PATCH 16/35] Fix docs typos (#2119) Changed frist to first and dont to don't --- .../slash-commands/creating-slash-commands.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guides/int_basics/application-commands/slash-commands/creating-slash-commands.md b/docs/guides/int_basics/application-commands/slash-commands/creating-slash-commands.md index 9e35de285..a0a7d6fe3 100644 --- a/docs/guides/int_basics/application-commands/slash-commands/creating-slash-commands.md +++ b/docs/guides/int_basics/application-commands/slash-commands/creating-slash-commands.md @@ -70,14 +70,14 @@ public async Task Client_Ready() // Let's do our global command var globalCommand = new SlashCommandBuilder(); globalCommand.WithName("first-global-command"); - globalCommand.WithDescription("This is my frist global slash command"); + globalCommand.WithDescription("This is my first global slash command"); try { // Now that we have our builder, we can call the CreateApplicationCommandAsync method to make our slash command. await guild.CreateApplicationCommandAsync(guildCommand.Build()); - // With global commands we dont need the guild. + // With global commands we don't need the guild. await client.CreateGlobalApplicationCommandAsync(globalCommand.Build()); // Using the ready event is a simple implementation for the sake of the example. Suitable for testing and development. // For a production bot, it is recommended to only run the CreateGlobalApplicationCommandAsync() once for each command. From a13dce2550a1778169c368936fafb593915fa70d Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Wed, 2 Mar 2022 21:24:34 +0100 Subject: [PATCH 17/35] FAQ rework, replacing outdated info, better interaction FAQ (#2106) * FAQ rework, replacing outdated info, better interaction faq * Update docs/faq/basics/getting-started.md Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * Update docs/faq/basics/getting-started.md Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * Update docs/faq/int_framework/general.md Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> * fix TOC reference Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> Co-authored-by: Quin Lynch --- .../dependency-injection.md | 18 ++----- docs/faq/basics/getting-started.md | 38 +++++++++----- docs/faq/basics/images/link.png | Bin 0 -> 5229 bytes docs/faq/basics/images/permissions.png | Bin 0 -> 60409 bytes docs/faq/basics/images/scopes.png | Bin 0 -> 28566 bytes docs/faq/{commands => basics}/samples/DI.cs | 0 .../samples/missing-dep.cs | 9 ++-- .../framework.md} | 43 ++++++++++----- .../general.md} | 49 +++++------------- .../images/scope.png | Bin docs/faq/int_framework/manual.md | 45 ++++++++++++++++ .../samples/interactionsyncing.cs | 6 +++ .../samples/propertyinjection.cs | 8 +++ .../samples/registerint.cs | 0 docs/faq/misc/legacy.md | 21 +++++++- .../{commands => text_commands}/general.md | 17 +++--- .../samples/Remainder.cs | 0 .../samples/runmode-cmdattrib.cs | 0 .../samples/runmode-cmdconfig.cs | 0 docs/faq/toc.yml | 22 ++++---- 20 files changed, 179 insertions(+), 97 deletions(-) rename docs/faq/{commands => basics}/dependency-injection.md (71%) create mode 100644 docs/faq/basics/images/link.png create mode 100644 docs/faq/basics/images/permissions.png create mode 100644 docs/faq/basics/images/scopes.png rename docs/faq/{commands => basics}/samples/DI.cs (100%) rename docs/faq/{commands => basics}/samples/missing-dep.cs (82%) rename docs/faq/{commands/interaction.md => int_framework/framework.md} (52%) rename docs/faq/{basics/interactions.md => int_framework/general.md} (52%) rename docs/faq/{basics => int_framework}/images/scope.png (100%) create mode 100644 docs/faq/int_framework/manual.md create mode 100644 docs/faq/int_framework/samples/interactionsyncing.cs create mode 100644 docs/faq/int_framework/samples/propertyinjection.cs rename docs/faq/{basics => int_framework}/samples/registerint.cs (100%) rename docs/faq/{commands => text_commands}/general.md (89%) rename docs/faq/{commands => text_commands}/samples/Remainder.cs (100%) rename docs/faq/{commands => text_commands}/samples/runmode-cmdattrib.cs (100%) rename docs/faq/{commands => text_commands}/samples/runmode-cmdconfig.cs (100%) diff --git a/docs/faq/commands/dependency-injection.md b/docs/faq/basics/dependency-injection.md similarity index 71% rename from docs/faq/commands/dependency-injection.md rename to docs/faq/basics/dependency-injection.md index d6b7f8b58..fe5686797 100644 --- a/docs/faq/commands/dependency-injection.md +++ b/docs/faq/basics/dependency-injection.md @@ -1,12 +1,12 @@ --- -uid: FAQ.Commands.DI -title: Questions about Dependency Injection with Commands +uid: FAQ.Basics.DI +title: Questions about Dependency Injection. --- # Dependency-injection-related Questions In the following section, you will find common questions and answers -to utilizing dependency injection with @Discord.Commands, as well as +to utilizing dependency injection with @Discord.Commands and @Discord.Interactions, as well as common troubleshooting steps regarding DI. ## What is a service? Why does my module not hold any data after execution? @@ -22,8 +22,7 @@ Service is often used to hold data externally so that they persist throughout execution. Think of it like a chest that holds whatever you throw at it that won't be affected by anything unless you want it to. Note that you should also learn Microsoft's -implementation of [Dependency Injection] \([video]) before proceeding, -as well as how it works in [Discord.Net](xref:Guides.TextCommands.DI#usage-in-modules). +implementation of [Dependency Injection] \([video]) before proceeding. A brief example of service and dependency injection can be seen below. @@ -32,18 +31,12 @@ A brief example of service and dependency injection can be seen below. [Dependency Injection]: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection [video]: https://www.youtube.com/watch?v=QtDTfn8YxXg -## Why is my `CommandService` complaining about a missing dependency? +## Why is my Command/Interaction Service complaining about a missing dependency? If you encounter an error similar to `Failed to create MyModule, dependency MyExternalDependency was not found.`, you may have forgotten to add the external dependency to the dependency container. -Starting from Discord.Net 2.0, all dependencies required by each -module must be present when the module is loaded into the -[CommandService]. This means when loading the module, you must pass a -valid [IServiceProvider] with the dependency loaded before the module -can be successfully registered. - For example, if your module, `MyModule`, requests a `DatabaseService` in its constructor, the `DatabaseService` must be present in the [IServiceProvider] when registering `MyModule`. @@ -51,4 +44,3 @@ in its constructor, the `DatabaseService` must be present in the [!code-csharp[Missing Dependencies](samples/missing-dep.cs)] [IServiceProvider]: xref:System.IServiceProvider -[CommandService]: xref:Discord.Commands.CommandService diff --git a/docs/faq/basics/getting-started.md b/docs/faq/basics/getting-started.md index dc4b11548..ba5782ed7 100644 --- a/docs/faq/basics/getting-started.md +++ b/docs/faq/basics/getting-started.md @@ -11,18 +11,32 @@ introduction to the Discord API ecosystem. ## How do I add my bot to my server/guild? -You can do so by using the [permission calculator] provided -by [FiniteReality]. -This tool allows you to set permissions that the bot will be assigned -with, and invite the bot into your guild. With this method, bots will -also be assigned a unique role that a regular user cannot use; this -is what we call a `Managed` role. Because you cannot assign this -role to any other users, it is much safer than creating a single -role which, intentionally or not, can be applied to other users -to escalate their privilege. - -[FiniteReality]: https://github.com/FiniteReality/permissions-calculator -[permission calculator]: https://finitereality.github.io/permissions-calculator +Inviting your bot can be done by using the OAuth2 url generator provided by the [Discord Developer Portal]. + +Permissions can be granted by selecting the `bot` scope in the scopes section. + +![Scopes](images/scopes.png) + +A permissions tab will appear below the scope selection, +from which you can pick any permissions your bot may require to function. +When invited, the role this bot is granted will include these permissions. +If you grant no permissions, no role will be created for your bot upon invitation as there is no need for one. + +![Permissions](images/permissions.png) + +When done selecting permissions, you can use the link below in your browser to invite the bot +to servers where you have the `Manage Server` permission. + +![Invite](images/link.png) + +If you are planning to play around with slash/context commands, +make sure to check the `application commands` scope before inviting your bot! + +> [!NOTE] +> You do not have to kick and reinvite your bot to update permissions/scopes later on. +> Simply reusing the invite link with provided scopes/perms will update it accordingly. + +[Discord Developer Portal]: https://discord.com/developers/applications/ ## What is a token? diff --git a/docs/faq/basics/images/link.png b/docs/faq/basics/images/link.png new file mode 100644 index 0000000000000000000000000000000000000000..dd6b520abd2accd4104c880ba547a9656224d607 GIT binary patch literal 5229 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%0DB$G7^^e8uQERTx=J~VShq9p)zliugn%S5k# zh!&gvVRYuQAU%;2MIOvRZ+yO3e?uQ5yP=Odh8byX5zl@VtqSEF8^3vVIwsn9N&p6x z)N?Mf%I&g|D#8Bx{^19$WHA>T`;i~UGXKj;KxwJ`)&%WMeZ0N*kLOo^L!!1_Kc|>p zl3rUUDBC}>0vGJ^n?P4~XVrUer52xlW}?5_XI>Ye#lo@$uN3CBee*9>z4w^-VqM2| zzY6lz3DzDX=i2mA%V1?XFNr+6Mt$uRk%N0Tx~H#%jXIKV+0hp-;4}b)ImZv1a_6V7 zD$=9ZH4o`oz|9p!{Sc-wLvWZlsjKE|M$f?rr;qU1T@f33-bc?9%Rle@z?ChC+;|GX z<6`$TiD?8xMbp2M`=qJ+p3!?uqFQWwSaA^%MP*%q$;K!-goG<`seKBXDTL&uMGYToH(>lV@ zQ(-m~M@Q592+|#rU+IsLmrA#0?QIoBp~3^q&H#gyaZlWlF&g%JigxNm?}m4x)f>{S zjx9N|i{mYBY#`Xu_uwv{tZj-fEMr%#mpmZbg-P?+DYj72_p$jM2Pd66a5ACZ?=4r5 zi&`#d8`HYa9ip0iK425--vxboUEaQ)!B84iFy4LX^ovxm>cg`;K>e?DZJYxVX8Rsy zt-Y zrDIrNFRWNGBTiECmX5dV^2Yh*Qu&!r%Fp9I%V9=7$QypgAYLSi1j{_~mzq`gLu|~J zqTZsZ$3)}S-g*|DN8bZII+x(ynBru1q`htSwSF(b6`YBTcmBIvY_R=Kv&R+C^$UOW zBh0!A@iuH5V`dFdKRRAj88iE&(@cvE#|PdX++3>>qk-Oq{vj)`CC}0_m`Q#?Xvm#I zpy>vHoVEZAgRk`kzjw*LO78_Xwu3-|2LNup{`%a+zHNR@0l<|0a81yI`SX={gNKN9 z%HT=M!=UOre?+8X`oPv_J(HwsvlSd0bg8!h0`9LpZv7izkq7<*HvPwkKBl!TeEfd> zU-K&d|4f9V^(oXyzQ&`Yk_WUi6F(5I_P&D$_k7z7Dd<1O5GbTyRm4_^4PG`aW~iiz z9>kYgtTcp6$+N$9e7Z#y>zU}8G>tH@N{FMfnqLJ!Pv&_<^=Nuiw_(%E4TbiGX|&bC z*0x#nLFbw<%n5PFDp7*y7;XY~JjG2T$}` zn;+kDPZ*l>n`PecF)GNG@Y#kdvM%1PoxQ z4A3Et2qQ>={ylq>i?`jO>&}kZCQ>bV1KPy##5bP20m;K#RXSnqMTOq&L{g`Nzo}wI z2TS==gYRhM3Q{qB2oW<%F|vKJx4?OH6GMHgA@WgYdXyCawlv=}ENwC(PZURPiIW&A zR-x?9lAf*80ULvW5tZXQ-=oR*=l~^+7ibnxb26cOoRbdhU?t6N3 zWaMn0sn-;xVqI!p`6d#FM#cZ^qhk6ZKkS~uJ3tfdJ_<#) z)Kseq9e%#O;=Rovi|yw#d%pWSqh>c#D@$V5}A ziuY2|$?KRCEGRE{(|vyHTDEd?sS&+HguTha0UitgzRSZQH&U-GcfDIbY>_YK4u%Ha z27g#>%YEbV1tgdA8-O*}{UODj1$jtSinW7v5JFEhsDDJ6G5#e^D_B6cR55bw$`t1ZDUS=KXH?4HbvGqjibR)dOf?~`%SaZ0lTXg{8P(#0|LtnN+=XZo zQBi@(x+UP`Wr&gPks`l@G}H(JbtyFN&s{>5bo7i@yE*oJN!H>LP`C198Eeuf4N$jY z)ni>lNg@rqX3z`HRK#U;9c-k?8iKwl+xKT z9vHgqlqfLqK7QJ97vWL%o*c^`G;el4Mu)M~fc4b{atiPyQQzkc6bPQfi8DE*2 zsl6XO?cB(%@AvDsUPt(8_2>=>L_2HW(QxlP6>8s5*-_U4HPQv90;Kgz?A%L zT+e}xZ6&NzsFs4j`YS;PR42lU+aZXe3iP6PfJ|TL+N6G|+O(i=!>MjY(22wGb7~#O z=Dr0KRF<9g-V3pCYZ1c-b7jChCv-9N4~}hn&6U{{$6O z`Xo>mz&TFd?U$F_3In(eRsYf-*A?l$Gd*Vb1@4KL8e-H-N;&Jac99G9$lO{AnZ z%sYXzmh9Z6;g2n>Tpxsh9GU=;D&o$2#yjTYtEs(cBTA1M;sB}{PyzkOn#geozDC}v z&x(A;K8H`yZaPuYrQH^M~Xm;Hk^pJsF=-Y-#_LExLtaIYHW5KGRT5#IYajPa^%bI zQ}HZ`du9v4=vbP*o=~%4(S#$yp#)eUbu&fi!fLp&+)w`9K&iAlo_7BJv}cX0+0AIQ z(R68us8fDv4!rVGPS@2Ho0fviq9rp9!)LmPk%tdY9IhUe z7QyXKK^}p5ePyODJ(tSJ^!LhRxy+P%yVqc8SG{!_X8YLh7ex?`x1|1Pn<9T>i$?>l z9}c&;xrO||acOh0P~2Kh2&ncHe7GW#0+@!k#DylFvpwrbUS%GW;c+B7UiMIEHjk}y z!Lh5ewNPxpXR%6LQ)=mB(#CSyatf>fDRE#2Z|$AzWk`RNLVm24Oh9Nr03`>${8gau zFk9f-Hpr$6z`8r0C>EA9Tou+-P3`H{Idwbv>2%>#y7mnT@_8h7Ji^_4VPu8jtG|J-U$%9)VOXw}?8h;$mC}J!^d@Sa(|lweReq zM;ZdCx*jlM?&4=jlZ3s#hN8ki!6mJVi`Bnm-`ftD?Jt(++d6pz3N7tHp}>+U-J4P82Ph_mRu1or7S64-YZRG@Ir3Mjz{iq++WAY znMZW2?$X?`($(vXKPH%0GjlH1Cg z7J!i^g@6RUGf_#A>t#iAh%scsxL|$Ib&>z69U!bRQWm7w8?$i-WZA;M$q=v?{zHZ* zf0JR&HW~Ib;tj&|Z;SJo{y&%+XYrn>od}&R!u!Fh@4uzZIotn8>-%S^+cfWgb)oYA zpg;b5#n^wg^!fiILggPB`hU_cONO3mSXrkfj&8hB%$*>*ScQ@Vt%D>7{#T3U&PN9( z`yIdCzVEL&&aMETvS&dlsp6Ih#Zb+>PIuv$4s&2N*46fIZ0-EXG*RZ;+BX$OwGO3e zL!xpEsF67($s)i!sXRGUb}t~pfyR8E!n3BhZND3I!|0ctPV#82bsir{H-YTCz}{i1 zl9lO&JpigYL$J*Ay{~^54ncfqE$?02IAXQ2W_Wn@cwTPxoXP#<=q@wI`iJ7s@>FHc zcPRlp6q)F`_fZdE>zt%l)c~b?zC^^=c@>=W zHc4sQ-&v>LUD%S?2MgwTcU+wOY6zL4YhK({h!C9(%$07#LEC3*3{gkkfox3yp2OK^ z*eDQO9g|!f2U_gSYhQN&e4J!zY`F4gTog*56$8i+37Z7KHA<`lW2OO*h<1HOL?F$Hmz@R)pcMr01>r zAxI|eO=B-j@HWZOss(kXbX%f-I@p1)E%tk1X{e}%)g1nmdH!z*j5zE`=^wEgD%)ln zQD804Z-=O{J|$H~&UE*S7nnFnm%$><{Hvsu(_O*8yo|t$)W?S(o3;C7gq1dz3z*ID zJW2#3Px#qp(9wlt8Z+OSHEpxb`tIRhg;W+fW#I*rav7^-4sF;-O1jrwggv-#mE3!( zD11r>tVnqUQB)`j2nfQAJPdON`>_=%Gn%CjgyylX0tKr4`NidHCt@u+SX=6?H($M` z8vW6}k%d)>Vv4`+TOF4gA@FDWi$wC#h6}Sc)>BW|>NV?TKv)~I=cChDtVQl<(gglJ z^{Qh>lbf-sC88o-TAaLp!GY^=5zjZD#O59X%m={u9auHs-@FA6d!>f#gL*UlSo*1cP8-hXEW9XL3x6Z?BK6sq% zoIiOax?9=`<)n8*)4DJ=5WjO;2bzY?c5yoXTX#OfF7(ClT9-k88LkaFVntG7M1FKIxUXA_SG$~tvkHZNmliJI88`4dr97`{+?aIMs&gaD=#N&`>kQ@(ylrw< zg-7~FY0dWq3hqw+>8&4j!F{_1<24ll5z?do(npIsP@i-rp&{^AEj2ZQ`l4*=`#<*2 zG*Qk@%u`5fsHRP4pn6-YhZrOSLR_{?4SZ=XEKzj@l!?Fkfw*vnT;-Q-42t+x`UJ9f;gK{+Zpo;PGfmRGY zNep@+k1~}B&!0zP3Yc9#!FPFg}DV#!?z~3 zgmk7kPKqK+%EgNM61I^({>`WFFECw(Edg_(fg8@n*J`ulCiMgO>Uz>lEwRyjwDkFH zeto{{HmN#9UM|@M{Axp_pQ^a(D?UTio?q_l!N!3^A`(Prd`%9@<%Z`lEJ(FRPOejM zrqP0fKC082hK(RNIdX1!%qmZ<>t;miTgSJUTMQ~m?ra_Ad(^Y-UMSV+8=RB<77=Eh zkGJ#LkYpY0p5HCtlr@E+Fyi6QGUAl|I|8l4?}6+3RFQe2zKbE|HSqTg`ulY)h`VD{ zG*6>s+@vx|@Dw{UNG&7IxGY-g5T>Mf(=cpig0!qoQGX&i|yE4^A@p1KYd*Z-Px2JvysK0B!nw@>~pvLyYKldNk_h8 z!B0z+p0C`%&1G!3RlN7q2+4k(c4=_S5n9}6hA=!>!+WuwPyPn2+It6#_CX*(`7_PD zXR%GuJL5K|YbSNtI_&3KjQ6J!NwEWl8uf1Uu7%uvpfl%zNznE^tW?Ga`<{x(1KPPVC)3##-Z<_<9_!ShJ@rT(c{y;Cq;=M?GKFi*k$F+Uq z3;SOLw{!(o^OlC!IEAcyW1x0n_8x6*zVvtJx~;+2h|xfxl0*LV`b*>w_RZ2FWmiki zD(gZVmzvt`Bu6C#9E}uk=kEqq2e$L*0!bMlJ#S=;!kAmIpm*9tbnl{GckM#1Ne=WF z#zSgGKT|zRxDWq5LdADFi3AI(84~QBpWUZfAflZgI}oaq?i3-`SoF#Y^w$x77!PM` z+t-y=!=heqi)swmJYmdA!j=G)Ha~6Iq6SkHLP3$f;ekhOPD=1lJ8306#xrINK$f}qciR1M1=##vxTZ(lk_7pcrjfndRmvTQ zf;I}?7CXxfpVYs!c|&kWPA2;LPpy;3D8EvNPpT=*17dB3W`#QG*3Z0u;MYW}y}0!Q zH=p?M(_ez#J%*AOw|&o215Jqi#2*Dn^rf7VD+lGo!n4~0lUh34fjM&=erIz95?+L5 znf)WhRcU`K0?c_@k=3+WCRaNzRe9?n{hV;4gq6*>$=L3}y%IPN2x~pkzbDv4Xx^h*`|qVRG5^t=ZbM*oY_^HvVEun%@4cg% zO566)*g!?Fpdcus(nOkc2m&fdk)|TODMdg!DInE`h*Ff^QA!YL0-;w0L0TvQ0)&o~ z5E7)9KsZluX1@3P)_dOHS?jd5&N}`xGmPS9@14Ex>%Q7Zi(6MBr8m^N7?utW!h;24 zLPUHPkFd*LSum5O;x$xH|Jdk^xda62VO5ziEa7T>TBb@TYKq`piZ zUe8$x#!>bo7zXCVTFJ80=1+a~pyuR$MX#ieD8E>T;Tl%qh6-zFE(iTO3?H1XXc+D( zdU<#x{^$ip*Manv>6fv>bkNhM1P3$fOCed}6_;m5-i7wd0s30wW7X~_@xqU2>{#a| zd|Z$IcvgHjpEKuCqR8jh$(INS)v*8%W5`&mEI5%_?Ff|vv+$%__@^3Mlq94SI_(!? zKN`0DP5RI@APjTdNne;vjuP$XR^qwjLN`Yr&Ir1alJoDM^Q$&9_T@s6w;z}5KBLKp zM^}FAiH9d_r^@<;J4(a2mys%TUV|hAtg1qB0eTegXv^EBPZ*`dZsfoFnBY&LI0xrf z%2`o&sxi**&|5evrD#+BeH?>uEYrnPd+r)>C&Pzt%Q+Y{DHA3b-res9*NduoYoNxp#V6ygroj9!aP+rmm%vDCJn~!?8MsIPv zM)o1YxGSr9&OyEQRIu8=ug2IF$@$b0Z>4`DY3-=PIAqm{>+dBI&X_5=BfW4($i+LE zr<6h+U_?3KTW~Bbv9`yecTtQMKJ##utH5K3>XMXWN-u498;ES<%@HF#>H6tdZ{A?c zd3jUyY%C^ZHy2@p|4ly)m1vBWDL|LIo+l1F1eASNPwO3y zY}3K~+^+2QbXy``aesZv>CA3*Yoq^}2uiIiB~wz$mc<&`>khOvW2Kw^jxJ5q5h05t zNGbrMSttdLf-{a9D;bAVe2$Amil4YxYrcZOD)|0JIT#miz_q1-G^~-2T~X6JKIMY3 zjpBs(q!*rE%9S&;(3V}MqQ6}Vbqu*PzGhOOoqrvRPOc0AELgV`uCuh4#ix8laSiC(t zWLm>0!pYb2k@zE5uI*hXW@%|?3uK&IpU@_Oas?;Ou zyC3^lA_|?(w7z1mL9a!>EdV=s)RT)3z=p z1-T&`1AU!nB+BNQ5-~pqoUrlzeSJLD5n$Kguo<%hsWUo>NYJ4f{9Y>Kjm|j4jAZJH zB^gFEWb?CaeBi@D?&fN6Dq(W6ySK2hnpJmN?ptc}(s`df#5kt;_AlY88xZu?2J19) zQdi9E24?n*hGPHt2V4I52d994kY-36l&iRW#PttP8+G8{xDjbWU5@uFgfOP!ynz28 z()}wqOh)JbnGlvSaQt9Z5Sbk4|G}a$(;7aJFWL1U)&=%Ao-E9UD{d1iJC`E<>=pj4 zWLnC1i}^Nva~e&I3|Py*G{%vCG{&tojqzRLS5ooDDa3vV=E8s5gV26(;q(9FOJ}^V z@o5Oax=lFmKZb8t2x^E>Zna`k$uSPoe%FC^aV)wgV4js6mqQj(Q?+A*B${QiD9y>) zNEGnIy-c+BDCHLIHW-ABa)q#fj3Fv^>y+)8Da*=+QR}x?dqilxK-f68#viI z6j)r%yZn;GGvkg1ic_sqX*MnSA+cWJVnZzV=^(a-0Q-K~dW)a+U|90`s=@w!ZAUZY zWveA{?aWgOm{*Q`agF!=j43l*F#;|EfP5viNk7LIR0~&R^o`9GFKcirHjm3b`KgLi zY;Jsqw09EsLRm|CbK5V=I*#6_Sr2r;EEsQNxg25WazR~JkT*nerMbaH*CL%!D8??k z(!JGwA%t2=v8?83@ps;#ehIQ_cdw@mrATl@q_23V;EjhcrYYQ-IfhB|k&Ro~-956G zmqlLR!AyB_2OVH$1>7aE&*^b7c8sCV!m8XpAXRi!Lv4 zewR%J2P^F7?C!|w%3Aw0zYwCp@T>^f_D)tVhhIwy18Dx<;Xi0TIr~=x@8Eup#6b0C z?hNM?7&&A7SO6y~doS?zr1Je!36yZpyQY0S z%ieO~p5EKE!`V|a*L1E)^GfrUa>v9m_)!GdkKL4#I-QaP$+;r4xGaT$+T)cE$1&xyC z_>W<0M);T)Z3g<=)%)No(dH|g0FSHf5+&6arnrmZBL9{MlUEX;xiGU1`#B$ZZt0d| zSYdTEBH_kqH8gPEcy^c5B~43=5YDNXN7}tZG&jgqABUwZpwhYgQ>N+b0NzI|SaCgS zVXqd0Y;0-sy|$b0{IK6ZsUQh@~=18@(t!`H8+ljUavL!@g~qgoL5^&Z6P1UTqbrZ@%2g_!F6>Gu{~YO zRNY}CS7U_d%|o!%{b?G&Ch~x8oZBLa7H@_NSWZDIZNtM`f*|R8hZ9A+AAbLm_Gt@At=f?;)lgB zvV4AlVzniDTYDt%OHayTl1b~T!pe|W?%L9^X=7%t?q9uAel9y?kNZO?gC`4>7IRB< z&YP*?NzrE^AN6SqA)TicNd)16S1ugSfo`~8*UC%t<8qkcY($paK3B^8h z=awa18+M9*Eud5wyDEQMY@e;ySDkpx8`+ghYNvo%cQJtw8|x);t#4Q&Dfw@N4oR=i z*Q!?>O$(vw5&Y7yFE0epDfp-6(qag5#(KK?Vx|?OZGTatVtpBl*mM^i7Jn%5BtF z{ek4m*LeH*mZJ%t*Wux};!n$Me-+6_DfIxm>fkhotf~KW39((Q>IszIMK!%QAv}KT zMUa%!WJ;`qd>a7Z6%q;uTYb@?k>%PU8o<#!5pO`#cqGaPe0KH`aOFS_iLRqKzEmXe zV$RS1gvn)nc2lX1cnlle^AXThd1j58J!u)LZEOW*>!qI@16{%gbalHOEORbkzZ_9? zk@x|83GML?h0HFz`tciy-Y!sQ_I$;4D8!U~Z`KpQ3;PQkSO8;V_a>tSfAWJl3kjc5 z;SSx$qf}(6P|j}(waicR1M~W)t82C^Ln&>=g8c<(@^q}AP1t<;#Bwr={hK(VwgYEV zwe8MG8#UT~a>2CxYRK_+Sw%xJ67P0P>AKhOwD)j{X4Sa}GADYN4^X`vK=%rvCN)!={n+xp z5!2fBQo3u*o(xLgBn{&sD6aYWLu$jyCP2KW-2s%O2Eb|JSHRmh4jiPfhnU_4i42A+ z3eIO9`8-59NpX?mUqkU(u*^CR92bAVAp%^NtQzQYOEa;={sLy_bGp#f?>6#6z90^lSuOf^O|Z8vif9xt-tayAatWa@z-U)B}UBWpLR%W zBdrz~xYtCXJ)Z4;^`?KUoP506adbcwF8>^oQJ-SaMx~E-1ooOQ3isSwSDG~*Dw|00 z)H^@xtaIO3{CPfR*+@9Su87#fN7?H6j88cxP+c=7wOq4vc)_F^3uU#aF-9jGOV3F~u7HrsX`Q#5awdb+EoPNQ z19f!y{MnUn4UJ!=1GnVvY~EwQr1r<*wD}rSf?<}yn-{i6pMUV1GR@RpH64^J_?0%m z68pX9@($Rc#P`QAXhw7sbQw^(nb_wg)`VgG@nkq!x$BKoW`uF;e?93+%$%cVpf89) zR5MxHZT}fy_lX5cx8JQzWqdQ`ZWKlQ(ahRT(wam(^k5}?pf;0%QM!Le@ zJ)b9nUGZ$!_IgSgU@YXA)wI2pnUxM;_fb0~3%yh=oPbPtNb~o@MGu>@G*G25!KW=QrD!A#Vtm)D_2fiCTPv zaan#IbnT;1cB>x=!WjjwvaUKi3!&VV z3Vij72%sohqD%4M*mQyjSN%EV=vIRagzL?2dg!g3PV=ZRzNQtC;2Yn+bT3etc?M~3 z-=UNva_dI`9oxQ0Kf>-5HAwmDeR=P>Dy7KLuGd58yn&q64p=N1V62qh`IT(%qd3hS zF(}4yL?e2_K`+!!d^NTL2F)=UjV`I=U0$f>(*_K8RCwFg3(YFS_qTQPbbd*jdlBjF zHlqhhM9u-w9jQf3T4<07Zg@%1Bin9XA5S! zwed|{9xd-nhl2t(rQ7X?280@tOpzKWk_{vLrVFg5nnSL{sAjoD8`ZYMIiMkN(6U!@ zIVr&b4X5M<1udqh3#})wT*u25Zdl$yF&jJ2tuov%}V}y zW(uHBsk8w3>>pY;MTn_Ki&d0g=xKKecjC_X!zY>iSL$tC|0_4BuS#i35J7$$cp0guDKbJGZ_7j8lg8-M(%{`j@|HJh6fAQph z3A?+2>v*YkBQjWfyO{SsiMtuqNE~KMfr`GY#(t;%!*&6s zhG1i1HxQ@iud!v3ukIznH}Epi5bq#HSr*EIwFVjhlc-&nXy)8Mi2VFQkEPnbWyDcP zHlVM!M9ZhQl}TMLn)VCC-)e!3ou^6NOf1%0fu42s%C4OJXARyF5TNt&YoW;O11o%N z4mCNvt5XajlK`?a?19g;el*OQ;Fg1~0_HmfkT$*?dGO1^ho=I3gpD^t<*Dm%y?8Ip zubMnIb;GvsB!ExfO`$D+O8?D)2lAi>ZIHNz_pshaG@uM?uUzb8dH@m%hdPYjU4q=L z1kYo{s$}aj+8(Bu*L>rr(r!hQL%6t8-kF7~{FGE}$&xs~ec*)791AMB%(3Weq{aPX zFfqI_wXmWq(t4D)L+{jQxev@lEuFYbOD15P6^_Tgy)+st^C(WHpdcoaJW&YpQ8OaK z`{i^v=$yjZC0)oGsn!Q?$Fwbw)FyI+62PrwdJRnKb8}dvBu`L7jpO#9=>rz=+5s4< zUD9pZfo~8T1ij*z%7|0Sb7CU$L!muDSQnlWGK5!XtlJ4z(lSNvn`NF)0 zmyJq=mAO%hCh^)_!XFVlu%GJIq~lf<;@*g~Hacq;qKXPvQgzsUR=u$D;tA?;XU~5i zc(jU*gS-hW6t|j)!r+wLx!_;2L+kJ#W^mtIj?OFN%d|zbz^IVu;;TZQA!fV!LxZ<7 z%BL~>*dyXogX3O9;rXRB#k316DwhsKvLF8jUc`zpYUHLc*{mHr?H*|hyq^7RCqVt# z@UHtq*D$h)@PVLAYIU@I4A%nMb7AUQcZTZ zXt|>W2nU+b&&+Sfq!?J?3#)@~E{b|pzpIUledq!lmsDBx^eQVB>y5|b-no?&K~Dc@ za{!(<6{0n-^XC<95g`!TtD<5He%kGhiWVha=z_YBQ;Rx01og`m(dH4HUtNm-e#zdw zv{YfWm{Njd^F0?sRhfYyc7i(IftU9f0PZz`%(Y}3k*Z#4VZBH48-!ukX}Zj=_OJmY}VVP2K^xx`@o$?Ta}EGGy|J?eH;pgLp2(iJmuFS zP~UZ<6xe(vFAt9RNl)-wT)P}^2hcB9&^K-SU`dVL)FZB&d^TsuIw=o1E(F;?{ftLky|udorz_oF zqWcmA3|xMA|K=MUw1E}@KD!;&7Pkf+^Yw?^ZM6S!g5ylJ=mi*ReM42nAt9zW>jv;! zZ%#bxg353)E5;$a(MgP@THd=fO94Ii8GP4UQjz_x5cU%Gpz)4SoEnGv79wouYs`sG zq@=Cl!z?UQi|3s}LYz;N;0^p{k9ZcAxNa7MbfnXFM+Rm)`?LZBlb?C~Mme>O{IwS- zsyAthYB|jV3ow*YpR=WjCi|YG+nL@K@bFpu_QjKrGFKFy{z6{E+dO=7$4l#2@7YcO zxEH*urFgTAU|PtD_J)(N-STy-?k}TFGv@S=vORIc%r;$(i%yf^3Pa?Y)_hjNFv?8I zDWSAl8?S>MIX)!iWRYX`S__}J>Zj}QU0B>et)F0v9l<-<74ICGEV48Skm@IU$962g zL~%%oKIum{%WADvclv#>Ct8qyev*|M71`K{C?}V#4DN)8!Z2J$?C?{5m>m(BMPb^S zV(r|J^~c7q)g0<3CSp6fF~guof&RDTdm1r*NvllXJOmTX3Q6eAeTTE9`|3tqM^>Rwxo zm++h}B}9l>Ksp3!6icbME_ZA?R}u3bxJ-Gw`dQ21hIxvRbv@eAl_HSylfbS(gUJsP zkyU`{4rq8l%Sf(g-ob|+(0S1$+ZIOUgxYu&RUjokmibCi4xw1=6Nvt569}~97Nj|x z@RgN)0}2n2u>n8_}kb$t7Op^$Y2GbgG1pzqvf9qz+jNI1D!r zOd>AixECjN;g(_o(?{KP+rO@qUT-M9=wUy=8(hB4T%bO*GM4kfZ~GnZVXyxl}G`?a?PqPO6N}bWz`O z*ZBla4pqwWF(RO0#*QBi$Xs2!Mwa(Ao^(8}P|)=Vyg?(e87ea{{!Cr(;86zDG}=?TXfF{(3HFC98Y9i3?_mu`QRl_N1<7nX+Fu zwoa0tBr&z{>HE&17azM9MEY+E8h~>SK5lJ0qrSWvm8#*C*zD^+jzCPo<{3RKuszlh zrm&GF>}+Nj`mqxP??y&%YT34QG`18Hto3lA`i?1ov)wU#&f%Iw89`@%4{PZBzVHF* z(UYxV^GK-d=Iw$5X#x^t-*D59&RArvdxRfu)f|SBR}63SGTmCn-Y`j5tl=>#l4p(G zO4EJ8(&hnd4FIAi5qh33e!HN4@_d(m?Dws5@G1!1gF+w~WA}}?H#&Bc^60*x%T|pD zo5407e{x9w&Fyd8M!n!O@v9)${oHvL{Ql#CG=7}=;1YSZdhX}K*waE<&eaWzA0NRT zeg@p(G@d+9k%rmVm75uv$js+_!_#!emmS#cUS61WjRfa8-bJ-g)W}fv#sTw7v+(AX z5~j31rU&-{YJc%8>D990D!)d@qI$P(00R5{!Q4;1s@m`|J>S&aCHdHeT$w9h3@eXS z6>!_mJ%pcob_d|i)ADZ?RebGVv`F`=;J0qwQo|h_j(H`IXyRWFv!l+h4b0NgrAR)r z>`s$O0Z5o@`*{O1Irc=Bxm>+L;i9-(w(SZo;6^5#otu4bb>a8yESeI*-yDrz1s;00 zJNnf&Eb#aUzram;!EYO#A9Y`^Tv&fWJk{$5@H=gI(OW&E#eh)x2n=a&OR7iww0FN7 zi3}Q9-j1nm#@)Mqr`!qiRBb}1kF9r|Pl4~xeT-K>Ni zx2tWvK^Vpb$%r0xllsnh|lkCKC05Eo2l9d)&y%w+jrRPJo zr@tik;MLKWwOqZIb1SA_?UXLs!V>*+k3bH)K}8$vaSXcGlKAD>m9|~NU@TE<(9@`D zIl%@Fj^PqR8#!ZHD!O%<1j162K0HrGdYi@jaIB$IzZ=W$S+J`c;=+8HJidf=XQ{}- z6rW4ODnSO;pd$yP-)WyZW|}kFYnSF(c42$A$EuU77avy^(Pf#jM2z<)3NpsmuKH;; zb>jUdXQplp5&S*42Uca-s9CLM{&{zsW=vJbJs~k>e{f^SDoXw;rL@Ez=G!{(%OZBn zuY~#WJEhH+y$f)? zYxzP3VB)3>SKQgUir$6PfFaWF`KNv+r>f)cMz)weEnyu8v%+MX(^o>=q6L58zMKb) zuPB1}J>}e%RPnH6oc}_I-Mi+btVruVR_lo$F9hS=eJyJjyS=TlqFv&gc6s;xpRRv58syXv3vS!^{lJ=%xQCl2N-?qp!n;!BJxMyk*`@h)XdA}4 z8D2^r6=u$6e+e%?D4#D|dk$xectBw(a!&EX*N%R<^IKEusgv7e~| zesJYc^Oi&J8twDJW`iI5-LJ1eBh{DJt+stHD=jEa4c8J(HYrDWe9zG4G5VkqHPa~6 z#68>I?n61l(&}&+fmG7<)HqY5hUXFb){;d~F$O zwS8fx*T&a#hdc?K_r^C8LHgEb{I&?2lH()aw_kh)N`5a-;*TWFQtfy5`518zVlH!t zy1tL-WerPD%QZ8W<7`&`4!@w^=;IP}01^qyviiwsg*{1!fiWGuFL@)_o?bhapc+AQJrjX!I@Mcrf4*xT1hS)pMI`HP?j& zPw#*B#BA&AQq%t+-fQ}Q7Gu*yw9mRl8+w*r>_|}v*B`mVc=0mW4$KJ9Vz&N&HvVrC z{a=E*{@-Hb4*_2MGJm}j)k#R~ghfO{zI^vX1svj!nR=?V9`RTBR*vBx^!`~h2_%*~ zYoRz_MfH-oaYrCCKLEv&=A?NFL^V}&+=)M&#m%A0%1#x~SfgjXRTKEio8Q(Tyq zM0}c)A~|Go!ePhAlMT{yWltyvmJwh9p$1T^>RCA`FV1=qx7Jx$5jf3metcjj`Kx;^ z@J`2(T#g8354=3oJTKIG!&hw}D7I$kPtx*oHc}BHa{=&9pmYQuv*|rziyf;OWhL&s zktnA~{1xog#>t$=W$v^;doT5Bys5bJ#{G3qE%#dgEz9PwbNS2=Oc;olY^+bW)=Efv z%F*}SJ_h70jqHNOM0lb2XJCd#7i(nC>AWqDiS%25nY_au!CJF|4FT)jy|;pjuKU@+ zJo$*;EBHZrUVT!d(kTy7HbdF)5Q~I&C4ofGYAyoKB9-$K?n8v56rVSt^Zp}CMAvE}_XrB;D(a4a zR4>uC<32>E`%-J+m^UJdJ6*}zcfO-HXD)Y58|2l40JpkBG?9_iYabA{xii+ZBKp?0 z%`_oA>0j?0xLlCoQ^|>1#0eG56K5lo`@#p_Po2Hhk)i8S3UdcRt{K~6OcfIE zD~FyF_=JEdVap^pO+Ai;BOW2gsF5_tX+d}a2eF;bhGovSl2j{%?3*9zbb^p8w^M~~ zFgx)=3>!fPYw-~{`j+28!*>MDKk?1+p*r;>5R{dGh0HKJt;&lzy{Kz^P>vW*tb3~X zt2vzRhWFMzkKb9o!(;K*LfC??XQzk}PaP~yu|v?Na`J(ymbu!^I|8Eiv%wl*7%9Q4 zvz6fm84X@ri<-n<$i_YOEECbtk;@S66RfT^3U6&IznM-8Dl_81U3A*NPQ&(W(wla9 zlr$bfFwZ+3%CFb5IB5{7GoW!NpUNT5QJ2Ye{m_2btKdeEahjt^;@1}Zk6Hk2ju8jX ziHLj4lht39pW74ZZLKHxuylAgWmRU!<_5`3NBB;}FNJwE%TD_=3age+OVh`{cPdP` zs_ko;QT5}9_v9a1f*Me_RBE(lIw(P^XtK{1C?VNAxP#vD10y0E1YvVnr&h=IUte zK)c)IRjfwlJ}-L=*QXm+YM;v$B~Y>H*B3lmt`<`=R-i|lB}vE6bky*B3uCFJHT~iB zJ;Bv&HzSf}VULf?ay2J448#58op&7beuRqG?7#^JL2-=se2=?-w@%FN5wC(D6cDcl$$lf^&g#B-ZcIizIlq-; zW6qLqZ?J79!=d5-qFbTVqxN5~11-~y*$z|x?z=+M%E>#YbDL=4=Ba*KN#)5yEpHaC zV??vlv`tLZ(1dagRd=~BPU~jSR=(ZN(N21Pqh0r7Q5YNWg!iI;4`=jh--IQ=j5Y%; zzwLaVF!ww0%C83SpTnLOZy?PS4_l=%I)yYHNt=TURDMp^jL&TW)9$=%9k z0KNWAIm&Q8fGS( zSZWm*^yoQkhI^+aNNK9}-TT1E-DbXUI`%7X@_KhDNzAz7o;vEDLl~%5ih?^M;mTe7*sgrNXIAHBQ z5%6a*GO_!p$Oc~N9~r~fCw#y~usN>eE4fVJ))gm@)tjAM%cI|~IEL{?yb!l|k=6xh zN#GIhA+vVfr03M!O|=^xb3fKJG~*T|Me$ruUf{Zdc~k<|S} z@g8hwv;C@~APCd?h<$g7$~l5rVQD=Yddk&2cC5G(B_uDQxH2rp(N%27e{YI-!;WI+ z8Sl3z;7<~ofmL1~RS0jxv$4%C_GP*0IpSLnlU!dp9dGRFR!$HnU6I%GeDorZt7rFa zZOWa|0Hia>a2t}#qxrvCp}K#6q770?KB<1Qv7Pg_9@+Z!j!UTfi$lcIDSxn3-yxZU zN9=3)Eoe2@K;7wkF`=O&CeeQRz9IIbO%1XhuaK9zrn#r!4-kI5VY2Ita+n8C)`vTU zDf{mCBl?>wzOP`HX2gDECrxo;LJ z$k4|gif0&aBl84yqoKJXQ4xv70D?z@*H>%mdUPo>@1Sv}<_&S&!m)M*F@$jIv;5`W zo)okN>b=yyA2qdFF0U-qQc2HcOZ`~!US<>?#J)P5J5*Fz^&ma|Hids%b(a~Y6k)R| ztuEdA`>lP)dg9v9py_+%iV9jV`^9BZTMw3QZ?J`E5A!e0`B$`mrgW6{c)hHFX2S(? z7dT+-Eq^qYl&{Z6YvPaRT=i!i1-?0`r&l83Vw{78uM_zjMfi;(laap8cMcw**Zz}yOE)AOSXb;aAArC^xo1I9a{1{Gh)DouZ0YG za?bBzUaS0im4WR5a#W}^pep!Vrre{6B%IIPtkSRdmzY-oam9QLh==_nt>I8da^P!tkX;XVhxazvv zDBVm|5?>X#bcA8Edx-@c2! zVW<0gm>lV!?cdAAo^fw_A1i*!o0g%C*HZl_L%XbdrhK}p>Y{ZP{aY&m2pI2QGxiCB?t|DK-^-2r zjQ0V%6w?c|ADorxfBmKXgEle+ycz;|4Z-ly;DC99N^%T}Y7mw#oIanRh-_}Cl#KH` zp|4;0Pmo#NZC9{Kb;4Jlo!A;G@$p=Z$O(NNlk`_lkpQ+VCUU%_yYP=~U648RMQW8k zM22BcAB>aYJz}wK7#yaqDrj9>1s+fwF;wCso8Hf>p0&U{8|=XvxWaFIN2g_2%-1#CATH zX~JJGoI7KA{8`akWs@8Bfs?HgV_wOT+@~moiw>}RMs11Wd_kokJ_hnT4gd+|bK=@^ zUC2{#B*au=?{6<+c4FxMFgkrb(ZY8EKcn&GQWuil?b%4om~ez$ST`v96CJCP!-H5E z3az2Mgc$jnRr@XLmEJdSe;4-Mt;Ynd*o)D#uoj7H7cK-9D_F%y4<$#eJi4Qz6zHWG zp~tMekT53Y0Obr&PClZXFVtKmwJ#UnGvl55;IHzZd0P3;U`&G&-!;35`QMW@g!ijz zR@FxyfVS_O+TB4X%VASrB&b!6FPtvf;VGQt)^02y%;P8@0t# zTWvIb|2!2@(rb*PH7QYhE;EW^*^Haj$-qt;F{f(X48Jh#jTUzYM)vu4t9>TxMH-KV zm|M+dW`OB|JE%h)9kQjO&&fQwl$1oAu2>UPU#V1=u{`(;c6Dz<0TKxnKC8WrvS{S} zhtruojH|P(UCVYTPv8l~Z z#ZjB**FKz&-+D}sKX61^g!XaY(d~UQ*&UC9({8lcX z#beF{uW33xoBb^d0T)dRYvsRV!tKqDnS@C`|NNC#C#?KbOn|!+3-1K7w3hzS>V5LoHxT`x6P!VKp9cXks{c#iD+LdsIe?_ z!qL?yW+wsN)gV(@wEUegE7Pp#q4;0td9d&A3^6zkWQgq?+vFhYL=Nl(a2a{c3*Z99 zI5o?6?*1y93<=LlgI=i%AHZW~8ncqDqEmvJL+HhMSz?K!TGJMVcL Eq~_7i)rtU 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

nGtrRhT+4r(htzqndPk?t=>#M<^-(jW3 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 literal 0 HcmV?d00001 diff --git a/docs/faq/basics/images/scopes.png b/docs/faq/basics/images/scopes.png new file mode 100644 index 0000000000000000000000000000000000000000..8511908d525730c97b52729a94ed111880bfb732 GIT binary patch literal 28566 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 - .AddService(client) + .AddSingleton() + .AddSingleton(client) // We are missing DatabaseService! .BuildServiceProvider(); } @@ -25,5 +25,8 @@ public class CommandHandler // registered in this instance of _services. await _commands.AddModulesAsync(Assembly.GetEntryAssembly(), _services); // ... + + // The same approach applies to the interaction service. + // Make sure to resolve these issues! } -} \ No newline at end of file +} diff --git a/docs/faq/commands/interaction.md b/docs/faq/int_framework/framework.md similarity index 52% rename from docs/faq/commands/interaction.md rename to docs/faq/int_framework/framework.md index db61bc3f5..793b44d3e 100644 --- a/docs/faq/commands/interaction.md +++ b/docs/faq/int_framework/framework.md @@ -1,34 +1,54 @@ --- -uid: FAQ.Commands.Interactions -title: Interaction service +uid: FAQ.Interactions.Framework +title: Interaction Framework --- -# Interaction commands in services +# The Interaction Framework -A chapter talking about the interaction service framework. -For questions about interactions in general, refer to the [Interactions FAQ] +Common misconceptions and questions about the Interaction Framework. + +## How can I restrict some of my commands so only specific users can execute them? + +Based on how you want to implement the restrictions, you can use the +built-in `RequireUserPermission` precondition, which allows you to +restrict the command based on the user's current permissions in the +guild or channel (*e.g., `GuildPermission.Administrator`, +`ChannelPermission.ManageMessages`*). + +[RequireUserPermission]: xref:Discord.Commands.RequireUserPermissionAttribute + +> [!NOTE] +> There are many more preconditions to use, including being able to make some yourself. +> Examples on self-made preconditions can be found +> [here](https://github.com/discord-net/Discord.Net/blob/dev/samples/InteractionFramework/Attributes/RequireOwnerAttribute.cs) + +## Why do preconditions not hide my commands? + +In the current permission design by Discord, +it is not very straight forward to limit vision of slash/context commands to users. +If you want to hide commands, you should take a look at the commands' `DefaultPermissions` parameter. ## Module dependencies aren't getting populated by Property Injection? Make sure the properties are publicly accessible and publicly settable. -## How do I use this * interaction specific method/property? - -If your interaction context holds a down-casted version of the interaction object, you need to up-cast it. -Ideally, use pattern matching to make sure its the type of interaction you are expecting it to be. +[!code-csharp[Property Injection](samples/propertyinjection.cs)] ## `InteractionService.ExecuteAsync()` always returns a successful result, how do i access the failed command execution results? -If you are using `RunMode.Async` you need to setup your post-execution pipeline around `CommandExecuted` events. +If you are using `RunMode.Async` you need to setup your post-execution pipeline around +`..Executed` events exposed by the Interaction Service. ## How do I check if the executing user has * permission? Refer to the [documentation about preconditions] +[documentation about preconditions]: xref:Guides.ChatCommands.Preconditions + ## How do I send the HTTP Response from inside the command modules. Set the `RestResponseCallback` property of [InteractionServiceConfig] with a delegate for handling HTTP Responses and use -`RestInteractionModuleBase` to create your command modules. `RespondAsync()` and `DeferAsync()` methods of this module base will use the +`RestInteractionModuleBase` to create your command modules. `RespondWithModalAsync()`, `RespondAsync()` and `DeferAsync()` methods of this module base will use the `RestResponseCallback` to create interaction responses. ## Is there a cleaner way of creating parameter choices other than using `[Choice]`? @@ -49,4 +69,3 @@ It compares the _target base type_ key of the [TypeConverter]: xref:Discord.Interactions.TypeConverter [Interactions FAQ]: xref: FAQ.Basics.Interactions [InteractionServiceConfig]: xref:Discord.Interactions.InteractionServiceConfig -[documentation about preconditions]: xref: Guides.ChatCommands.Preconditions diff --git a/docs/faq/basics/interactions.md b/docs/faq/int_framework/general.md similarity index 52% rename from docs/faq/basics/interactions.md rename to docs/faq/int_framework/general.md index 33b89ac2d..af574edb6 100644 --- a/docs/faq/basics/interactions.md +++ b/docs/faq/int_framework/general.md @@ -1,11 +1,13 @@ --- -uid: FAQ.Basics.InteractionBasics -title: Basics of interactions, common practice +uid: FAQ.Interactions.General +title: Interactions --- -# Interactions basics, where to get started +# Interaction basics -This section answers basic questions and common mistakes in handling application commands, and responding to them. +This chapter mostly refers to interactions in general, +and will include questions that are common among users of the Interaction Framework +as well as users that register and handle commands manually. ## What's the difference between RespondAsync, DeferAsync and FollowupAsync? @@ -24,33 +26,20 @@ DeferAsync will not send out a response, RespondAsync will. ## Im getting System.TimeoutException: 'Cannot respond to an interaction after 3 seconds!' -This happens because your computers clock is out of sync or your trying to respond after 3 seconds. If your clock is out of sync and you cant fix it, you can set the `UseInteractionSnowflakeDate` to false in the config. +This happens because your computer's clock is out of sync or you're trying to respond after 3 seconds. +If your clock is out of sync and you can't fix it, you can set the `UseInteractionSnowflakeDate` to false in the [DiscordSocketConfig]. -## Bad form Exception when I try to create my commands, why do I get this? +[!code-csharp[Interaction Sync](samples/interactionsyncing.cs)] -Bad form exceptions are thrown if the slash, user or message command builder has invalid values. -The following options could resolve your error. +[DiscordClientConfig]: xref:Discord.WebSocket.DiscordSocketConfig -#### Is your command name lowercase? +## How do I use this * interaction specific method/property? -If your command name is not lowercase, it is not seen as a valid command entry. -`Avatar` is invalid; `avatar` is valid. - -#### Are your values below or above the required amount? (This also applies to message components) - -Discord expects all values to be below maximum allowed. -Going over this maximum amount of characters causes an exception. +If your interaction context holds a down-casted version of the interaction object, you need to up-cast it. +Ideally, use pattern matching to make sure its the type of interaction you are expecting it to be. > [!NOTE] -> All maximum and minimum value requirements can be found in the [Discord Developer Docs]. -> For components, structure documentation is found [here]. - -[Discord Developer Docs]: https://discord.com/developers/docs/interactions/application-commands#application-commands -[here]: https://discord.com/developers/docs/interactions/message-components#message-components - -#### Is your subcommand branching correct? - -Branching structure is covered properly here: xref:Guides.SlashCommands.SubCommand +> Further documentation on pattern matching can be found [here](xref:Guides.Entities.Casting). ## My interaction commands are not showing up? @@ -65,16 +54,6 @@ Did you register a guild command (should be instant), or waited more than an hou - Do you have the application commands scope checked when adding your bot to guilds? -![Scope check](images/scope.png) - -## There are many options for creating commands, which do I use? - -[!code-csharp[Register examples](samples/registerint.cs)] - -> [!NOTE] -> You can use bulkoverwrite even if there are no commands in guild, nor globally. -> The bulkoverwrite method disposes the old set of commands and replaces it with the new. - ## Do I need to create commands on startup? If you are registering your commands for the first time, it is required to create them once. diff --git a/docs/faq/basics/images/scope.png b/docs/faq/int_framework/images/scope.png similarity index 100% rename from docs/faq/basics/images/scope.png rename to docs/faq/int_framework/images/scope.png diff --git a/docs/faq/int_framework/manual.md b/docs/faq/int_framework/manual.md new file mode 100644 index 000000000..7ce0984a5 --- /dev/null +++ b/docs/faq/int_framework/manual.md @@ -0,0 +1,45 @@ +--- +uid: FAQ.Interactions.Manual +title: Manual handling +--- + +# Manually handing interactions. + +This section talks about the manual building and responding to interactions. +If you are using the interaction framework (highly recommended) this section does not apply to you. + +## Bad form Exception when I try to create my commands, why do I get this? + +Bad form exceptions are thrown if the slash, user or message command builder has invalid values. +The following options could resolve your error. + +#### Is your command name lowercase? + +If your command name is not lowercase, it is not seen as a valid command entry. +`Avatar` is invalid; `avatar` is valid. + +#### Are your values below or above the required amount? (This also applies to message components) + +Discord expects all values to be below maximum allowed. +Going over this maximum amount of characters causes an exception. + +> [!NOTE] +> All maximum and minimum value requirements can be found in the [Discord Developer Docs]. +> For components, structure documentation is found [here]. + +[Discord Developer Docs]: https://discord.com/developers/docs/interactions/application-commands#application-commands +[here]: https://discord.com/developers/docs/interactions/message-components#message-components + +#### Is your subcommand branching correct? + +Branching structure is covered properly here: xref:Guides.SlashCommands.SubCommand + +![Scope check](images/scope.png) + +## There are many options for creating commands, which do I use? + +[!code-csharp[Register examples](samples/registerint.cs)] + +> [!NOTE] +> You can use bulkoverwrite even if there are no commands in guild, nor globally. +> The bulkoverwrite method disposes the old set of commands and replaces it with the new. diff --git a/docs/faq/int_framework/samples/interactionsyncing.cs b/docs/faq/int_framework/samples/interactionsyncing.cs new file mode 100644 index 000000000..64066194a --- /dev/null +++ b/docs/faq/int_framework/samples/interactionsyncing.cs @@ -0,0 +1,6 @@ +DiscordSocketConfig config = new() +{ + UseInteractionSnowflakeDate = false +}; + +DiscordSocketclient client = new(config); diff --git a/docs/faq/int_framework/samples/propertyinjection.cs b/docs/faq/int_framework/samples/propertyinjection.cs new file mode 100644 index 000000000..fcacd52d1 --- /dev/null +++ b/docs/faq/int_framework/samples/propertyinjection.cs @@ -0,0 +1,8 @@ +public class MyModule +{ + // Intended. + public InteractionService Service { get; set; } + + // Will not work. A private setter cannot be accessed by the serviceprovider. + private InteractionService Service { get; private set; } +} diff --git a/docs/faq/basics/samples/registerint.cs b/docs/faq/int_framework/samples/registerint.cs similarity index 100% rename from docs/faq/basics/samples/registerint.cs rename to docs/faq/int_framework/samples/registerint.cs diff --git a/docs/faq/misc/legacy.md b/docs/faq/misc/legacy.md index fbfb41ac2..0b0b51159 100644 --- a/docs/faq/misc/legacy.md +++ b/docs/faq/misc/legacy.md @@ -8,15 +8,32 @@ title: Questions about Legacy Versions This section refers to legacy library-related questions that do not apply to the latest or recent version of the Discord.Net library. +## Migrating your commands to application commands. + +The new interaction service was designed to act like the previous service for text-based commands. +Your pre-existing code will continue to work, but you will need to migrate your modules and response functions to use the new +interaction service methods. Documentation on this can be found in the [Guides](xref:Guides.IntFw.Intro). + +## Gateway event parameters changed, why? + +With 3.0, a higher focus on [Cacheable]'s was introduced. +[Cacheable]'s get an entity from cache, rather than making an API call to retrieve it's data. +The entity can be retrieved from cache by calling `GetOrDownloadAsync()` on the [Cacheable] type. + +> [!NOTE] +> GetOrDownloadAsync will download the entity if its not available directly from the cache. + +[Cacheable]: xref:Discord.Cacheable + ## X, Y, Z does not work! It doesn't return a valid value anymore. If you are currently using an older version of the stable branch, -please upgrade to the latest pre-release version to ensure maximum +please upgrade to the latest release version to ensure maximum compatibility. Several features may be broken in older versions and will likely not be fixed in the version branch due to their breaking nature. -Visit the repo's [release tag] to see the latest public pre-release. +Visit the repo's [release tag] to see the latest public release. [release tag]: https://github.com/discord-net/Discord.Net/releases diff --git a/docs/faq/commands/general.md b/docs/faq/text_commands/general.md similarity index 89% rename from docs/faq/commands/general.md rename to docs/faq/text_commands/general.md index cff078746..202ceb299 100644 --- a/docs/faq/commands/general.md +++ b/docs/faq/text_commands/general.md @@ -1,6 +1,6 @@ --- -uid: FAQ.Commands.General -title: General Questions about chat Commands +uid: FAQ.TextCommands.General +title: General Questions about Text Commands --- # Chat Command-related Questions @@ -10,21 +10,16 @@ answered regarding general command usage when using @Discord.Commands. ## How can I restrict some of my commands so only specific users can execute them? -Based on how you want to implement the restrictions, you can use the -built-in [RequireUserPermission] precondition, which allows you to +You can use the built-in `RequireUserPermission` precondition, which allows you to restrict the command based on the user's current permissions in the guild or channel (*e.g., `GuildPermission.Administrator`, `ChannelPermission.ManageMessages`*). -If, however, you wish to restrict the commands based on the user's -role, you can either create your custom precondition or use -Joe4evr's [Preconditions Addons] that provides a few custom -preconditions that aren't provided in the stock library. -Its source can also be used as an example for creating your -custom preconditions. +> [!NOTE] +> There are many more preconditions to use, including being able to make some yourself. +> Precondition documentation is covered [here](xref:Guides.TextCommands.Preconditions) [RequireUserPermission]: xref:Discord.Commands.RequireUserPermissionAttribute -[Preconditions Addons]: https://github.com/Joe4evr/Discord.Addons/tree/master/src/Discord.Addons.Preconditions ## Why am I getting an error about `Assembly.GetEntryAssembly`? diff --git a/docs/faq/commands/samples/Remainder.cs b/docs/faq/text_commands/samples/Remainder.cs similarity index 100% rename from docs/faq/commands/samples/Remainder.cs rename to docs/faq/text_commands/samples/Remainder.cs diff --git a/docs/faq/commands/samples/runmode-cmdattrib.cs b/docs/faq/text_commands/samples/runmode-cmdattrib.cs similarity index 100% rename from docs/faq/commands/samples/runmode-cmdattrib.cs rename to docs/faq/text_commands/samples/runmode-cmdattrib.cs diff --git a/docs/faq/commands/samples/runmode-cmdconfig.cs b/docs/faq/text_commands/samples/runmode-cmdconfig.cs similarity index 100% rename from docs/faq/commands/samples/runmode-cmdconfig.cs rename to docs/faq/text_commands/samples/runmode-cmdconfig.cs diff --git a/docs/faq/toc.yml b/docs/faq/toc.yml index 2f04dc98c..97e327aba 100644 --- a/docs/faq/toc.yml +++ b/docs/faq/toc.yml @@ -6,15 +6,19 @@ topicUid: FAQ.Basics.BasicOp - name: Client Basics topicUid: FAQ.Basics.ClientBasics - - name: Interactions - topicUid: FAQ.Basics.InteractionBasics -- name: Commands - items: - - name: String commands - topicUid: FAQ.Commands.General - - name: Interaction commands - topicUid: FAQ.Commands.Interactions - name: Dependency Injection - topicUid: FAQ.Commands.DI + topicUid: FAQ.Basics.DI +- name: Interactions + items: + - name: Starting out + topicUid: FAQ.Interactions.General + - name: Interaction Service/Framework + topicUid: FAQ.Interactions.Framework + - name: Manual handling + topicUid: FAQ.Interactions.Manual +- name: Text Commands + items: + - name: Text Command basics + topicUid: FAQ.TextCommands.General - name: Legacy or Upgrade topicUid: FAQ.Legacy From 3e52fab67b11a792288828f80c8e39d74a975954 Mon Sep 17 00:00:00 2001 From: Cenk Ergen <57065323+Cenngo@users.noreply.github.com> Date: Wed, 2 Mar 2022 23:25:06 +0300 Subject: [PATCH 18/35] Add return statement to precondition handling (#2062) --- src/Discord.Net.Commands/CommandService.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index fce67b9b2..d6dfc2fb7 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -557,6 +557,7 @@ namespace Discord.Commands if (matchResult.Pipeline is PreconditionResult preconditionResult) { await _commandExecutedEvent.InvokeAsync(matchResult.Match.Value.Command, context, preconditionResult).ConfigureAwait(false); + return preconditionResult; } return matchResult; From 8bcd3da9e4ee025b3f5dfd6cdb171587ca63d79a Mon Sep 17 00:00:00 2001 From: EpicOfficer <9379778+EpicOfficer@users.noreply.github.com> Date: Wed, 2 Mar 2022 20:27:29 +0000 Subject: [PATCH 19/35] Add self_video to VoiceState (#2137) * Add self_video to VoiceState * Update selfVideo flag --- src/Discord.Net.Core/Entities/Users/IVoiceState.cs | 7 +++++++ src/Discord.Net.Rest/API/Common/VoiceState.cs | 2 ++ src/Discord.Net.Rest/Entities/Users/RestGroupUser.cs | 2 ++ src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs | 2 ++ .../Entities/Users/RestWebhookUser.cs | 2 ++ .../Entities/Users/SocketGroupUser.cs | 2 ++ .../Entities/Users/SocketGuildUser.cs | 2 ++ .../Entities/Users/SocketThreadUser.cs | 4 ++++ .../Entities/Users/SocketVoiceState.cs | 11 ++++++++--- .../Entities/Users/SocketWebhookUser.cs | 2 ++ 10 files changed, 33 insertions(+), 3 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Users/IVoiceState.cs b/src/Discord.Net.Core/Entities/Users/IVoiceState.cs index c9a22761f..f346fc914 100644 --- a/src/Discord.Net.Core/Entities/Users/IVoiceState.cs +++ b/src/Discord.Net.Core/Entities/Users/IVoiceState.cs @@ -65,6 +65,13 @@ namespace Discord /// bool IsStreaming { get; } ///

+ /// Gets a value that indicates if the user is videoing in a voice channel. + /// + /// + /// true if the user has their camera turned on; otherwise false. + /// + bool IsVideoing { get; } + /// /// Gets the time on which the user requested to speak. /// DateTimeOffset? RequestToSpeakTimestamp { get; } diff --git a/src/Discord.Net.Rest/API/Common/VoiceState.cs b/src/Discord.Net.Rest/API/Common/VoiceState.cs index f7cd54a72..adfa7f20e 100644 --- a/src/Discord.Net.Rest/API/Common/VoiceState.cs +++ b/src/Discord.Net.Rest/API/Common/VoiceState.cs @@ -28,6 +28,8 @@ namespace Discord.API public bool Suppress { get; set; } [JsonProperty("self_stream")] public bool SelfStream { get; set; } + [JsonProperty("self_video")] + public bool SelfVideo { get; set; } [JsonProperty("request_to_speak_timestamp")] public Optional RequestToSpeakTimestamp { get; set; } } diff --git a/src/Discord.Net.Rest/Entities/Users/RestGroupUser.cs b/src/Discord.Net.Rest/Entities/Users/RestGroupUser.cs index 40e45b135..a8c1a9b0a 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestGroupUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestGroupUser.cs @@ -41,6 +41,8 @@ namespace Discord.Rest /// bool IVoiceState.IsStreaming => false; /// + bool IVoiceState.IsVideoing => false; + /// DateTimeOffset? IVoiceState.RequestToSpeakTimestamp => null; #endregion } diff --git a/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs b/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs index d6c7b5d7c..0a4a33099 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs @@ -223,6 +223,8 @@ namespace Discord.Rest /// bool IVoiceState.IsStreaming => false; /// + bool IVoiceState.IsVideoing => false; + /// DateTimeOffset? IVoiceState.RequestToSpeakTimestamp => null; #endregion } diff --git a/src/Discord.Net.Rest/Entities/Users/RestWebhookUser.cs b/src/Discord.Net.Rest/Entities/Users/RestWebhookUser.cs index d03800676..3fa88649a 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestWebhookUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestWebhookUser.cs @@ -131,6 +131,8 @@ namespace Discord.Rest /// bool IVoiceState.IsStreaming => false; /// + bool IVoiceState.IsVideoing => false; + /// DateTimeOffset? IVoiceState.RequestToSpeakTimestamp => null; #endregion } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGroupUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGroupUser.cs index d027bf0aa..a40ae59be 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGroupUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGroupUser.cs @@ -70,6 +70,8 @@ namespace Discord.WebSocket /// bool IVoiceState.IsStreaming => false; /// + bool IVoiceState.IsVideoing => false; + /// DateTimeOffset? IVoiceState.RequestToSpeakTimestamp => null; #endregion } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs index ac3a53f17..051687b78 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs @@ -65,6 +65,8 @@ namespace Discord.WebSocket /// public bool IsStreaming => VoiceState?.IsStreaming ?? false; /// + public bool IsVideoing => VoiceState?.IsVideoing ?? false; + /// public DateTimeOffset? RequestToSpeakTimestamp => VoiceState?.RequestToSpeakTimestamp ?? null; /// public bool? IsPending { get; private set; } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs index 08a1cbab4..025d34d0f 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketThreadUser.cs @@ -122,6 +122,10 @@ namespace Discord.WebSocket public bool IsStreaming => GuildUser.IsStreaming; + /// + public bool IsVideoing + => GuildUser.IsVideoing; + /// public DateTimeOffset? RequestToSpeakTimestamp => GuildUser.RequestToSpeakTimestamp; diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketVoiceState.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketVoiceState.cs index 816a839fc..6c5b867b5 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketVoiceState.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketVoiceState.cs @@ -13,7 +13,7 @@ namespace Discord.WebSocket /// /// Initializes a default with everything set to null or false. /// - public static readonly SocketVoiceState Default = new SocketVoiceState(null, null, null, false, false, false, false, false, false); + public static readonly SocketVoiceState Default = new SocketVoiceState(null, null, null, false, false, false, false, false, false, false); [Flags] private enum Flags : byte @@ -25,6 +25,7 @@ namespace Discord.WebSocket SelfMuted = 0x08, SelfDeafened = 0x10, SelfStream = 0x20, + SelfVideo = 0x40, } private readonly Flags _voiceStates; @@ -50,9 +51,11 @@ namespace Discord.WebSocket public bool IsSelfDeafened => (_voiceStates & Flags.SelfDeafened) != 0; /// public bool IsStreaming => (_voiceStates & Flags.SelfStream) != 0; + /// + public bool IsVideoing => (_voiceStates & Flags.SelfVideo) != 0; - internal SocketVoiceState(SocketVoiceChannel voiceChannel, DateTimeOffset? requestToSpeak, string sessionId, bool isSelfMuted, bool isSelfDeafened, bool isMuted, bool isDeafened, bool isSuppressed, bool isStream) + internal SocketVoiceState(SocketVoiceChannel voiceChannel, DateTimeOffset? requestToSpeak, string sessionId, bool isSelfMuted, bool isSelfDeafened, bool isMuted, bool isDeafened, bool isSuppressed, bool isStream, bool isVideo) { VoiceChannel = voiceChannel; VoiceSessionId = sessionId; @@ -71,11 +74,13 @@ namespace Discord.WebSocket voiceStates |= Flags.Suppressed; if (isStream) voiceStates |= Flags.SelfStream; + if (isVideo) + voiceStates |= Flags.SelfVideo; _voiceStates = voiceStates; } internal static SocketVoiceState Create(SocketVoiceChannel voiceChannel, Model model) { - return new SocketVoiceState(voiceChannel, model.RequestToSpeakTimestamp.IsSpecified ? model.RequestToSpeakTimestamp.Value : null, model.SessionId, model.SelfMute, model.SelfDeaf, model.Mute, model.Deaf, model.Suppress, model.SelfStream); + return new SocketVoiceState(voiceChannel, model.RequestToSpeakTimestamp.IsSpecified ? model.RequestToSpeakTimestamp.Value : null, model.SessionId, model.SelfMute, model.SelfDeaf, model.Mute, model.Deaf, model.Suppress, model.SelfStream, model.SelfVideo); } /// diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs index df5fe786d..2b2c259c5 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs @@ -164,6 +164,8 @@ namespace Discord.WebSocket /// bool IVoiceState.IsStreaming => false; /// + bool IVoiceState.IsVideoing => false; + /// DateTimeOffset? IVoiceState.RequestToSpeakTimestamp => null; #endregion } From 559473913537708c7c5985f0dc92b1395189ba52 Mon Sep 17 00:00:00 2001 From: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Wed, 2 Mar 2022 16:43:33 -0400 Subject: [PATCH 20/35] Clarify Users property on SocketGuildChannel (#2149) --- .../Entities/Channels/SocketGuildChannel.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs index d38a8975b..45eb28444 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs @@ -35,6 +35,10 @@ namespace Discord.WebSocket /// /// Gets a collection of users that are able to view the channel. /// + /// + /// If this channel is a voice channel, a collection of users who are currently connected to this channel + /// is returned. + /// /// /// A read-only collection of users that can access the channel (i.e. the users seen in the user list). /// From 202554fdde5ebcc38e36b0a1a55eb199cd6bfad9 Mon Sep 17 00:00:00 2001 From: Discord-NET-Robot <95661365+Discord-NET-Robot@users.noreply.github.com> Date: Wed, 2 Mar 2022 18:31:52 -0400 Subject: [PATCH 21/35] [Robot] Add missing json error (#2152) * Add 30046, 40004, 40060, 50068, 50086 Error codes * Update src/Discord.Net.Core/DiscordErrorCode.cs Co-authored-by: Discord.Net Robot Co-authored-by: Quin Lynch <49576606+quinchs@users.noreply.github.com> --- src/Discord.Net.Core/DiscordErrorCode.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Discord.Net.Core/DiscordErrorCode.cs b/src/Discord.Net.Core/DiscordErrorCode.cs index 03f8b19e9..3eb9637eb 100644 --- a/src/Discord.Net.Core/DiscordErrorCode.cs +++ b/src/Discord.Net.Core/DiscordErrorCode.cs @@ -96,9 +96,11 @@ namespace Discord #endregion #region General Request Errors (40XXX) + MaximumNumberOfEditsReached = 30046, TokenUnauthorized = 40001, InvalidVerification = 40002, OpeningDMTooFast = 40003, + SendMessagesHasBeenTemporarilyDisabled = 40004, RequestEntityTooLarge = 40005, FeatureDisabled = 40006, UserBanned = 40007, @@ -108,6 +110,7 @@ namespace Discord #endregion #region Action Preconditions/Checks (50XXX) + InteractionHasAlreadyBeenAcknowledged = 40060, MissingPermissions = 50001, InvalidAccountType = 50002, CannotExecuteForDM = 50003, @@ -141,12 +144,14 @@ namespace Discord InvalidFileUpload = 50046, CannotSelfRedeemGift = 50054, InvalidGuild = 50055, + InvalidMessageType = 50068, PaymentSourceRequiredForGift = 50070, CannotDeleteRequiredCommunityChannel = 50074, InvalidSticker = 50081, CannotExecuteOnArchivedThread = 50083, InvalidThreadNotificationSettings = 50084, BeforeValueEarlierThanThreadCreation = 50085, + CommunityServerChannelsMustBeTextChannels = 50086, ServerLocaleUnavailable = 50095, ServerRequiresMonetization = 50097, ServerRequiresBoosts = 50101, From 1dc473c7e41226d986ab7b12f35f88d501b8068e Mon Sep 17 00:00:00 2001 From: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Wed, 2 Mar 2022 19:22:08 -0400 Subject: [PATCH 22/35] Add Image property to Guild Scheduled Events (#2151) * Add Image property to create and modify events * Add CDN routes to get cover image * Update banner names * Update CDN.cs * Update IGuildScheduledEvent.cs --- src/Discord.Net.Core/CDN.cs | 12 ++++++++++++ .../Guilds/GuildScheduledEventsProperties.cs | 5 +++++ src/Discord.Net.Core/Entities/Guilds/IGuild.cs | 2 ++ .../Entities/Guilds/IGuildScheduledEvent.cs | 13 +++++++++++++ .../API/Common/GuildScheduledEvent.cs | 2 ++ .../API/Rest/CreateGuildScheduledEventParams.cs | 2 ++ .../API/Rest/ModifyGuildScheduledEventParams.cs | 2 ++ src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs | 12 ++++++++++-- src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs | 8 +++++--- .../Entities/Guilds/RestGuildEvent.cs | 8 ++++++++ .../Entities/Guilds/SocketGuild.cs | 8 +++++--- .../Entities/Guilds/SocketGuildEvent.cs | 8 ++++++++ 12 files changed, 74 insertions(+), 8 deletions(-) diff --git a/src/Discord.Net.Core/CDN.cs b/src/Discord.Net.Core/CDN.cs index d6535a4f1..1a8795101 100644 --- a/src/Discord.Net.Core/CDN.cs +++ b/src/Discord.Net.Core/CDN.cs @@ -208,6 +208,18 @@ namespace Discord public static string GetStickerUrl(ulong stickerId, StickerFormatType format = StickerFormatType.Png) => $"{DiscordConfig.CDNUrl}stickers/{stickerId}.{FormatToExtension(format)}"; + /// + /// Returns an events cover image url. + /// + /// The guild id that the event is in. + /// The id of the event. + /// The id of the cover image asset. + /// The format of the image. + /// The size of the image. + /// + public static string GetEventCoverImageUrl(ulong guildId, ulong eventId, string assetId, ImageFormat format = ImageFormat.Auto, ushort size = 1024) + => $"{DiscordConfig.CDNUrl}guild-events/{guildId}/{eventId}/{assetId}.{FormatToExtension(format, assetId)}?size={size}"; + private static string FormatToExtension(StickerFormatType format) { return format switch diff --git a/src/Discord.Net.Core/Entities/Guilds/GuildScheduledEventsProperties.cs b/src/Discord.Net.Core/Entities/Guilds/GuildScheduledEventsProperties.cs index a3fd729e5..d3be8b784 100644 --- a/src/Discord.Net.Core/Entities/Guilds/GuildScheduledEventsProperties.cs +++ b/src/Discord.Net.Core/Entities/Guilds/GuildScheduledEventsProperties.cs @@ -54,5 +54,10 @@ namespace Discord /// Gets or sets the status of the event. /// public Optional Status { get; set; } + + /// + /// Gets or sets the banner image of the event. + /// + public Optional CoverImage { get; set; } } } diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs index ae1b2d67d..3111ff495 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs @@ -1105,6 +1105,7 @@ namespace Discord /// /// A collection of speakers for the event. /// The location of the event; links are supported + /// The optional banner image for the event. /// The options to be used when sending the request. /// /// A task that represents the asynchronous create operation. @@ -1118,6 +1119,7 @@ namespace Discord DateTimeOffset? endTime = null, ulong? channelId = null, string location = null, + Image? coverImage = null, RequestOptions options = null); /// diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuildScheduledEvent.cs b/src/Discord.Net.Core/Entities/Guilds/IGuildScheduledEvent.cs index e50f4cc2b..4b2fa3bee 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuildScheduledEvent.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuildScheduledEvent.cs @@ -39,6 +39,11 @@ namespace Discord /// string Description { get; } + /// + /// Gets the banner asset id of the event. + /// + string CoverImageId { get; } + /// /// Gets the start time of the event. /// @@ -80,6 +85,14 @@ namespace Discord /// int? UserCount { get; } + /// + /// Gets this events banner image url. + /// + /// The format to return. + /// The size of the image to return in. This can be any power of two between 16 and 2048. + /// The cover images url. + string GetCoverImageUrl(ImageFormat format = ImageFormat.Auto, ushort size = 1024); + /// /// Starts the event. /// diff --git a/src/Discord.Net.Rest/API/Common/GuildScheduledEvent.cs b/src/Discord.Net.Rest/API/Common/GuildScheduledEvent.cs index 338c24dc9..94c53e779 100644 --- a/src/Discord.Net.Rest/API/Common/GuildScheduledEvent.cs +++ b/src/Discord.Net.Rest/API/Common/GuildScheduledEvent.cs @@ -39,5 +39,7 @@ namespace Discord.API public Optional Creator { get; set; } [JsonProperty("user_count")] public Optional UserCount { get; set; } + [JsonProperty("image")] + public string Image { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Rest/CreateGuildScheduledEventParams.cs b/src/Discord.Net.Rest/API/Rest/CreateGuildScheduledEventParams.cs index a207d3374..2ccd06fe6 100644 --- a/src/Discord.Net.Rest/API/Rest/CreateGuildScheduledEventParams.cs +++ b/src/Discord.Net.Rest/API/Rest/CreateGuildScheduledEventParams.cs @@ -25,5 +25,7 @@ namespace Discord.API.Rest public Optional Description { get; set; } [JsonProperty("entity_type")] public GuildScheduledEventType Type { get; set; } + [JsonProperty("image")] + public Optional Image { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Rest/ModifyGuildScheduledEventParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyGuildScheduledEventParams.cs index 3d191a0b3..1179ddcbe 100644 --- a/src/Discord.Net.Rest/API/Rest/ModifyGuildScheduledEventParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyGuildScheduledEventParams.cs @@ -27,5 +27,7 @@ namespace Discord.API.Rest public Optional Type { get; set; } [JsonProperty("status")] public Optional Status { get; set; } + [JsonProperty("image")] + public Optional Image { get; set; } } } diff --git a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs index 874d3c2cd..25f474dcc 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs @@ -799,7 +799,12 @@ namespace Discord.Rest PrivacyLevel = args.PrivacyLevel, StartTime = args.StartTime, Status = args.Status, - Type = args.Type + Type = args.Type, + Image = args.CoverImage.IsSpecified + ? args.CoverImage.Value.HasValue + ? args.CoverImage.Value.Value.ToModel() + : null + : Optional.Unspecified }; if(args.Location.IsSpecified) @@ -839,6 +844,7 @@ namespace Discord.Rest DateTimeOffset? endTime = null, ulong? channelId = null, string location = null, + Image? bannerImage = null, RequestOptions options = null) { if(location != null) @@ -864,6 +870,7 @@ namespace Discord.Rest if (endTime != null && endTime <= startTime) throw new ArgumentOutOfRangeException(nameof(endTime), $"{nameof(endTime)} cannot be before the start time"); + var apiArgs = new CreateGuildScheduledEventParams() { ChannelId = channelId ?? Optional.Unspecified, @@ -872,7 +879,8 @@ namespace Discord.Rest Name = name, PrivacyLevel = privacyLevel, StartTime = startTime, - Type = type + Type = type, + Image = bannerImage.HasValue ? bannerImage.Value.ToModel() : Optional.Unspecified }; if(location != null) diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs index d90372636..2c37bb2da 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -1167,6 +1167,7 @@ namespace Discord.Rest /// /// A collection of speakers for the event. /// The location of the event; links are supported + /// The optional banner image for the event. /// The options to be used when sending the request. /// /// A task that represents the asynchronous create operation. @@ -1180,8 +1181,9 @@ namespace Discord.Rest DateTimeOffset? endTime = null, ulong? channelId = null, string location = null, + Image? coverImage = null, RequestOptions options = null) - => GuildHelper.CreateGuildEventAsync(Discord, this, name, privacyLevel, startTime, type, description, endTime, channelId, location, options); + => GuildHelper.CreateGuildEventAsync(Discord, this, name, privacyLevel, startTime, type, description, endTime, channelId, location, coverImage, options); #endregion @@ -1198,8 +1200,8 @@ namespace Discord.Rest IReadOnlyCollection IGuild.Stickers => Stickers; /// - async Task IGuild.CreateEventAsync(string name, DateTimeOffset startTime, GuildScheduledEventType type, GuildScheduledEventPrivacyLevel privacyLevel, string description, DateTimeOffset? endTime, ulong? channelId, string location, RequestOptions options) - => await CreateEventAsync(name, startTime, type, privacyLevel, description, endTime, channelId, location, options).ConfigureAwait(false); + async Task IGuild.CreateEventAsync(string name, DateTimeOffset startTime, GuildScheduledEventType type, GuildScheduledEventPrivacyLevel privacyLevel, string description, DateTimeOffset? endTime, ulong? channelId, string location, Image? coverImage, RequestOptions options) + => await CreateEventAsync(name, startTime, type, privacyLevel, description, endTime, channelId, location, coverImage, options).ConfigureAwait(false); /// async Task IGuild.GetEventAsync(ulong id, RequestOptions options) diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuildEvent.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuildEvent.cs index d3ec11fc6..0b02e60ba 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuildEvent.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuildEvent.cs @@ -28,6 +28,9 @@ namespace Discord.Rest /// public string Description { get; private set; } + /// + public string CoverImageId { get; private set; } + /// public DateTimeOffset StartTime { get; private set; } @@ -98,8 +101,13 @@ namespace Discord.Rest EntityId = model.EntityId; Location = model.EntityMetadata?.Location.GetValueOrDefault(); UserCount = model.UserCount.ToNullable(); + CoverImageId = model.Image; } + /// + public string GetCoverImageUrl(ImageFormat format = ImageFormat.Auto, ushort size = 1024) + => CDN.GetEventCoverImageUrl(Guild.Id, Id, CoverImageId, format, size); + /// public Task StartAsync(RequestOptions options = null) => ModifyAsync(x => x.Status = GuildScheduledEventStatus.Active); diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index 4a7d4fafb..b38dfcd74 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -1295,6 +1295,7 @@ namespace Discord.WebSocket /// /// A collection of speakers for the event. /// The location of the event; links are supported + /// The optional banner image for the event. /// The options to be used when sending the request. /// /// A task that represents the asynchronous create operation. @@ -1308,6 +1309,7 @@ namespace Discord.WebSocket DateTimeOffset? endTime = null, ulong? channelId = null, string location = null, + Image? coverImage = null, RequestOptions options = null) { // requirements taken from https://discord.com/developers/docs/resources/guild-scheduled-event#guild-scheduled-event-permissions-requirements @@ -1324,7 +1326,7 @@ namespace Discord.WebSocket break; } - return GuildHelper.CreateGuildEventAsync(Discord, this, name, privacyLevel, startTime, type, description, endTime, channelId, location, options); + return GuildHelper.CreateGuildEventAsync(Discord, this, name, privacyLevel, startTime, type, description, endTime, channelId, location, coverImage, options); } @@ -1803,8 +1805,8 @@ namespace Discord.WebSocket /// IReadOnlyCollection IGuild.Stickers => Stickers; /// - async Task IGuild.CreateEventAsync(string name, DateTimeOffset startTime, GuildScheduledEventType type, GuildScheduledEventPrivacyLevel privacyLevel, string description, DateTimeOffset? endTime, ulong? channelId, string location, RequestOptions options) - => await CreateEventAsync(name, startTime, type, privacyLevel, description, endTime, channelId, location, options).ConfigureAwait(false); + async Task IGuild.CreateEventAsync(string name, DateTimeOffset startTime, GuildScheduledEventType type, GuildScheduledEventPrivacyLevel privacyLevel, string description, DateTimeOffset? endTime, ulong? channelId, string location, Image? coverImage, RequestOptions options) + => await CreateEventAsync(name, startTime, type, privacyLevel, description, endTime, channelId, location, coverImage, options).ConfigureAwait(false); /// async Task IGuild.GetEventAsync(ulong id, RequestOptions options) => await GetEventAsync(id, options).ConfigureAwait(false); diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuildEvent.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuildEvent.cs index df619e4ca..a86aafadf 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuildEvent.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuildEvent.cs @@ -35,6 +35,9 @@ namespace Discord.WebSocket /// public string Description { get; private set; } + /// + public string CoverImageId { get; private set; } + /// public DateTimeOffset StartTime { get; private set; } @@ -109,8 +112,13 @@ namespace Discord.WebSocket StartTime = model.ScheduledStartTime; Status = model.Status; UserCount = model.UserCount.ToNullable(); + CoverImageId = model.Image; } + /// + public string GetCoverImageUrl(ImageFormat format = ImageFormat.Auto, ushort size = 1024) + => CDN.GetEventCoverImageUrl(Guild.Id, Id, CoverImageId, format, size); + /// public Task DeleteAsync(RequestOptions options = null) => GuildHelper.DeleteEventAsync(Discord, this, options); From 6bf5818e72fbdad1c2c2cf8cd588d6e7c5f21cb7 Mon Sep 17 00:00:00 2001 From: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Wed, 2 Mar 2022 19:22:29 -0400 Subject: [PATCH 23/35] Add IsInvitable and CreatedAt to threads (#2153) * Add IsInvitable and CreatedAt to threads * Update src/Discord.Net.Core/Entities/Channels/IThreadChannel.cs Co-Authored-By: Jared L <48422312+lhjt@users.noreply.github.com> Co-authored-by: Jared L <48422312+lhjt@users.noreply.github.com> --- .../Entities/Channels/IThreadChannel.cs | 17 +++++++++++++++++ .../API/Common/ThreadMetadata.cs | 6 ++++++ .../Entities/Channels/RestChannel.cs | 2 +- .../Entities/Channels/RestThreadChannel.cs | 16 +++++++++++++--- .../Entities/Channels/SocketChannel.cs | 2 +- .../Entities/Channels/SocketThreadChannel.cs | 17 +++++++++++++---- 6 files changed, 51 insertions(+), 9 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Channels/IThreadChannel.cs b/src/Discord.Net.Core/Entities/Channels/IThreadChannel.cs index 50e46efa6..f03edbbf9 100644 --- a/src/Discord.Net.Core/Entities/Channels/IThreadChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IThreadChannel.cs @@ -48,6 +48,23 @@ namespace Discord ///
int MessageCount { get; } + /// + /// Gets whether non-moderators can add other non-moderators to a thread. + /// + /// + /// This property is only available on private threads. + /// + bool? IsInvitable { get; } + + /// + /// Gets when the thread was created. + /// + /// + /// This property is only populated for threads created after 2022-01-09, hence the default date of this + /// property will be that date. + /// + new DateTimeOffset CreatedAt { get; } + /// /// Joins the current thread. /// diff --git a/src/Discord.Net.Rest/API/Common/ThreadMetadata.cs b/src/Discord.Net.Rest/API/Common/ThreadMetadata.cs index 39e9bd13e..15854fab4 100644 --- a/src/Discord.Net.Rest/API/Common/ThreadMetadata.cs +++ b/src/Discord.Net.Rest/API/Common/ThreadMetadata.cs @@ -16,5 +16,11 @@ namespace Discord.API [JsonProperty("locked")] public Optional Locked { get; set; } + + [JsonProperty("invitable")] + public Optional Invitable { get; set; } + + [JsonProperty("create_timestamp")] + public Optional CreatedAt { get; set; } } } diff --git a/src/Discord.Net.Rest/Entities/Channels/RestChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestChannel.cs index 83c6d8bfb..c730596c7 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestChannel.cs @@ -13,7 +13,7 @@ namespace Discord.Rest { #region RestChannel /// - public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); + public virtual DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); internal RestChannel(BaseDiscordClient discord, ulong id) : base(discord, id) diff --git a/src/Discord.Net.Rest/Entities/Channels/RestThreadChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestThreadChannel.cs index 63071b9a5..c763a6660 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestThreadChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestThreadChannel.cs @@ -34,17 +34,26 @@ namespace Discord.Rest /// public int MessageCount { get; private set; } + /// + public bool? IsInvitable { get; private set; } + + /// + public override DateTimeOffset CreatedAt { get; } + /// /// Gets the parent text channel id. /// public ulong ParentChannelId { get; private set; } - internal RestThreadChannel(BaseDiscordClient discord, IGuild guild, ulong id) - : base(discord, guild, id) { } + internal RestThreadChannel(BaseDiscordClient discord, IGuild guild, ulong id, DateTimeOffset? createdAt) + : base(discord, guild, id) + { + CreatedAt = createdAt ?? new DateTimeOffset(2022, 1, 9, 0, 0, 0, TimeSpan.Zero); + } internal new static RestThreadChannel Create(BaseDiscordClient discord, IGuild guild, Model model) { - var entity = new RestThreadChannel(discord, guild, model.Id); + var entity = new RestThreadChannel(discord, guild, model.Id, model.ThreadMetadata.GetValueOrDefault()?.CreatedAt.GetValueOrDefault()); entity.Update(model); return entity; } @@ -57,6 +66,7 @@ namespace Discord.Rest if (model.ThreadMetadata.IsSpecified) { + IsInvitable = model.ThreadMetadata.Value.Invitable.ToNullable(); IsArchived = model.ThreadMetadata.Value.Archived; AutoArchiveDuration = model.ThreadMetadata.Value.AutoArchiveDuration; ArchiveTimestamp = model.ThreadMetadata.Value.ArchiveTimestamp; diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketChannel.cs index 758ee9271..c30b3d254 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketChannel.cs @@ -17,7 +17,7 @@ namespace Discord.WebSocket /// /// Gets when the channel is created. /// - public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); + public virtual DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); /// /// Gets a collection of users from the WebSocket cache. /// diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs index 7fcafc14a..c26a23afd 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketThreadChannel.cs @@ -44,7 +44,7 @@ namespace Discord.WebSocket /// /// Gets the parent channel this thread resides in. /// - public SocketTextChannel ParentChannel { get; private set; } + public SocketGuildChannel ParentChannel { get; private set; } /// public int MessageCount { get; private set; } @@ -64,6 +64,12 @@ namespace Discord.WebSocket /// public bool IsLocked { get; private set; } + /// + public bool? IsInvitable { get; private set; } + + /// + public override DateTimeOffset CreatedAt { get; } + /// /// Gets a collection of cached users within this thread. /// @@ -78,17 +84,19 @@ namespace Discord.WebSocket private readonly object _downloadLock = new object(); - internal SocketThreadChannel(DiscordSocketClient discord, SocketGuild guild, ulong id, SocketTextChannel parent) + internal SocketThreadChannel(DiscordSocketClient discord, SocketGuild guild, ulong id, SocketGuildChannel parent, + DateTimeOffset? createdAt) : base(discord, id, guild) { ParentChannel = parent; _members = new ConcurrentDictionary(); + CreatedAt = createdAt ?? new DateTimeOffset(2022, 1, 9, 0, 0, 0, TimeSpan.Zero); } internal new static SocketThreadChannel Create(SocketGuild guild, ClientState state, Model model) { - var parent = (SocketTextChannel)guild.GetChannel(model.CategoryId.Value); - var entity = new SocketThreadChannel(guild.Discord, guild, model.Id, parent); + var parent = guild.GetChannel(model.CategoryId.Value); + var entity = new SocketThreadChannel(guild.Discord, guild, model.Id, parent, model.ThreadMetadata.GetValueOrDefault()?.CreatedAt.ToNullable()); entity.Update(state, model); return entity; } @@ -103,6 +111,7 @@ namespace Discord.WebSocket if (model.ThreadMetadata.IsSpecified) { + IsInvitable = model.ThreadMetadata.Value.Invitable.ToNullable(); IsArchived = model.ThreadMetadata.Value.Archived; ArchiveTimestamp = model.ThreadMetadata.Value.ArchiveTimestamp; AutoArchiveDuration = model.ThreadMetadata.Value.AutoArchiveDuration; From b3370c33e2eb1ccfe5011989aff472b0f9a84eb7 Mon Sep 17 00:00:00 2001 From: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Wed, 2 Mar 2022 19:22:59 -0400 Subject: [PATCH 24/35] Fix usage of CacheMode.AllowDownload in channels (#2154) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: ✨ <25006819+sabihoshi@users.noreply.github.com> Co-authored-by: ✨ <25006819+sabihoshi@users.noreply.github.com> --- .../Entities/Channels/IChannel.cs | 2 +- .../Entities/Channels/RestGuildChannel.cs | 2 +- .../Entities/Channels/RestTextChannel.cs | 9 ++--- src/Discord.Net.WebSocket/BaseSocketClient.cs | 17 ++++++--- .../DiscordShardedClient.cs | 11 +++++- .../DiscordSocketClient.cs | 15 ++++++-- .../Channels/SocketCategoryChannel.cs | 38 +++++++++++++++---- .../Entities/Channels/SocketGroupChannel.cs | 2 +- .../Entities/Channels/SocketGuildChannel.cs | 4 +- .../Entities/Channels/SocketTextChannel.cs | 19 ++++++++-- .../Entities/Guilds/SocketGuild.cs | 13 +++++-- 11 files changed, 97 insertions(+), 35 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Channels/IChannel.cs b/src/Discord.Net.Core/Entities/Channels/IChannel.cs index e2df86f2a..6d58486f8 100644 --- a/src/Discord.Net.Core/Entities/Channels/IChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IChannel.cs @@ -21,7 +21,7 @@ namespace Discord /// /// /// - /// The returned collection is an asynchronous enumerable object; one must call + /// The returned collection is an asynchronous enumerable object; one must call /// to access the individual messages as a /// collection. /// diff --git a/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs index bc9d4110a..fa2362854 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs @@ -227,7 +227,7 @@ namespace Discord.Rest /// IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options) - => AsyncEnumerable.Empty>(); //Overridden //Overridden in Text/Voice + => AsyncEnumerable.Empty>(); //Overridden in Text/Voice /// Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(null); //Overridden in Text/Voice diff --git a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs index f1bdee65c..198ff22ac 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs @@ -261,7 +261,7 @@ namespace Discord.Rest /// The duration on which this thread archives after. /// /// Note: Options and - /// are only available for guilds that are boosted. You can check in the to see if the + /// are only available for guilds that are boosted. You can check in the to see if the /// guild has the THREE_DAY_THREAD_ARCHIVE and SEVEN_DAY_THREAD_ARCHIVE. /// /// @@ -364,10 +364,9 @@ namespace Discord.Rest /// IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options) { - if (mode == CacheMode.AllowDownload) - return GetUsersAsync(options); - else - return AsyncEnumerable.Empty>(); + return mode == CacheMode.AllowDownload + ? GetUsersAsync(options) + : AsyncEnumerable.Empty>(); } #endregion diff --git a/src/Discord.Net.WebSocket/BaseSocketClient.cs b/src/Discord.Net.WebSocket/BaseSocketClient.cs index 20acd85dd..bb2d489b4 100644 --- a/src/Discord.Net.WebSocket/BaseSocketClient.cs +++ b/src/Discord.Net.WebSocket/BaseSocketClient.cs @@ -209,7 +209,7 @@ namespace Discord.WebSocket /// Sets the of the logged-in user. /// /// - /// This method sets the of the user. + /// This method sets the of the user. /// /// Discord will only accept setting of name and the type of activity. /// @@ -219,7 +219,7 @@ namespace Discord.WebSocket /// /// /// Rich Presence cannot be set via this method or client. Rich Presence is strictly limited to RPC - /// clients only. + /// clients only. /// /// /// The activity to be set. @@ -240,7 +240,7 @@ namespace Discord.WebSocket /// Creates a guild for the logged-in user who is in less than 10 active guilds. /// /// - /// This method creates a new guild on behalf of the logged-in user. + /// This method creates a new guild on behalf of the logged-in user. /// /// Due to Discord's limitation, this method will only work for users that are in less than 10 guilds. /// @@ -317,8 +317,15 @@ namespace Discord.WebSocket => await CreateGuildAsync(name, region, jpegIcon, options).ConfigureAwait(false); /// - Task IDiscordClient.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) - => Task.FromResult(GetUser(id)); + async Task IDiscordClient.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + { + var user = GetUser(id); + if (user is not null || mode == CacheMode.CacheOnly) + return user; + + return await Rest.GetUserAsync(id, options).ConfigureAwait(false); + } + /// Task IDiscordClient.GetUserAsync(string username, string discriminator, RequestOptions options) => Task.FromResult(GetUser(username, discriminator)); diff --git a/src/Discord.Net.WebSocket/DiscordShardedClient.cs b/src/Discord.Net.WebSocket/DiscordShardedClient.cs index 51c6d3c34..8374f2877 100644 --- a/src/Discord.Net.WebSocket/DiscordShardedClient.cs +++ b/src/Discord.Net.WebSocket/DiscordShardedClient.cs @@ -533,8 +533,15 @@ namespace Discord.WebSocket => await CreateGuildAsync(name, region, jpegIcon).ConfigureAwait(false); /// - Task IDiscordClient.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) - => Task.FromResult(GetUser(id)); + async Task IDiscordClient.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + { + var user = GetUser(id); + if (user is not null || mode == CacheMode.CacheOnly) + return user; + + return await Rest.GetUserAsync(id, options).ConfigureAwait(false); + } + /// Task IDiscordClient.GetUserAsync(string username, string discriminator, RequestOptions options) => Task.FromResult(GetUser(username, discriminator)); diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index b0215d9ef..cd40a491f 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -543,7 +543,7 @@ namespace Discord.WebSocket if(model == null) return null; - + if (model.GuildId.IsSpecified) { var guild = State.GetGuild(model.GuildId.Value); @@ -2128,7 +2128,7 @@ namespace Discord.WebSocket { await TimedInvokeAsync(_speakerRemoved, nameof(SpeakerRemoved), stage, guildUser); } - } + } } await TimedInvokeAsync(_userVoiceStateUpdatedEvent, nameof(UserVoiceStateUpdated), user, before, after).ConfigureAwait(false); @@ -2520,7 +2520,7 @@ namespace Discord.WebSocket } break; - case "THREAD_MEMBERS_UPDATE": + case "THREAD_MEMBERS_UPDATE": { await _gatewayLogger.DebugAsync("Received Dispatch (THREAD_MEMBERS_UPDATE)").ConfigureAwait(false); @@ -3113,7 +3113,14 @@ namespace Discord.WebSocket /// async Task IDiscordClient.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) - => mode == CacheMode.AllowDownload ? await GetUserAsync(id, options).ConfigureAwait(false) : GetUser(id); + { + var user = GetUser(id); + if (user is not null || mode == CacheMode.CacheOnly) + return user; + + return await Rest.GetUserAsync(id, options).ConfigureAwait(false); + } + /// Task IDiscordClient.GetUserAsync(string username, string discriminator, RequestOptions options) => Task.FromResult(GetUser(username, discriminator)); diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs index 9c7dd4fbd..43f23de1a 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs @@ -4,6 +4,7 @@ using System.Collections.Immutable; using System.Diagnostics; using System.Linq; using System.Threading.Tasks; +using Discord.Rest; using Model = Discord.API.Channel; namespace Discord.WebSocket @@ -64,21 +65,44 @@ namespace Discord.WebSocket #endregion #region IGuildChannel + /// - IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options) - => ImmutableArray.Create>(Users).ToAsyncEnumerable(); + IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode, + RequestOptions options) + { + return mode == CacheMode.AllowDownload + ? ChannelHelper.GetUsersAsync(this, Guild, Discord, null, null, options) + : ImmutableArray.Create>(Users).ToAsyncEnumerable(); + } /// - Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) - => Task.FromResult(GetUser(id)); + async Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + { + var user = GetUser(id); + if (user is not null || mode == CacheMode.CacheOnly) + return user; + + return await ChannelHelper.GetUserAsync(this, Guild, Discord, id, options).ConfigureAwait(false); + } #endregion #region IChannel + /// IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) - => ImmutableArray.Create>(Users).ToAsyncEnumerable(); + { + return mode == CacheMode.AllowDownload + ? ChannelHelper.GetUsersAsync(this, Guild, Discord, null, null, options) + : ImmutableArray.Create>(Users).ToAsyncEnumerable(); + } /// - Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) - => Task.FromResult(GetUser(id)); + async Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + { + var user = GetUser(id); + if (user is not null || mode == CacheMode.CacheOnly) + return user; + + return await ChannelHelper.GetUserAsync(this, Guild, Discord, id, options).ConfigureAwait(false); + } #endregion } } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs index c8137784f..afb133ac2 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs @@ -352,7 +352,7 @@ namespace Discord.WebSocket Task IAudioChannel.ModifyAsync(Action func, RequestOptions options) { throw new NotSupportedException(); } #endregion - #region IChannel + #region IChannel /// Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetUser(id)); diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs index 45eb28444..79f02fe1c 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs @@ -214,10 +214,10 @@ namespace Discord.WebSocket /// IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options) - => ImmutableArray.Create>(Users).ToAsyncEnumerable(); + => ImmutableArray.Create>(Users).ToAsyncEnumerable(); //Overridden in Text/Voice /// Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) - => Task.FromResult(GetUser(id)); + => Task.FromResult(GetUser(id)); //Overridden in Text/Voice #endregion #region IChannel diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs index dbf238625..9591f68fe 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs @@ -103,7 +103,7 @@ namespace Discord.WebSocket /// The duration on which this thread archives after. /// /// Note: Options and - /// are only available for guilds that are boosted. You can check in the to see if the + /// are only available for guilds that are boosted. You can check in the to see if the /// guild has the THREE_DAY_THREAD_ARCHIVE and SEVEN_DAY_THREAD_ARCHIVE. /// /// @@ -355,11 +355,22 @@ namespace Discord.WebSocket #region IGuildChannel /// - Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) - => Task.FromResult(GetUser(id)); + async Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + { + var user = GetUser(id); + if (user is not null || mode == CacheMode.CacheOnly) + return user; + + return await ChannelHelper.GetUserAsync(this, Guild, Discord, id, options).ConfigureAwait(false); + } /// IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options) - => ImmutableArray.Create>(Users).ToAsyncEnumerable(); + { + return mode == CacheMode.AllowDownload + ? ChannelHelper.GetUsersAsync(this, Guild, Discord, null, null, options) + : ImmutableArray.Create>(Users).ToAsyncEnumerable(); + } + #endregion #region IMessageChannel diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index b38dfcd74..bd5d811f1 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -372,7 +372,7 @@ namespace Discord.WebSocket /// This field is based off of caching alone, since there is no events returned on the guild model. /// /// - /// A read-only collection of guild events found within this guild. + /// A read-only collection of guild events found within this guild. /// public IReadOnlyCollection Events => _events.ToReadOnlyCollection(); @@ -1928,8 +1928,15 @@ namespace Discord.WebSocket async Task IGuild.AddGuildUserAsync(ulong userId, string accessToken, Action func, RequestOptions options) => await AddGuildUserAsync(userId, accessToken, func, options); /// - Task IGuild.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) - => Task.FromResult(GetUser(id)); + async Task IGuild.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + { + var user = GetUser(id); + if (user is not null || mode == CacheMode.CacheOnly) + return user; + + return await GuildHelper.GetUserAsync(this, Discord, id, options).ConfigureAwait(false); + } + /// Task IGuild.GetCurrentUserAsync(CacheMode mode, RequestOptions options) => Task.FromResult(CurrentUser); From 1fb62de14b071f0d2a4b261ae7344d3a586c4e41 Mon Sep 17 00:00:00 2001 From: CottageDwellingCat <80918250+CottageDwellingCat@users.noreply.github.com> Date: Wed, 2 Mar 2022 17:23:27 -0600 Subject: [PATCH 25/35] Support Sending Message Flags (#2131) * Add message flags * Add webhook message flags --- .../Entities/Channels/IMessageChannel.cs | 15 ++- .../API/Rest/CreateMessageParams.cs | 3 + .../Entities/Channels/ChannelHelper.cs | 58 +++++++++--- .../Entities/Channels/IRestMessageChannel.cs | 86 ++--------------- .../Entities/Channels/RestDMChannel.cs | 79 ++++++++++++---- .../Entities/Channels/RestGroupChannel.cs | 90 +++++++++++++----- .../Entities/Channels/RestTextChannel.cs | 76 +++++++++++---- .../Channels/ISocketMessageChannel.cs | 93 ++++--------------- .../Entities/Channels/SocketDMChannel.cs | 81 ++++++++++++---- .../Entities/Channels/SocketGroupChannel.cs | 80 ++++++++++++---- .../Entities/Channels/SocketTextChannel.cs | 89 +++++++++++++----- .../DiscordWebhookClient.cs | 23 +++-- .../WebhookClientHelper.cs | 42 +++++++-- .../MockedEntities/MockedDMChannel.cs | 10 +- .../MockedEntities/MockedGroupChannel.cs | 10 +- .../MockedEntities/MockedTextChannel.cs | 13 +-- 16 files changed, 515 insertions(+), 333 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs b/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs index 00ec38746..60a7c7575 100644 --- a/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs @@ -31,11 +31,12 @@ 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 task that represents an asynchronous send operation for delivering the message. The task result /// contains the sent message. /// - 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); + 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); /// /// Sends a file to this message channel with an optional caption. /// @@ -71,11 +72,12 @@ 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 task that represents an asynchronous send operation for delivering the message. The task result /// contains the sent message. /// - 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); + 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); /// /// Sends a file to this message channel with an optional caption. /// @@ -108,11 +110,12 @@ 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 task that represents an asynchronous send operation for delivering the message. The task result /// contains the sent message. /// - 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); + 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); /// /// Sends a file to this message channel with an optional caption. /// @@ -137,11 +140,12 @@ 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 task that represents an asynchronous send operation for delivering the message. The task result /// contains the sent message. /// - 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); + 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); /// /// Sends a collection of files to this message channel. /// @@ -166,11 +170,12 @@ 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 task that represents an asynchronous send operation for delivering the message. The task result /// contains the sent message. /// - 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); + 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); /// /// Gets a message from this message channel. diff --git a/src/Discord.Net.Rest/API/Rest/CreateMessageParams.cs b/src/Discord.Net.Rest/API/Rest/CreateMessageParams.cs index 5996c7e83..466ad41e3 100644 --- a/src/Discord.Net.Rest/API/Rest/CreateMessageParams.cs +++ b/src/Discord.Net.Rest/API/Rest/CreateMessageParams.cs @@ -28,6 +28,9 @@ namespace Discord.API.Rest [JsonProperty("sticker_ids")] public Optional Stickers { get; set; } + + [JsonProperty("flags")] + public Optional Flags { get; set; } public CreateMessageParams(string content) { diff --git a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs index b5087cd2f..d66fd5e51 100644 --- a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs +++ b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs @@ -266,8 +266,10 @@ namespace Discord.Rest } /// Message content is too long, length must be less or equal to . + /// The only valid are and . public static async Task SendMessageAsync(IMessageChannel channel, BaseDiscordClient client, - string text, bool isTTS, Embed embed, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, RequestOptions options, Embed[] embeds) + string text, bool isTTS, Embed embed, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, RequestOptions options, Embed[] embeds, MessageFlags flags) { embeds ??= Array.Empty(); if (embed != null) @@ -298,6 +300,10 @@ 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) + throw new ArgumentException("The only valid MessageFlags are SuppressEmbeds and none.", nameof(flags)); + var args = new CreateMessageParams(text) { IsTTS = isTTS, @@ -305,7 +311,8 @@ namespace Discord.Rest AllowedMentions = allowedMentions?.ToModel(), MessageReference = messageReference?.ToModel(), Components = components?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified, - Stickers = stickers?.Any() ?? false ? stickers.Select(x => x.Id).ToArray() : Optional.Unspecified + Stickers = stickers?.Any() ?? false ? stickers.Select(x => x.Id).ToArray() : Optional.Unspecified, + Flags = flags }; var model = await client.ApiClient.CreateMessageAsync(channel.Id, args, options).ConfigureAwait(false); return RestUserMessage.Create(client, channel, client.CurrentUser, model); @@ -335,29 +342,44 @@ 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 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, bool isSpoiler, Embed[] embeds) + string filePath, string text, bool isTTS, Embed embed, AllowedMentions allowedMentions, + MessageReference messageReference, MessageComponent components, ISticker[] stickers, RequestOptions options, + bool isSpoiler, Embed[] embeds, MessageFlags flags = MessageFlags.None) { string filename = Path.GetFileName(filePath); using (var file = File.OpenRead(filePath)) - return await SendFileAsync(channel, client, file, filename, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, isSpoiler, embeds).ConfigureAwait(false); + return await SendFileAsync(channel, client, file, filename, text, isTTS, embed, allowedMentions, + messageReference, components, stickers, options, isSpoiler, embeds, flags).ConfigureAwait(false); } /// Message content is too long, length must be less or equal to . + /// The only valid are and . public static async Task SendFileAsync(IMessageChannel channel, BaseDiscordClient client, - Stream stream, string filename, string text, bool isTTS, Embed embed, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, RequestOptions options, bool isSpoiler, Embed[] embeds) + Stream stream, string filename, string text, bool isTTS, Embed embed, AllowedMentions allowedMentions, + MessageReference messageReference, MessageComponent components, ISticker[] stickers, RequestOptions options, + bool isSpoiler, Embed[] embeds, MessageFlags flags = MessageFlags.None) { using (var file = new FileAttachment(stream, filename, isSpoiler: isSpoiler)) - return await SendFileAsync(channel, client, file, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds).ConfigureAwait(false); + return await SendFileAsync(channel, client, file, text, isTTS, embed, allowedMentions, messageReference, + components, stickers, options, embeds, flags).ConfigureAwait(false); } /// Message content is too long, length must be less or equal to . + /// The only valid are and . public static Task SendFileAsync(IMessageChannel channel, BaseDiscordClient client, - FileAttachment attachment, string text, bool isTTS, Embed embed, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, RequestOptions options, Embed[] embeds) - => SendFilesAsync(channel, client, new[] { attachment }, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds); + FileAttachment attachment, string text, bool isTTS, Embed embed, AllowedMentions allowedMentions, + MessageReference messageReference, MessageComponent components, ISticker[] stickers, RequestOptions options, + Embed[] embeds, MessageFlags flags = MessageFlags.None) + => SendFilesAsync(channel, client, new[] { attachment }, text, isTTS, embed, allowedMentions, messageReference, + components, stickers, options, embeds, flags); + /// 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, Embed[] embeds) + IEnumerable attachments, string text, bool isTTS, Embed embed, AllowedMentions allowedMentions, + MessageReference messageReference, MessageComponent components, ISticker[] stickers, RequestOptions options, + Embed[] embeds, MessageFlags flags) { embeds ??= Array.Empty(); if (embed != null) @@ -366,7 +388,7 @@ 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."); - + foreach(var attachment in attachments) { Preconditions.NotNullOrEmpty(attachment.FileName, nameof(attachment.FileName), "File Name must not be empty or null"); @@ -398,12 +420,26 @@ 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 (stickers != null) { Preconditions.AtMost(stickers.Length, 3, nameof(stickers), "A max of 3 stickers are allowed."); } - var args = new UploadFileParams(attachments.ToArray()) { Content = text, IsTTS = isTTS, Embeds = embeds.Any() ? embeds.Select(x => x.ToModel()).ToArray() : Optional.Unspecified, AllowedMentions = allowedMentions?.ToModel() ?? Optional.Unspecified, MessageReference = messageReference?.ToModel() ?? Optional.Unspecified, MessageComponent = components?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified, Stickers = stickers?.Any() ?? false ? stickers.Select(x => x.Id).ToArray() : Optional.Unspecified }; + var args = new UploadFileParams(attachments.ToArray()) + { + Content = text, + IsTTS = isTTS, + Embeds = embeds.Any() ? embeds.Select(x => x.ToModel()).ToArray() : Optional.Unspecified, + AllowedMentions = allowedMentions?.ToModel() ?? Optional.Unspecified, + MessageReference = messageReference?.ToModel() ?? Optional.Unspecified, + MessageComponent = components?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() ?? Optional.Unspecified, + Stickers = stickers?.Any() ?? false ? stickers.Select(x => x.Id).ToArray() : Optional.Unspecified, + Flags = flags + }; + var model = await client.ApiClient.UploadFileAsync(channel.Id, args, options).ConfigureAwait(false); return RestUserMessage.Create(client, channel, client.CurrentUser, model); } diff --git a/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs b/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs index 1af936a57..0cf92bb04 100644 --- a/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs @@ -9,84 +9,14 @@ namespace Discord.Rest /// public interface IRestMessageChannel : IMessageChannel { - /// - /// Sends a message to this message channel. - /// - /// - /// This method follows the same behavior as described in . - /// Please visit its documentation for more details on this method. - /// - /// The message to be sent. - /// Determines whether the message should be read aloud by Discord or not. - /// The to be sent. - /// The options to be used when sending the request. - /// - /// Specifies if notifications are sent for mentioned users and roles in the message . - /// If null, all mentioned roles and users will be notified. - /// - /// The message references to be included. Used to reply to specific messages. - /// 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 task that represents an asynchronous send operation for delivering the message. The task result - /// contains the sent message. - /// - new 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); - /// - /// Sends a file to this message channel with an optional caption. - /// - /// - /// This method follows the same behavior as described in - /// . Please visit - /// its documentation for more details on this method. - /// - /// The file path of the file. - /// The message to be sent. - /// Whether the message should be read aloud by Discord or not. - /// The to be sent. - /// The options to be used when sending the request. - /// Whether the message attachment should be hidden as a spoiler. - /// - /// Specifies if notifications are sent for mentioned users and roles in the message . - /// If null, all mentioned roles and users will be notified. - /// - /// The message references to be included. Used to reply to specific messages. - /// 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 task that represents an asynchronous send operation for delivering the message. The task result - /// contains the sent message. - /// - new 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); - /// - /// Sends a file to this message channel with an optional caption. - /// - /// - /// This method follows the same behavior as described in . - /// Please visit its documentation for more details on this method. - /// - /// The of the file to be sent. - /// The name of the attachment. - /// The message to be sent. - /// Whether the message should be read aloud by Discord or not. - /// The to be sent. - /// The options to be used when sending the request. - /// Whether the message attachment should be hidden as a spoiler. - /// - /// Specifies if notifications are sent for mentioned users and roles in the message . - /// If null, all mentioned roles and users will be notified. - /// - /// The message references to be included. Used to reply to specific messages. - /// 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 task that represents an asynchronous send operation for delivering the message. The task result - /// contains the sent message. - /// - new 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); + /// + new 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); + + /// + new 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); + + /// + new 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); /// /// Gets a message from this message channel. diff --git a/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs index 36b190e56..3bf43a594 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs @@ -94,8 +94,12 @@ namespace Discord.Rest /// /// Message content is too long, length must be less or equal to . - public 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) - => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds); + /// The only valid are and . + public 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) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, messageReference, + components, stickers, options, embeds, flags); /// /// @@ -122,22 +126,39 @@ 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 . - public Task SendFileAsync(string filePath, string text, 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) - => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, isSpoiler, embeds); + /// The only valid are and . + public Task SendFileAsync(string filePath, string text, 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) + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, messageReference, + 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, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, isSpoiler, embeds); + public Task SendFileAsync(Stream stream, string filename, string text, 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) + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, + messageReference, components, stickers, options, isSpoiler, embeds, flags); /// /// Message content is too long, length must be less or equal to . - public Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFileAsync(this, Discord, attachment, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds); + public Task SendFileAsync(FileAttachment attachment, string text, 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) + => ChannelHelper.SendFileAsync(this, Discord, attachment, text, isTTS, embed, allowedMentions, messageReference, + components, stickers, options, embeds, flags); /// /// Message content is too long, length must be less or equal to . - public Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFilesAsync(this, Discord, attachments, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds); + public Task SendFilesAsync(IEnumerable attachments, string text, 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) + => ChannelHelper.SendFilesAsync(this, Discord, attachments, text, isTTS, embed, allowedMentions, messageReference, + components, stickers, options, embeds, flags); /// public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) @@ -219,20 +240,38 @@ namespace Discord.Rest async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) => await GetPinnedMessagesAsync(options).ConfigureAwait(false); /// - async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, + RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, + components, stickers, embeds, flags).ConfigureAwait(false); + /// - async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, + Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, + components, stickers, embeds, flags).ConfigureAwait(false); + /// - async Task IMessageChannel.SendFileAsync(FileAttachment attachment, string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFileAsync(attachment, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(FileAttachment attachment, string text, bool isTTS, + Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFileAsync(attachment, text, isTTS, embed, options, allowedMentions, messageReference, components, + stickers, embeds, flags).ConfigureAwait(false); + /// - async Task IMessageChannel.SendFilesAsync(IEnumerable attachments, string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFilesAsync(attachments, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendFilesAsync(IEnumerable attachments, string text, + bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFilesAsync(attachments, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds, flags).ConfigureAwait(false); + /// - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendMessageAsync(text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, + AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, + ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendMessageAsync(text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds, flags).ConfigureAwait(false); + #endregion #region IChannel diff --git a/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs index 03858fbbe..d21852f93 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs @@ -104,8 +104,12 @@ namespace Discord.Rest /// /// Message content is too long, length must be less or equal to . - public 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) - => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds); + /// The only valid are and . + public 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) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, messageReference, + components, stickers, options, embeds, flags); /// /// @@ -132,20 +136,40 @@ 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 . - public Task SendFileAsync(string filePath, string text, 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) - => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, isSpoiler, embeds); + /// The only valid are and . + public Task SendFileAsync(string filePath, string text, 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) + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, messageReference, + 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, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, isSpoiler, embeds); + /// The only valid are and . + public Task SendFileAsync(Stream stream, string filename, string text, 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) + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, + messageReference, components, stickers, options, isSpoiler, embeds, flags); /// /// Message content is too long, length must be less or equal to . - public Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFileAsync(this, Discord, attachment, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds); + /// The only valid are and . + public Task SendFileAsync(FileAttachment attachment, string text, 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) + => ChannelHelper.SendFileAsync(this, Discord, attachment, text, isTTS, embed, allowedMentions, messageReference, + components, stickers, options, embeds, flags); /// /// Message content is too long, length must be less or equal to . - public Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFilesAsync(this, Discord, attachments, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds); + /// The only valid are and . + public Task SendFilesAsync(IEnumerable attachments, string text, 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) + => ChannelHelper.SendFilesAsync(this, Discord, attachments, text, isTTS, embed, allowedMentions, + messageReference, components, stickers, options, embeds, flags); /// public Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); @@ -197,17 +221,41 @@ namespace Discord.Rest async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) => await GetPinnedMessagesAsync(options).ConfigureAwait(false); - async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); - - async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); - async Task IMessageChannel.SendFileAsync(FileAttachment attachment, string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFileAsync(attachment, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); - async Task IMessageChannel.SendFilesAsync(IEnumerable attachments, string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFilesAsync(attachments, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendMessageAsync(text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + /// + async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, + RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, + components, stickers, embeds, flags).ConfigureAwait(false); + + /// + async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, + Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, + components, stickers, embeds, flags).ConfigureAwait(false); + + /// + async Task IMessageChannel.SendFileAsync(FileAttachment attachment, string text, bool isTTS, + Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFileAsync(attachment, text, isTTS, embed, options, allowedMentions, messageReference, components, + stickers, embeds, flags).ConfigureAwait(false); + + /// + async Task IMessageChannel.SendFilesAsync(IEnumerable attachments, string text, + bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFilesAsync(attachments, text, isTTS, embed, options, allowedMentions, messageReference, components, + stickers, embeds, flags).ConfigureAwait(false); + + /// + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, + AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, + ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendMessageAsync(text, isTTS, embed, options, allowedMentions, messageReference, components, + stickers, embeds, flags).ConfigureAwait(false); + #endregion #region IAudioChannel diff --git a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs index 198ff22ac..76c75ab6e 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs @@ -103,8 +103,12 @@ namespace Discord.Rest /// /// Message content is too long, length must be less or equal to . - public 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) - => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds); + /// The only valid are and . + public 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) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, messageReference, + components, stickers, options, embeds, flags); /// /// @@ -131,23 +135,42 @@ 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 . - public Task SendFileAsync(string filePath, string text, 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) - => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, isSpoiler, embeds); + /// The only valid are and . + public Task SendFileAsync(string filePath, string text, 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) + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, messageReference, + 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, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, isSpoiler, embeds); + /// The only valid are and . + public Task SendFileAsync(Stream stream, string filename, string text, 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) + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, messageReference, + components, stickers, options, isSpoiler, embeds, flags); /// /// Message content is too long, length must be less or equal to . - public Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFileAsync(this, Discord, attachment, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds); + /// The only valid are and . + public Task SendFileAsync(FileAttachment attachment, string text, 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) + => ChannelHelper.SendFileAsync(this, Discord, attachment, text, isTTS, embed, allowedMentions, messageReference, + components, stickers, options, embeds, flags); /// /// Message content is too long, length must be less or equal to . - public Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFilesAsync(this, Discord, attachments, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds); + /// The only valid are and . + public Task SendFilesAsync(IEnumerable attachments, string text, 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) + => ChannelHelper.SendFilesAsync(this, Discord, attachments, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds, flags); /// public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) @@ -332,24 +355,37 @@ namespace Discord.Rest => await GetPinnedMessagesAsync(options).ConfigureAwait(false); /// - async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, + RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, + components, stickers, embeds, flags).ConfigureAwait(false); /// - async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, + Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, + components, stickers, embeds, flags).ConfigureAwait(false); /// - async Task IMessageChannel.SendFileAsync(FileAttachment attachment, string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFileAsync(attachment, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(FileAttachment attachment, string text, bool isTTS, + Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFileAsync(attachment, text, isTTS, embed, options, allowedMentions, messageReference, components, + stickers, embeds, flags).ConfigureAwait(false); /// - async Task IMessageChannel.SendFilesAsync(IEnumerable attachments, string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFilesAsync(attachments, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendFilesAsync(IEnumerable attachments, string text, + bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFilesAsync(attachments, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds, flags).ConfigureAwait(false); /// - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendMessageAsync(text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, + AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, + ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendMessageAsync(text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds, flags).ConfigureAwait(false); #endregion #region IGuildChannel diff --git a/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs index 3e9b635de..b632bcb60 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs @@ -18,83 +18,22 @@ namespace Discord.WebSocket /// IReadOnlyCollection CachedMessages { get; } - /// - /// Sends a message to this message channel. - /// - /// - /// This method follows the same behavior as described in . - /// Please visit its documentation for more details on this method. - /// - /// The message to be sent. - /// Determines whether the message should be read aloud by Discord or not. - /// The to be sent. - /// The options to be used when sending the request. - /// - /// Specifies if notifications are sent for mentioned users and roles in the message . - /// If null, all mentioned roles and users will be notified. - /// - /// The message references to be included. Used to reply to specific messages. - /// 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 task that represents an asynchronous send operation for delivering the message. The task result - /// contains the sent message. - /// - new 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); - /// - /// Sends a file to this message channel with an optional caption. - /// - /// - /// This method follows the same behavior as described in . - /// Please visit its documentation for more details on this method. - /// - /// The file path of the file. - /// The message to be sent. - /// Whether the message should be read aloud by Discord or not. - /// The to be sent. - /// The options to be used when sending the request. - /// Whether the message attachment should be hidden as a spoiler. - /// - /// Specifies if notifications are sent for mentioned users and roles in the message . - /// If null, all mentioned roles and users will be notified. - /// - /// The message references to be included. Used to reply to specific messages. - /// 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 task that represents an asynchronous send operation for delivering the message. The task result - /// contains the sent message. - /// - new 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); - /// - /// Sends a file to this message channel with an optional caption. - /// - /// - /// This method follows the same behavior as described in . - /// Please visit its documentation for more details on this method. - /// - /// The of the file to be sent. - /// The name of the attachment. - /// The message to be sent. - /// Whether the message should be read aloud by Discord or not. - /// The to be sent. - /// The options to be used when sending the request. - /// Whether the message attachment should be hidden as a spoiler. - /// - /// Specifies if notifications are sent for mentioned users and roles in the message . - /// If null, all mentioned roles and users will be notified. - /// - /// The message references to be included. Used to reply to specific messages. - /// 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 task that represents an asynchronous send operation for delivering the message. The task result - /// contains the sent message. - /// - new 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); + /// + new 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); + + /// + new 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); + + /// + new 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); /// /// Gets a cached message from this channel. diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs index f4fe12755..17ab4ebe3 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs @@ -139,24 +139,48 @@ namespace Discord.WebSocket /// /// Message content is too long, length must be less or equal to . - public 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) - => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds); + /// The only valid are and . + public 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) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, messageReference, + components, stickers, options, embeds, flags); /// - public Task SendFileAsync(string filePath, string text, 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) - => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, isSpoiler, embeds); + /// The only valid are and . + public Task SendFileAsync(string filePath, string text, 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) + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, messageReference, + 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, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, isSpoiler, embeds); + /// The only valid are and . + public Task SendFileAsync(Stream stream, string filename, string text, 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) + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, + messageReference, components, stickers, options, isSpoiler, embeds, flags); /// /// Message content is too long, length must be less or equal to . - public Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFileAsync(this, Discord, attachment, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds); + /// The only valid are and . + public Task SendFileAsync(FileAttachment attachment, string text, 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) + => ChannelHelper.SendFileAsync(this, Discord, attachment, text, isTTS, embed, allowedMentions, + messageReference, components, stickers, options, embeds, flags); /// /// Message content is too long, length must be less or equal to . - public Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFilesAsync(this, Discord, attachments, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds); + /// The only valid are and . + public Task SendFilesAsync(IEnumerable attachments, string text, 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) + => ChannelHelper.SendFilesAsync(this, Discord, attachments, text, isTTS, embed, allowedMentions, + messageReference, components, stickers, options, embeds, flags); /// public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) @@ -255,20 +279,37 @@ namespace Discord.WebSocket async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) => await GetPinnedMessagesAsync(options).ConfigureAwait(false); /// - async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, + RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, + components, stickers, embeds, flags).ConfigureAwait(false); + /// - async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, + Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, + components, stickers, embeds, flags).ConfigureAwait(false); + /// - async Task IMessageChannel.SendFileAsync(FileAttachment attachment, string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFileAsync(attachment, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(FileAttachment attachment, string text, bool isTTS, + Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFileAsync(attachment, text, isTTS, embed, options, allowedMentions, messageReference, components, + stickers, embeds, flags).ConfigureAwait(false); + /// - async Task IMessageChannel.SendFilesAsync(IEnumerable attachments, string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFilesAsync(attachments, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendFilesAsync(IEnumerable attachments, string text, + bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFilesAsync(attachments, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds, flags).ConfigureAwait(false); + /// - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendMessageAsync(text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, + AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, + ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendMessageAsync(text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds, flags).ConfigureAwait(false); #endregion #region IChannel diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs index afb133ac2..4f068cf81 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs @@ -178,24 +178,48 @@ namespace Discord.WebSocket /// /// Message content is too long, length must be less or equal to . - public 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) - => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds); + /// The only valid are and . + public 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) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, messageReference, + components, stickers, options, embeds, flags); /// - public Task SendFileAsync(string filePath, string text, 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) - => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, isSpoiler, embeds); + /// The only valid are and . + public Task SendFileAsync(string filePath, string text, 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) + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, messageReference, + 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, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, isSpoiler, embeds); + /// The only valid are and . + public Task SendFileAsync(Stream stream, string filename, string text, 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) + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, + messageReference, components, stickers, options, isSpoiler, embeds, flags); /// /// Message content is too long, length must be less or equal to . - public Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFileAsync(this, Discord, attachment, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds); + /// The only valid are and . + public Task SendFileAsync(FileAttachment attachment, string text, 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) + => ChannelHelper.SendFileAsync(this, Discord, attachment, text, isTTS, embed, allowedMentions, + messageReference, components, stickers, options, embeds, flags); /// /// Message content is too long, length must be less or equal to . - public Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFilesAsync(this, Discord, attachments, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds); + /// The only valid are and . + public Task SendFilesAsync(IEnumerable attachments, string text, 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) + => ChannelHelper.SendFilesAsync(this, Discord, attachments, text, isTTS, embed, allowedMentions, + messageReference, components, stickers, options, embeds, flags); /// public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) @@ -327,21 +351,37 @@ namespace Discord.WebSocket => await GetPinnedMessagesAsync(options).ConfigureAwait(false); /// - async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, + RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, + components, stickers, embeds, flags).ConfigureAwait(false); + /// - async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, + Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, + components, stickers, embeds, flags).ConfigureAwait(false); + /// - async Task IMessageChannel.SendFileAsync(FileAttachment attachment, string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFileAsync(attachment, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(FileAttachment attachment, string text, bool isTTS, + Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFileAsync(attachment, text, isTTS, embed, options, allowedMentions, messageReference, components, + stickers, embeds, flags).ConfigureAwait(false); + /// - async Task IMessageChannel.SendFilesAsync(IEnumerable attachments, string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFilesAsync(attachments, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendFilesAsync(IEnumerable attachments, string text, + bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFilesAsync(attachments, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds, flags).ConfigureAwait(false); /// - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendMessageAsync(text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, + AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, + ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendMessageAsync(text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds, flags).ConfigureAwait(false); #endregion #region IAudioChannel diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs index 9591f68fe..e4a299edc 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs @@ -212,27 +212,48 @@ namespace Discord.WebSocket /// /// Message content is too long, length must be less or equal to . - public 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) - => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds); - - /// - public Task SendFileAsync(string filePath, string text, 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) - => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, isSpoiler, embeds); - + /// The only valid are and . + public 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) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, messageReference, + components, stickers, options, embeds, flags); + + /// + /// The only valid are and . + public Task SendFileAsync(string filePath, string text, 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) + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, messageReference, + 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, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, isSpoiler, embeds); - + /// The only valid are and . + public Task SendFileAsync(Stream stream, string filename, string text, 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) + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, + messageReference, components, stickers, options, isSpoiler, embeds, flags); /// /// Message content is too long, length must be less or equal to . - public Task SendFileAsync(FileAttachment attachment, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFileAsync(this, Discord, attachment, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds); - + /// The only valid are and . + public Task SendFileAsync(FileAttachment attachment, string text, 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) + => ChannelHelper.SendFileAsync(this, Discord, attachment, text, isTTS, embed, allowedMentions, + messageReference, components, stickers, options, embeds, flags); /// /// Message content is too long, length must be less or equal to . - public Task SendFilesAsync(IEnumerable attachments, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent components = null, ISticker[] stickers = null, Embed[] embeds = null) - => ChannelHelper.SendFilesAsync(this, Discord, attachments, text, isTTS, embed, allowedMentions, messageReference, components, stickers, options, embeds); + /// The only valid are and . + public Task SendFilesAsync(IEnumerable attachments, string text, 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) + => ChannelHelper.SendFilesAsync(this, Discord, attachments, text, isTTS, embed, allowedMentions, + messageReference, components, stickers, options, embeds, flags); /// public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) @@ -396,20 +417,38 @@ namespace Discord.WebSocket => await GetPinnedMessagesAsync(options).ConfigureAwait(false); /// - async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, + RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, + components, stickers, embeds, flags).ConfigureAwait(false); + /// - async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, + Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions, messageReference, + components, stickers, embeds, flags).ConfigureAwait(false); + /// - async Task IMessageChannel.SendFileAsync(FileAttachment attachment, string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFileAsync(attachment, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(FileAttachment attachment, string text, bool isTTS, + Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFileAsync(attachment, text, isTTS, embed, options, allowedMentions, messageReference, components, + stickers, embeds, flags).ConfigureAwait(false); + /// - async Task IMessageChannel.SendFilesAsync(IEnumerable attachments, string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendFilesAsync(attachments, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendFilesAsync(IEnumerable attachments, string text, + bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, + MessageComponent components, ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendFilesAsync(attachments, text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds, flags).ConfigureAwait(false); + /// - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, ISticker[] stickers, Embed[] embeds) - => await SendMessageAsync(text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds).ConfigureAwait(false); + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, + AllowedMentions allowedMentions, MessageReference messageReference, MessageComponent components, + ISticker[] stickers, Embed[] embeds, MessageFlags flags) + => await SendMessageAsync(text, isTTS, embed, options, allowedMentions, messageReference, components, stickers, embeds, flags).ConfigureAwait(false); + #endregion #region INestedChannel diff --git a/src/Discord.Net.Webhook/DiscordWebhookClient.cs b/src/Discord.Net.Webhook/DiscordWebhookClient.cs index f7bc38587..405100f89 100644 --- a/src/Discord.Net.Webhook/DiscordWebhookClient.cs +++ b/src/Discord.Net.Webhook/DiscordWebhookClient.cs @@ -87,8 +87,9 @@ namespace Discord.Webhook /// 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) - => WebhookClientHelper.SendMessageAsync(this, text, isTTS, embeds, username, avatarUrl, allowedMentions, options, components); + string username = null, string avatarUrl = null, RequestOptions options = null, AllowedMentions allowedMentions = null, + MessageComponent components = null, MessageFlags flags = MessageFlags.None) + => WebhookClientHelper.SendMessageAsync(this, text, isTTS, embeds, username, avatarUrl, allowedMentions, options, components, flags); /// /// Modifies a message posted using this webhook. @@ -124,33 +125,35 @@ namespace Discord.Webhook 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) + MessageComponent components = null, MessageFlags flags = MessageFlags.None) => WebhookClientHelper.SendFileAsync(this, filePath, text, isTTS, embeds, username, avatarUrl, - allowedMentions, options, isSpoiler, components); + allowedMentions, options, isSpoiler, components, flags); /// 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) + MessageComponent components = null, MessageFlags flags = MessageFlags.None) => WebhookClientHelper.SendFileAsync(this, stream, filename, text, isTTS, embeds, username, - avatarUrl, allowedMentions, options, isSpoiler, components); + avatarUrl, allowedMentions, options, isSpoiler, components, flags); /// 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) + RequestOptions options = null, AllowedMentions allowedMentions = null, MessageComponent components = null, + MessageFlags flags = MessageFlags.None) => WebhookClientHelper.SendFileAsync(this, attachment, text, isTTS, embeds, username, - avatarUrl, allowedMentions, components, options); + avatarUrl, allowedMentions, components, options, flags); /// 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) + RequestOptions options = null, AllowedMentions allowedMentions = null, MessageComponent components = null, + MessageFlags flags = MessageFlags.None) => WebhookClientHelper.SendFilesAsync(this, attachments, text, isTTS, embeds, username, avatarUrl, - allowedMentions, components, options); + allowedMentions, components, options, flags); /// Modifies the properties of this webhook. diff --git a/src/Discord.Net.Webhook/WebhookClientHelper.cs b/src/Discord.Net.Webhook/WebhookClientHelper.cs index a9d5a25da..0a974a9d9 100644 --- a/src/Discord.Net.Webhook/WebhookClientHelper.cs +++ b/src/Discord.Net.Webhook/WebhookClientHelper.cs @@ -21,12 +21,14 @@ namespace Discord.Webhook 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) + string text, bool isTTS, IEnumerable embeds, string username, string avatarUrl, + AllowedMentions allowedMentions, RequestOptions options, MessageComponent components, MessageFlags flags) { var args = new CreateWebhookMessageParams { Content = text, - IsTTS = isTTS + IsTTS = isTTS, + Flags = flags }; if (embeds != null) @@ -40,6 +42,9 @@ namespace Discord.Webhook if (components != null) args.Components = components?.Components.Select(x => new API.ActionRowComponent(x)).ToArray(); + if (flags is not MessageFlags.None and not MessageFlags.SuppressEmbeds) + throw new ArgumentException("The only valid MessageFlags are SuppressEmbeds and none.", nameof(flags)); + var model = await client.ApiClient.CreateWebhookMessageAsync(client.Webhook.Id, args, options: options).ConfigureAwait(false); return model.Id; } @@ -97,22 +102,27 @@ 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, MessageComponent components) + IEnumerable embeds, string username, string avatarUrl, AllowedMentions allowedMentions, RequestOptions options, + bool isSpoiler, MessageComponent components, MessageFlags flags = MessageFlags.None) { 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, components).ConfigureAwait(false); + return await SendFileAsync(client, file, filename, text, isTTS, embeds, username, avatarUrl, allowedMentions, options, isSpoiler, components, flags).ConfigureAwait(false); } 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); + MessageComponent components, MessageFlags flags) + => SendFileAsync(client, new FileAttachment(stream, filename, isSpoiler: isSpoiler), text, isTTS, embeds, username, avatarUrl, allowedMentions, components, options, flags); - 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 Task SendFileAsync(DiscordWebhookClient client, FileAttachment attachment, string text, bool isTTS, + IEnumerable embeds, string username, string avatarUrl, AllowedMentions allowedMentions, + MessageComponent components, RequestOptions options, MessageFlags flags) + => SendFilesAsync(client, new FileAttachment[] { attachment }, text, isTTS, embeds, username, avatarUrl, allowedMentions, components, options, flags); 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) + IEnumerable attachments, string text, bool isTTS, IEnumerable embeds, string username, + string avatarUrl, AllowedMentions allowedMentions, MessageComponent components, RequestOptions options, + MessageFlags flags) { embeds ??= Array.Empty(); @@ -141,7 +151,19 @@ namespace Discord.Webhook } } - 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 }; + if (flags is not MessageFlags.None and not MessageFlags.SuppressEmbeds) + throw new ArgumentException("The only valid MessageFlags are SuppressEmbeds and none.", nameof(flags)); + + 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, + Flags = flags + }; var msg = await client.ApiClient.UploadWebhookFileAsync(client.Webhook.Id, args, options).ConfigureAwait(false); return msg.Id; } diff --git a/test/Discord.Net.Tests.Unit/MockedEntities/MockedDMChannel.cs b/test/Discord.Net.Tests.Unit/MockedEntities/MockedDMChannel.cs index 519bab4d9..2a7f8065a 100644 --- a/test/Discord.Net.Tests.Unit/MockedEntities/MockedDMChannel.cs +++ b/test/Discord.Net.Tests.Unit/MockedEntities/MockedDMChannel.cs @@ -83,10 +83,10 @@ namespace Discord throw new NotImplementedException(); } - public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) => throw new NotImplementedException(); - 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 component = null, ISticker[] stickers = null, Embed[] embeds = null) => throw new NotImplementedException(); - 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 component = null, ISticker[] stickers = null, Embed[] embeds = null) => throw new NotImplementedException(); - public Task SendFileAsync(FileAttachment attachment, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) => throw new NotImplementedException(); - public Task SendFilesAsync(IEnumerable attachments, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) => throw new NotImplementedException(); + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) => throw new NotImplementedException(); + 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 component = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) => throw new NotImplementedException(); + 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 component = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) => throw new NotImplementedException(); + public Task SendFileAsync(FileAttachment attachment, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) => throw new NotImplementedException(); + public Task SendFilesAsync(IEnumerable attachments, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) => throw new NotImplementedException(); } } diff --git a/test/Discord.Net.Tests.Unit/MockedEntities/MockedGroupChannel.cs b/test/Discord.Net.Tests.Unit/MockedEntities/MockedGroupChannel.cs index 9c94efffa..b7f98f572 100644 --- a/test/Discord.Net.Tests.Unit/MockedEntities/MockedGroupChannel.cs +++ b/test/Discord.Net.Tests.Unit/MockedEntities/MockedGroupChannel.cs @@ -93,17 +93,17 @@ namespace Discord throw new NotImplementedException(); } - 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 component = null, ISticker[] stickers = null, Embed[] embeds = 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 component = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) { throw new NotImplementedException(); } - 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 component = null, ISticker[] stickers = null, Embed[] embeds = null) + 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 component = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) { throw new NotImplementedException(); } - public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) { throw new NotImplementedException(); } @@ -113,7 +113,7 @@ namespace Discord throw new NotImplementedException(); } - public Task SendFileAsync(FileAttachment attachment, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) => throw new NotImplementedException(); - public Task SendFilesAsync(IEnumerable attachments, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) => throw new NotImplementedException(); + public Task SendFileAsync(FileAttachment attachment, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) => throw new NotImplementedException(); + public Task SendFilesAsync(IEnumerable attachments, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) => throw new NotImplementedException(); } } diff --git a/test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs b/test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs index ad0af04b2..0dfcab7a5 100644 --- a/test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs +++ b/test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs @@ -176,17 +176,17 @@ namespace Discord throw new NotImplementedException(); } - 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 component = null, ISticker[] stickers = null, Embed[] embeds = 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 component = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) { throw new NotImplementedException(); } - 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 component = null, ISticker[] stickers = null, Embed[] embeds = null) + 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 component = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) { throw new NotImplementedException(); } - public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) { throw new NotImplementedException(); } @@ -211,9 +211,10 @@ namespace Discord throw new NotImplementedException(); } - public Task SendFileAsync(FileAttachment attachment, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) => throw new NotImplementedException(); - public Task SendFilesAsync(IEnumerable attachments, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null) => throw new NotImplementedException(); - public Task CreateThreadAsync(string name, ThreadType type = ThreadType.PublicThread, ThreadArchiveDuration autoArchiveDuration = ThreadArchiveDuration.OneDay, IMessage message = null, bool? invitable = null, int? slowmode = null, RequestOptions options = null) => throw new NotImplementedException(); + public Task SendFileAsync(FileAttachment attachment, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) => throw new NotImplementedException(); + public Task SendFilesAsync(IEnumerable attachments, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null, MessageComponent component = null, ISticker[] stickers = null, Embed[] embeds = null, MessageFlags flags = MessageFlags.None) => throw new NotImplementedException(); + public Task CreateThreadAsync(string name, ThreadType type = ThreadType.PublicThread, ThreadArchiveDuration autoArchiveDuration = ThreadArchiveDuration.OneDay, IMessage message = null, bool? invitable = null, int? slowmode = null, RequestOptions options = null, MessageFlags flags = MessageFlags.None) => throw new NotImplementedException(); public Task CreateInviteToApplicationAsync(DefaultApplications application, int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) => throw new NotImplementedException(); + public Task CreateThreadAsync(string name, ThreadType type = ThreadType.PublicThread, ThreadArchiveDuration autoArchiveDuration = ThreadArchiveDuration.OneDay, IMessage message = null, bool? invitable = null, int? slowmode = null, RequestOptions options = null) => throw new NotImplementedException(); } } From 9ba64f62d1013387231a98c00dc5d1dbb86bdb23 Mon Sep 17 00:00:00 2001 From: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Wed, 2 Mar 2022 19:23:39 -0400 Subject: [PATCH 26/35] Interaction Service Complex Parameters (#2155) * Interaction Service Complex Parameters * add complex parameters * add complex parameters * fix build errors * add argument parsing * add nested complex parameter checks * add inline docs * add preferred constructor declaration * fix autocompletehandlers for complex parameters * make GetConstructor private * use flattened params in ToProps method * make DiscordType of SlashParameter nullable * add docs to Flattened parameters collection and move the GetComplexParameterCtor method * add inline docs to SlashCommandParameterBuilder.ComplexParameterFields * add check for validating required/optinal parameter order * implement change requests * return internal ParseResult as ExecuteResult Co-Authored-By: Cenk Ergen <57065323+Cenngo@users.noreply.github.com> * fix merge errors Co-authored-by: Cenk Ergen <57065323+Cenngo@users.noreply.github.com> --- .../Attributes/ComplexParameterAttribute.cs | 30 +++++ .../ComplexParameterCtorAttribute.cs | 10 ++ .../Builders/ModuleClassBuilder.cs | 57 +++++++++- .../SlashCommandParameterBuilder.cs | 68 +++++++++++- .../Info/Commands/CommandInfo.cs | 3 + .../Info/Commands/SlashCommandInfo.cs | 104 ++++++++++++++---- .../Parameters/SlashCommandParameterInfo.cs | 30 ++++- .../InteractionService.cs | 4 +- .../Results/ParseResult.cs | 36 ++++++ .../Utilities/ApplicationCommandRestUtil.cs | 6 +- 10 files changed, 315 insertions(+), 33 deletions(-) create mode 100644 src/Discord.Net.Interactions/Attributes/ComplexParameterAttribute.cs create mode 100644 src/Discord.Net.Interactions/Attributes/ComplexParameterCtorAttribute.cs create mode 100644 src/Discord.Net.Interactions/Results/ParseResult.cs diff --git a/src/Discord.Net.Interactions/Attributes/ComplexParameterAttribute.cs b/src/Discord.Net.Interactions/Attributes/ComplexParameterAttribute.cs new file mode 100644 index 000000000..952ca06a4 --- /dev/null +++ b/src/Discord.Net.Interactions/Attributes/ComplexParameterAttribute.cs @@ -0,0 +1,30 @@ +using System; + +namespace Discord.Interactions +{ + /// + /// Registers a parameter as a complex parameter. + /// + [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)] + public class ComplexParameterAttribute : Attribute + { + /// + /// Gets the parameter array of the constructor method that should be prioritized. + /// + public Type[] PrioritizedCtorSignature { get; } + + /// + /// Registers a slash command parameter as a complex parameter. + /// + public ComplexParameterAttribute() { } + + /// + /// Registers a slash command parameter as a complex parameter with a specified constructor signature. + /// + /// Type array of the preferred constructor parameters. + public ComplexParameterAttribute(Type[] types) + { + PrioritizedCtorSignature = types; + } + } +} diff --git a/src/Discord.Net.Interactions/Attributes/ComplexParameterCtorAttribute.cs b/src/Discord.Net.Interactions/Attributes/ComplexParameterCtorAttribute.cs new file mode 100644 index 000000000..59ee3377b --- /dev/null +++ b/src/Discord.Net.Interactions/Attributes/ComplexParameterCtorAttribute.cs @@ -0,0 +1,10 @@ +using System; + +namespace Discord.Interactions +{ + /// + /// Tag a type constructor as the preferred Complex command constructor. + /// + [AttributeUsage(AttributeTargets.Constructor, AllowMultiple = false, Inherited = true)] + public class ComplexParameterCtorAttribute : Attribute { } +} diff --git a/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs b/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs index 6615f131c..88a34f3b2 100644 --- a/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/ModuleClassBuilder.cs @@ -397,7 +397,6 @@ namespace Discord.Interactions.Builders builder.Description = paramInfo.Name; builder.IsRequired = !paramInfo.IsOptional; builder.DefaultValue = paramInfo.DefaultValue; - builder.SetParameterType(paramType, services); foreach (var attribute in attributes) { @@ -435,12 +434,32 @@ namespace Discord.Interactions.Builders case MinValueAttribute minValue: builder.MinValue = minValue.Value; break; + case ComplexParameterAttribute complexParameter: + { + builder.IsComplexParameter = true; + ConstructorInfo ctor = GetComplexParameterConstructor(paramInfo.ParameterType.GetTypeInfo(), complexParameter); + + foreach (var parameter in ctor.GetParameters()) + { + if (parameter.IsDefined(typeof(ComplexParameterAttribute))) + throw new InvalidOperationException("You cannot create nested complex parameters."); + + builder.AddComplexParameterField(fieldBuilder => BuildSlashParameter(fieldBuilder, parameter, services)); + } + + var initializer = builder.Command.Module.InteractionService._useCompiledLambda ? + ReflectionUtils.CreateLambdaConstructorInvoker(paramInfo.ParameterType.GetTypeInfo()) : ctor.Invoke; + builder.ComplexParameterInitializer = args => initializer(args); + } + break; default: builder.AddAttributes(attribute); break; } } + builder.SetParameterType(paramType, services); + // Replace pascal casings with '-' builder.Name = Regex.Replace(builder.Name, "(?<=[a-z])(?=[A-Z])", "-").ToLower(); } @@ -608,5 +627,41 @@ namespace Discord.Interactions.Builders propertyInfo.SetMethod?.IsStatic == false && propertyInfo.IsDefined(typeof(ModalInputAttribute)); } + + private static ConstructorInfo GetComplexParameterConstructor(TypeInfo typeInfo, ComplexParameterAttribute complexParameter) + { + var ctors = typeInfo.GetConstructors(); + + if (ctors.Length == 0) + throw new InvalidOperationException($"No constructor found for \"{typeInfo.FullName}\"."); + + if (complexParameter.PrioritizedCtorSignature is not null) + { + var ctor = typeInfo.GetConstructor(complexParameter.PrioritizedCtorSignature); + + if (ctor is null) + throw new InvalidOperationException($"No constructor was found with the signature: {string.Join(",", complexParameter.PrioritizedCtorSignature.Select(x => x.Name))}"); + + return ctor; + } + + var prioritizedCtors = ctors.Where(x => x.IsDefined(typeof(ComplexParameterCtorAttribute), true)); + + switch (prioritizedCtors.Count()) + { + case > 1: + throw new InvalidOperationException($"{nameof(ComplexParameterCtorAttribute)} can only be used once in a type."); + case 1: + return prioritizedCtors.First(); + } + + switch (ctors.Length) + { + case > 1: + throw new InvalidOperationException($"Multiple constructors found for \"{typeInfo.FullName}\"."); + default: + return ctors.First(); + } + } } } diff --git a/src/Discord.Net.Interactions/Builders/Parameters/SlashCommandParameterBuilder.cs b/src/Discord.Net.Interactions/Builders/Parameters/SlashCommandParameterBuilder.cs index c208a4b0e..d600c9cc7 100644 --- a/src/Discord.Net.Interactions/Builders/Parameters/SlashCommandParameterBuilder.cs +++ b/src/Discord.Net.Interactions/Builders/Parameters/SlashCommandParameterBuilder.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; namespace Discord.Interactions.Builders { @@ -10,6 +11,7 @@ namespace Discord.Interactions.Builders { private readonly List _choices = new(); private readonly List _channelTypes = new(); + private readonly List _complexParameterFields = new(); /// /// Gets or sets the description of this parameter. @@ -36,6 +38,11 @@ namespace Discord.Interactions.Builders /// public IReadOnlyCollection ChannelTypes => _channelTypes; + /// + /// Gets the constructor parameters of this parameter, if is . + /// + public IReadOnlyCollection ComplexParameterFields => _complexParameterFields; + /// /// Gets or sets whether this parameter should be configured for Autocomplete Interactions. /// @@ -46,6 +53,16 @@ namespace Discord.Interactions.Builders /// public TypeConverter TypeConverter { get; private set; } + /// + /// Gets whether this type should be treated as a complex parameter. + /// + public bool IsComplexParameter { get; internal set; } + + /// + /// Gets the initializer delegate for this parameter, if is . + /// + public ComplexParameterInitializer ComplexParameterInitializer { get; internal set; } + /// /// Gets or sets the of this parameter. /// @@ -60,7 +77,14 @@ namespace Discord.Interactions.Builders /// Parent command of this parameter. /// Name of this command. /// Type of this parameter. - public SlashCommandParameterBuilder(ICommandBuilder command, string name, Type type) : base(command, name, type) { } + public SlashCommandParameterBuilder(ICommandBuilder command, string name, Type type, ComplexParameterInitializer complexParameterInitializer = null) + : base(command, name, type) + { + ComplexParameterInitializer = complexParameterInitializer; + + if (complexParameterInitializer is not null) + IsComplexParameter = true; + } /// /// Sets . @@ -168,7 +192,47 @@ namespace Discord.Interactions.Builders public SlashCommandParameterBuilder SetParameterType(Type type, IServiceProvider services = null) { base.SetParameterType(type); - TypeConverter = Command.Module.InteractionService.GetTypeConverter(ParameterType, services); + + if(!IsComplexParameter) + TypeConverter = Command.Module.InteractionService.GetTypeConverter(ParameterType, services); + + return this; + } + + /// + /// Adds a parameter builders to . + /// + /// factory. + /// + /// The builder instance. + /// + /// Thrown if the added field has a . + public SlashCommandParameterBuilder AddComplexParameterField(Action configure) + { + SlashCommandParameterBuilder builder = new(Command); + configure(builder); + + if(builder.IsComplexParameter) + throw new InvalidOperationException("You cannot create nested complex parameters."); + + _complexParameterFields.Add(builder); + return this; + } + + /// + /// Adds parameter builders to . + /// + /// New parameter builders to be added to . + /// + /// The builder instance. + /// + /// Thrown if the added field has a . + public SlashCommandParameterBuilder AddComplexParameterFields(params SlashCommandParameterBuilder[] fields) + { + if(fields.Any(x => x.IsComplexParameter)) + throw new InvalidOperationException("You cannot create nested complex parameters."); + + _complexParameterFields.AddRange(fields); return this; } diff --git a/src/Discord.Net.Interactions/Info/Commands/CommandInfo.cs b/src/Discord.Net.Interactions/Info/Commands/CommandInfo.cs index cf1a2dfa1..49ad009c9 100644 --- a/src/Discord.Net.Interactions/Info/Commands/CommandInfo.cs +++ b/src/Discord.Net.Interactions/Info/Commands/CommandInfo.cs @@ -31,6 +31,8 @@ namespace Discord.Interactions private readonly ExecuteCallback _action; private readonly ILookup _groupedPreconditions; + internal IReadOnlyDictionary _parameterDictionary { get; } + /// public ModuleInfo Module { get; } @@ -79,6 +81,7 @@ namespace Discord.Interactions _action = builder.Callback; _groupedPreconditions = builder.Preconditions.ToLookup(x => x.Group, x => x, StringComparer.Ordinal); + _parameterDictionary = Parameters?.ToDictionary(x => x.Name, x => x).ToImmutableDictionary(); } /// diff --git a/src/Discord.Net.Interactions/Info/Commands/SlashCommandInfo.cs b/src/Discord.Net.Interactions/Info/Commands/SlashCommandInfo.cs index 116a07ab4..456ad4bfe 100644 --- a/src/Discord.Net.Interactions/Info/Commands/SlashCommandInfo.cs +++ b/src/Discord.Net.Interactions/Info/Commands/SlashCommandInfo.cs @@ -13,6 +13,8 @@ namespace Discord.Interactions /// public class SlashCommandInfo : CommandInfo, IApplicationCommandInfo { + internal IReadOnlyDictionary _flattenedParameterDictionary { get; } + /// /// Gets the command description that will be displayed on Discord. /// @@ -30,11 +32,23 @@ namespace Discord.Interactions /// public override bool SupportsWildCards => false; + /// + /// Gets the flattened collection of command parameters and complex parameter fields. + /// + public IReadOnlyCollection FlattenedParameters { get; } + internal SlashCommandInfo (Builders.SlashCommandBuilder builder, ModuleInfo module, InteractionService commandService) : base(builder, module, commandService) { Description = builder.Description; DefaultPermission = builder.DefaultPermission; Parameters = builder.Parameters.Select(x => x.Build(this)).ToImmutableArray(); + FlattenedParameters = FlattenParameters(Parameters).ToImmutableArray(); + + for (var i = 0; i < FlattenedParameters.Count - 1; i++) + if (!FlattenedParameters.ElementAt(i).IsRequired && FlattenedParameters.ElementAt(i + 1).IsRequired) + throw new InvalidOperationException("Optional parameters must appear after all required parameters, ComplexParameters with optional parameters must be located at the end."); + + _flattenedParameterDictionary = FlattenedParameters?.ToDictionary(x => x.Name, x => x).ToImmutableDictionary(); } /// @@ -56,45 +70,81 @@ namespace Discord.Interactions { try { - if (paramList?.Count() < argList?.Count()) - return ExecuteResult.FromError(InteractionCommandError.BadArgs ,"Command was invoked with too many parameters"); - var args = new object[paramList.Count()]; for (var i = 0; i < paramList.Count(); i++) { var parameter = paramList.ElementAt(i); - var arg = argList?.Find(x => string.Equals(x.Name, parameter.Name, StringComparison.OrdinalIgnoreCase)); + var result = await ParseArgument(parameter, context, argList, services).ConfigureAwait(false); - if (arg == default) + if(!result.IsSuccess) { - if (parameter.IsRequired) - return ExecuteResult.FromError(InteractionCommandError.BadArgs, "Command was invoked with too few parameters"); - else - args[i] = parameter.DefaultValue; + var execResult = ExecuteResult.FromError(result); + await InvokeModuleEvent(context, execResult).ConfigureAwait(false); + return execResult; } + + if (result is ParseResult parseResult) + args[i] = parseResult.Value; else - { - var typeConverter = parameter.TypeConverter; + return ExecuteResult.FromError(InteractionCommandError.BadArgs, "Command parameter parsing failed for an unknown reason."); + } - var readResult = await typeConverter.ReadAsync(context, arg, services).ConfigureAwait(false); + return await RunAsync(context, args, services).ConfigureAwait(false); + } + catch (Exception ex) + { + var result = ExecuteResult.FromError(ex); + await InvokeModuleEvent(context, result).ConfigureAwait(false); + return result; + } + } - if (!readResult.IsSuccess) - { - await InvokeModuleEvent(context, readResult).ConfigureAwait(false); - return readResult; - } + private async Task ParseArgument(SlashCommandParameterInfo parameterInfo, IInteractionContext context, List argList, + IServiceProvider services) + { + if (parameterInfo.IsComplexParameter) + { + var ctorArgs = new object[parameterInfo.ComplexParameterFields.Count]; - args[i] = readResult.Value; - } + for (var i = 0; i < ctorArgs.Length; i++) + { + var result = await ParseArgument(parameterInfo.ComplexParameterFields.ElementAt(i), context, argList, services).ConfigureAwait(false); + + if (!result.IsSuccess) + return result; + + if (result is ParseResult parseResult) + ctorArgs[i] = parseResult.Value; + else + return ExecuteResult.FromError(InteractionCommandError.BadArgs, "Complex command parsing failed for an unknown reason."); } - return await RunAsync(context, args, services).ConfigureAwait(false); + return ParseResult.FromSuccess(parameterInfo._complexParameterInitializer(ctorArgs)); } - catch (Exception ex) + else { - return ExecuteResult.FromError(ex); + var arg = argList?.Find(x => string.Equals(x.Name, parameterInfo.Name, StringComparison.OrdinalIgnoreCase)); + + if (arg == default) + { + if (parameterInfo.IsRequired) + return ExecuteResult.FromError(InteractionCommandError.BadArgs, "Command was invoked with too few parameters"); + else + return ParseResult.FromSuccess(parameterInfo.DefaultValue); + } + else + { + var typeConverter = parameterInfo.TypeConverter; + + var readResult = await typeConverter.ReadAsync(context, arg, services).ConfigureAwait(false); + + if (!readResult.IsSuccess) + return readResult; + + return ParseResult.FromSuccess(readResult.Value); + } } } @@ -108,5 +158,15 @@ namespace Discord.Interactions else return $"Slash Command: \"{base.ToString()}\" for {context.User} in {context.Channel}"; } + + private static IEnumerable FlattenParameters(IEnumerable parameters) + { + foreach (var parameter in parameters) + if (!parameter.IsComplexParameter) + yield return parameter; + else + foreach(var complexParameterField in parameter.ComplexParameterFields) + yield return complexParameterField; + } } } diff --git a/src/Discord.Net.Interactions/Info/Parameters/SlashCommandParameterInfo.cs b/src/Discord.Net.Interactions/Info/Parameters/SlashCommandParameterInfo.cs index 68b63c806..8702d69f7 100644 --- a/src/Discord.Net.Interactions/Info/Parameters/SlashCommandParameterInfo.cs +++ b/src/Discord.Net.Interactions/Info/Parameters/SlashCommandParameterInfo.cs @@ -1,13 +1,25 @@ using System.Collections.Generic; using System.Collections.Immutable; +using System.Linq; namespace Discord.Interactions { + /// + /// Represents a cached argument constructor delegate. + /// + /// Method arguments array. + /// + /// Returns the constructed object. + /// + public delegate object ComplexParameterInitializer(object[] args); + /// /// Represents the parameter info class for commands. /// public class SlashCommandParameterInfo : CommandParameterInfo { + internal readonly ComplexParameterInitializer _complexParameterInitializer; + /// public new SlashCommandInfo Command => base.Command as SlashCommandInfo; @@ -43,9 +55,14 @@ namespace Discord.Interactions public bool IsAutocomplete { get; } /// - /// Gets the Discord option type this parameter represents. + /// Gets whether this type should be treated as a complex parameter. /// - public ApplicationCommandOptionType DiscordOptionType => TypeConverter.GetDiscordType(); + public bool IsComplexParameter { get; } + + /// + /// Gets the Discord option type this parameter represents. If the parameter is not a complex parameter. + /// + public ApplicationCommandOptionType? DiscordOptionType => TypeConverter?.GetDiscordType(); /// /// Gets the parameter choices of this Slash Application Command parameter. @@ -57,6 +74,11 @@ namespace Discord.Interactions /// public IReadOnlyCollection ChannelTypes { get; } + /// + /// Gets the constructor parameters of this parameter, if is . + /// + public IReadOnlyCollection ComplexParameterFields { get; } + internal SlashCommandParameterInfo(Builders.SlashCommandParameterBuilder builder, SlashCommandInfo command) : base(builder, command) { TypeConverter = builder.TypeConverter; @@ -64,9 +86,13 @@ namespace Discord.Interactions Description = builder.Description; MaxValue = builder.MaxValue; MinValue = builder.MinValue; + IsComplexParameter = builder.IsComplexParameter; IsAutocomplete = builder.Autocomplete; Choices = builder.Choices.ToImmutableArray(); ChannelTypes = builder.ChannelTypes.ToImmutableArray(); + ComplexParameterFields = builder.ComplexParameterFields?.Select(x => x.Build(command)).ToImmutableArray(); + + _complexParameterInitializer = builder.ComplexParameterInitializer; } } } diff --git a/src/Discord.Net.Interactions/InteractionService.cs b/src/Discord.Net.Interactions/InteractionService.cs index c1291bd6b..bf56eddc5 100644 --- a/src/Discord.Net.Interactions/InteractionService.cs +++ b/src/Discord.Net.Interactions/InteractionService.cs @@ -747,9 +747,7 @@ namespace Discord.Interactions if(autocompleteHandlerResult.IsSuccess) { - var parameter = autocompleteHandlerResult.Command.Parameters.FirstOrDefault(x => string.Equals(x.Name, interaction.Data.Current.Name, StringComparison.Ordinal)); - - if(parameter?.AutocompleteHandler is not null) + if (autocompleteHandlerResult.Command._flattenedParameterDictionary.TryGetValue(interaction.Data.Current.Name, out var parameter) && parameter?.AutocompleteHandler is not null) return await parameter.AutocompleteHandler.ExecuteAsync(context, interaction, parameter, services).ConfigureAwait(false); } } diff --git a/src/Discord.Net.Interactions/Results/ParseResult.cs b/src/Discord.Net.Interactions/Results/ParseResult.cs new file mode 100644 index 000000000..dfc6a57fe --- /dev/null +++ b/src/Discord.Net.Interactions/Results/ParseResult.cs @@ -0,0 +1,36 @@ +using System; + +namespace Discord.Interactions +{ + internal struct ParseResult : IResult + { + public object Value { get; } + + public InteractionCommandError? Error { get; } + + public string ErrorReason { get; } + + public bool IsSuccess => !Error.HasValue; + + private ParseResult(object value, InteractionCommandError? error, string reason) + { + Value = value; + Error = error; + ErrorReason = reason; + } + + public static ParseResult FromSuccess(object value) => + new ParseResult(value, null, null); + + public static ParseResult FromError(Exception exception) => + new ParseResult(null, InteractionCommandError.Exception, exception.Message); + + public static ParseResult FromError(InteractionCommandError error, string reason) => + new ParseResult(null, error, reason); + + public static ParseResult FromError(IResult result) => + new ParseResult(null, result.Error, result.ErrorReason); + + public override string ToString() => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; + } +} diff --git a/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs b/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs index 48b6e44e7..46f0f4a4a 100644 --- a/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs +++ b/src/Discord.Net.Interactions/Utilities/ApplicationCommandRestUtil.cs @@ -13,7 +13,7 @@ namespace Discord.Interactions { Name = parameterInfo.Name, Description = parameterInfo.Description, - Type = parameterInfo.DiscordOptionType, + Type = parameterInfo.DiscordOptionType.Value, IsRequired = parameterInfo.IsRequired, Choices = parameterInfo.Choices?.Select(x => new ApplicationCommandOptionChoiceProperties { @@ -46,7 +46,7 @@ namespace Discord.Interactions if (commandInfo.Parameters.Count > SlashCommandBuilder.MaxOptionsCount) throw new InvalidOperationException($"Slash Commands cannot have more than {SlashCommandBuilder.MaxOptionsCount} command parameters"); - props.Options = commandInfo.Parameters.Select(x => x.ToApplicationCommandOptionProps())?.ToList() ?? Optional>.Unspecified; + props.Options = commandInfo.FlattenedParameters.Select(x => x.ToApplicationCommandOptionProps())?.ToList() ?? Optional>.Unspecified; return props; } @@ -58,7 +58,7 @@ namespace Discord.Interactions Description = commandInfo.Description, Type = ApplicationCommandOptionType.SubCommand, IsRequired = false, - Options = commandInfo.Parameters?.Select(x => x.ToApplicationCommandOptionProps())?.ToList() + Options = commandInfo.FlattenedParameters?.Select(x => x.ToApplicationCommandOptionProps())?.ToList() }; public static ApplicationCommandProperties ToApplicationCommandProps(this ContextCommandInfo commandInfo) From c80067425a6e059c9862c40b47772e056e0b0eda Mon Sep 17 00:00:00 2001 From: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Wed, 2 Mar 2022 19:23:51 -0400 Subject: [PATCH 27/35] Display name support for enum type converter (#2156) * Display name support for enum type converter * allow display attribute on enum type converter * update docs/examples to include enum Display sample * Revert "allow display attribute on enum type converter" This reverts commit a0eec5b7555d366f9de7421f6fcf6bc71f2a4557. * adds ChoiceDisplay for enum type converters * Update EnumChoiceAttribute.cs * fix renamed folder issue * fix namespace Co-authored-by: Xeno --- .../samples/intro/groupattribute.cs | 8 +++--- samples/InteractionFramework/ExampleEnum.cs | 12 ++++++++- .../Attributes/EnumChoiceAttribute.cs | 25 +++++++++++++++++++ .../TypeConverters/EnumConverter.cs | 7 ++++-- 4 files changed, 46 insertions(+), 6 deletions(-) create mode 100644 src/Discord.Net.Interactions/Attributes/EnumChoiceAttribute.cs diff --git a/docs/guides/int_framework/samples/intro/groupattribute.cs b/docs/guides/int_framework/samples/intro/groupattribute.cs index 86a492c31..99d6cd67b 100644 --- a/docs/guides/int_framework/samples/intro/groupattribute.cs +++ b/docs/guides/int_framework/samples/intro/groupattribute.cs @@ -1,16 +1,18 @@ [SlashCommand("blep", "Send a random adorable animal photo")] -public async Task Blep([Choice("Dog", "dog"), Choice("Cat", "cat"), Choice("Penguin", "penguin")] string animal) +public async Task Blep([Choice("Dog", "dog"), Choice("Cat", "cat"), Choice("Guinea pig", "GuineaPig")] string animal) { ... } -// In most cases, you can use an enum to replace the seperate choice attributes in a command. +// In most cases, you can use an enum to replace the separate choice attributes in a command. public enum Animal { Cat, Dog, - Penguin + // You can also use the ChoiceDisplay attribute to change how they appear in the choice menu. + [ChoiceDisplay("Guinea pig")] + GuineaPig } [SlashCommand("blep", "Send a random adorable animal photo")] diff --git a/samples/InteractionFramework/ExampleEnum.cs b/samples/InteractionFramework/ExampleEnum.cs index 755f33d17..a70dd49a9 100644 --- a/samples/InteractionFramework/ExampleEnum.cs +++ b/samples/InteractionFramework/ExampleEnum.cs @@ -1,3 +1,11 @@ +using Discord.Interactions; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + namespace InteractionFramework { public enum ExampleEnum @@ -5,6 +13,8 @@ namespace InteractionFramework First, Second, Third, - Fourth + Fourth, + [ChoiceDisplay("Twenty First")] + TwentyFirst } } diff --git a/src/Discord.Net.Interactions/Attributes/EnumChoiceAttribute.cs b/src/Discord.Net.Interactions/Attributes/EnumChoiceAttribute.cs new file mode 100644 index 000000000..c7f83b6cd --- /dev/null +++ b/src/Discord.Net.Interactions/Attributes/EnumChoiceAttribute.cs @@ -0,0 +1,25 @@ +using System; + +namespace Discord.Interactions +{ + /// + /// Customize the displayed value of a slash command choice enum. Only works with the default enum type converter. + /// + [AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = true)] + public class ChoiceDisplayAttribute : Attribute + { + /// + /// Gets the name of the parameter. + /// + public string Name { get; } = null; + + /// + /// Modify the default name and description values of a Slash Command parameter. + /// + /// Name of the parameter. + public ChoiceDisplayAttribute(string name) + { + Name = name; + } + } +} diff --git a/src/Discord.Net.Interactions/TypeConverters/EnumConverter.cs b/src/Discord.Net.Interactions/TypeConverters/EnumConverter.cs index a06c70ec4..1406c6f1a 100644 --- a/src/Discord.Net.Interactions/TypeConverters/EnumConverter.cs +++ b/src/Discord.Net.Interactions/TypeConverters/EnumConverter.cs @@ -2,6 +2,7 @@ using Discord.WebSocket; using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using System.Threading.Tasks; namespace Discord.Interactions @@ -27,12 +28,14 @@ namespace Discord.Interactions var choices = new List(); foreach (var member in members) + { + var displayValue = member.GetCustomAttribute()?.Name ?? member.Name; choices.Add(new ApplicationCommandOptionChoiceProperties { - Name = member.Name, + Name = displayValue, Value = member.Name }); - + } properties.Choices = choices; } } From 507a18d389abb5931e1b4d0dcca06694ba3a6258 Mon Sep 17 00:00:00 2001 From: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Wed, 2 Mar 2022 19:24:00 -0400 Subject: [PATCH 28/35] Enforce valid button styles (#2157) Co-authored-by: CottageDwellingCat <80918250+CottageDwellingCat@users.noreply.github.com> --- .../Interactions/MessageComponents/ComponentBuilder.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs index 0fa8189c1..7becca0e0 100644 --- a/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs +++ b/src/Discord.Net.Core/Entities/Interactions/MessageComponents/ComponentBuilder.cs @@ -613,6 +613,9 @@ namespace Discord if (!(string.IsNullOrEmpty(Url) ^ string.IsNullOrEmpty(CustomId))) throw new InvalidOperationException("A button must contain either a URL or a CustomId, but not both!"); + if (Style == 0) + throw new ArgumentException("A button must have a style.", nameof(Style)); + if (Style == ButtonStyle.Link) { if (string.IsNullOrEmpty(Url)) From 36d6ce9ec8777cc49049fb814fdbfee1c05aa5f4 Mon Sep 17 00:00:00 2001 From: Quin Lynch <49576606+quinchs@users.noreply.github.com> Date: Wed, 2 Mar 2022 19:24:10 -0400 Subject: [PATCH 29/35] Unneeded build event (#2158) Build() at the end of the command creation isn't needed. The build is done on line 34. Co-authored-by: Cookiezzz --- .../application-commands/slash-commands/choice-slash-command.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/int_basics/application-commands/slash-commands/choice-slash-command.md b/docs/guides/int_basics/application-commands/slash-commands/choice-slash-command.md index 3951e1141..46805eb7f 100644 --- a/docs/guides/int_basics/application-commands/slash-commands/choice-slash-command.md +++ b/docs/guides/int_basics/application-commands/slash-commands/choice-slash-command.md @@ -27,7 +27,7 @@ private async Task Client_Ready() .AddChoice("Lovely", 4) .AddChoice("Excellent!", 5) .WithType(ApplicationCommandOptionType.Integer) - ).Build(); + ); try { From 5522bc443dbd7bc006730d1a16cf6bed88ddc525 Mon Sep 17 00:00:00 2001 From: Cenk Ergen <57065323+Cenngo@users.noreply.github.com> Date: Thu, 3 Mar 2022 03:02:12 +0300 Subject: [PATCH 30/35] Create Complex Params Docs (#2160) * create complex params docs * Update docs/guides/int_framework/intro.md Co-authored-by: Quin Lynch <49576606+quinchs@users.noreply.github.com> --- docs/guides/int_framework/intro.md | 15 ++++++++ .../samples/intro/complexparams.cs | 37 +++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 docs/guides/int_framework/samples/intro/complexparams.cs diff --git a/docs/guides/int_framework/intro.md b/docs/guides/int_framework/intro.md index 0a5cc19f1..764a100fe 100644 --- a/docs/guides/int_framework/intro.md +++ b/docs/guides/int_framework/intro.md @@ -143,6 +143,21 @@ In this case, user can only input Stage Channels and Text Channels to this param You can specify the permitted max/min value for a number type parameter using the [MaxValueAttribute] and [MinValueAttribute]. +#### Complex Parameters + +This allows users to create slash command options using an object's constructor allowing complex objects to be created which cannot be infered from only one input value. +Constructor methods support every attribute type that can be used with the regular slash commands ([Autocomplete], [Summary] etc. ). +Preferred constructor of a Type can be specified either by passing a `Type[]` to the `[ComplexParameterAttribute]` or tagging a type constructor with the `[ComplexParameterCtorAttribute]`. If nothing is specified, the InteractionService defaults to the only public constructor of the type. +TypeConverter pattern is used to parse the constructor methods objects. + +[!code-csharp[Complex Parameter](samples/intro/usercommand.cs)] + +Interaction service complex parameter constructors are prioritized in the following order: + +1. Constructor matching the signature provided in the `[ComplexParameter(Type[])]` overload. +2. Constuctor tagged with `[ComplexParameterCtor]`. +3. Type's only public constuctor. + ## User Commands A valid User Command must have the following structure: diff --git a/docs/guides/int_framework/samples/intro/complexparams.cs b/docs/guides/int_framework/samples/intro/complexparams.cs new file mode 100644 index 000000000..72c0616cc --- /dev/null +++ b/docs/guides/int_framework/samples/intro/complexparams.cs @@ -0,0 +1,37 @@ +public class Vector3 +{ + public int X {get;} + public int Y {get;} + public int Z {get;} + + public Vector3() + { + X = 0; + Y = 0; + Z = 0; + } + + [ComplexParameterCtor] + public Vector3(int x, int y, int z) + { + X = x; + Y = y; + Z = z; + } +} + +// Both of the commands below are displayed to the users identically. + +// With complex parameter +[SlashCommand("create-vector", "Create a 3D vector.")] +public async Task CreateVector([ComplexParameter]Vector3 vector3) +{ + ... +} + +// Without complex parameter +[SlashCommand("create-vector", "Create a 3D vector.")] +public async Task CreateVector(int x, int y, int z) +{ + ... +} \ No newline at end of file From 50d0000e260e65fbbcd0af9f0487fcc86327c99e Mon Sep 17 00:00:00 2001 From: Quin Lynch Date: Wed, 2 Mar 2022 20:13:58 -0400 Subject: [PATCH 31/35] meta: 3.4.0 --- CHANGELOG.md | 23 +++++++++++ Discord.Net.targets | 2 +- docs/docfx.json | 2 +- src/Discord.Net/Discord.Net.nuspec | 62 +++++++++++++++--------------- 4 files changed, 56 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ba61ed83..416f2ec6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,28 @@ # Changelog +## [3.4.0] - 2022-3-2 + +## Added +- #2146 Add FromDateTimeOffset in TimestampTag (553055b) +- #2062 Add return statement to precondition handling (3e52fab) +- #2131 Add support for sending Message Flags (1fb62de) +- #2137 Add self_video to VoiceState (8bcd3da) +- #2151 Add Image property to Guild Scheduled Events (1dc473c) +- #2152 Add missing json error codes (202554f) +- #2153 Add IsInvitable and CreatedAt to threads (6bf5818) +- #2155 Add Interaction Service Complex Parameters (9ba64f6) +- #2156 Add Display name support for enum type converter (c800674) + +## Fixed +- #2117 Fix stream access exception when ratelimited (a1cfa41) +- #2128 Fix context menu comand message type (f601e9b) +- #2135 Fix NRE when ratelimmited requests don't return a body (b95b942) +- #2154 Fix usage of CacheMode.AllowDownload in channels (b3370c3) + +## Misc +- #2149 Clarify Users property on SocketGuildChannel (5594739) +- #2157 Enforce valid button styles (507a18d) + ## [3.3.2] - 2022-02-16 ### Fixed diff --git a/Discord.Net.targets b/Discord.Net.targets index 1b6a19c72..d0e17b3c5 100644 --- a/Discord.Net.targets +++ b/Discord.Net.targets @@ -1,6 +1,6 @@ - 3.3.2 + 3.4.0 latest Discord.Net Contributors discord;discordapp diff --git a/docs/docfx.json b/docs/docfx.json index c0821ce5d..2ad0164f4 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -60,7 +60,7 @@ "overwrite": "_overwrites/**/**.md", "globalMetadata": { "_appTitle": "Discord.Net Documentation", - "_appFooter": "Discord.Net (c) 2015-2022 3.3.2", + "_appFooter": "Discord.Net (c) 2015-2022 3.4.0", "_enableSearch": true, "_appLogoPath": "marketing/logo/SVG/Logomark Purple.svg", "_appFaviconPath": "favicon.ico" diff --git a/src/Discord.Net/Discord.Net.nuspec b/src/Discord.Net/Discord.Net.nuspec index dec25413c..d98287ffa 100644 --- a/src/Discord.Net/Discord.Net.nuspec +++ b/src/Discord.Net/Discord.Net.nuspec @@ -2,7 +2,7 @@ Discord.Net - 3.3.2$suffix$ + 3.4.0$suffix$ Discord.Net Discord.Net Contributors foxbot @@ -14,44 +14,44 @@ https://github.com/RogueException/Discord.Net/raw/dev/docs/marketing/logo/PackageLogo.png - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + From 1ba96d6fbda0856f8b3c095a1e66a7e2b8c46aca Mon Sep 17 00:00:00 2001 From: MrCakeSlayer <13650699+MrCakeSlayer@users.noreply.github.com> Date: Wed, 2 Mar 2022 20:30:17 -0500 Subject: [PATCH 32/35] Add configuration toggle to suppress Unknown dispatch warnings (#2162) --- src/Discord.Net.WebSocket/DiscordSocketClient.cs | 4 +++- src/Discord.Net.WebSocket/DiscordSocketConfig.cs | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index cd40a491f..b692f0691 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -78,6 +78,7 @@ namespace Discord.WebSocket internal bool AlwaysDownloadDefaultStickers { get; private set; } internal bool AlwaysResolveStickers { get; private set; } internal bool LogGatewayIntentWarnings { get; private set; } + internal bool SuppressUnknownDispatchWarnings { get; private set; } internal new DiscordSocketApiClient ApiClient => base.ApiClient; /// public override IReadOnlyCollection Guilds => State.Guilds; @@ -150,6 +151,7 @@ namespace Discord.WebSocket AlwaysDownloadDefaultStickers = config.AlwaysDownloadDefaultStickers; AlwaysResolveStickers = config.AlwaysResolveStickers; LogGatewayIntentWarnings = config.LogGatewayIntentWarnings; + SuppressUnknownDispatchWarnings = config.SuppressUnknownDispatchWarnings; HandlerTimeout = config.HandlerTimeout; State = new ClientState(0, 0); Rest = new DiscordSocketRestClient(config, ApiClient); @@ -2771,7 +2773,7 @@ namespace Discord.WebSocket #region Others default: - await _gatewayLogger.WarningAsync($"Unknown Dispatch ({type})").ConfigureAwait(false); + if(!SuppressUnknownDispatchWarnings) await _gatewayLogger.WarningAsync($"Unknown Dispatch ({type})").ConfigureAwait(false); break; #endregion } diff --git a/src/Discord.Net.WebSocket/DiscordSocketConfig.cs b/src/Discord.Net.WebSocket/DiscordSocketConfig.cs index f0e6dc857..4cd64dbc2 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketConfig.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketConfig.cs @@ -188,6 +188,11 @@ namespace Discord.WebSocket /// public bool LogGatewayIntentWarnings { get; set; } = true; + /// + /// Gets or sets whether or not Unknown Dispatch event messages should be logged. + /// + public bool SuppressUnknownDispatchWarnings { get; set; } = true; + /// /// Initializes a new instance of the class with the default configuration. /// From 72629906541619bef58781f0e473c0d69f136e8b Mon Sep 17 00:00:00 2001 From: Armano den Boef <68127614+Rozen4334@users.noreply.github.com> Date: Thu, 3 Mar 2022 22:19:25 +0100 Subject: [PATCH 33/35] Resolve complex param sample reference (#2166) --- docs/guides/int_framework/intro.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/int_framework/intro.md b/docs/guides/int_framework/intro.md index 764a100fe..abea2a735 100644 --- a/docs/guides/int_framework/intro.md +++ b/docs/guides/int_framework/intro.md @@ -150,7 +150,7 @@ Constructor methods support every attribute type that can be used with the regul Preferred constructor of a Type can be specified either by passing a `Type[]` to the `[ComplexParameterAttribute]` or tagging a type constructor with the `[ComplexParameterCtorAttribute]`. If nothing is specified, the InteractionService defaults to the only public constructor of the type. TypeConverter pattern is used to parse the constructor methods objects. -[!code-csharp[Complex Parameter](samples/intro/usercommand.cs)] +[!code-csharp[Complex Parameter](samples/intro/complexparams.cs)] Interaction service complex parameter constructors are prioritized in the following order: From 48bc723f9e7ca01e479115f89fff8ddc4c2135ea Mon Sep 17 00:00:00 2001 From: KeylAmi Date: Thu, 3 Mar 2022 16:20:02 -0500 Subject: [PATCH 34/35] Update bugreport.yml (#2159) * Update bugreport.yml * Update bugreport.yml removed d.net reference. fixed spelling. * Update bugreport.yml Adjusted verbiage for clarity --- .github/ISSUE_TEMPLATE/bugreport.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/bugreport.yml b/.github/ISSUE_TEMPLATE/bugreport.yml index d130991bf..e2c154130 100644 --- a/.github/ISSUE_TEMPLATE/bugreport.yml +++ b/.github/ISSUE_TEMPLATE/bugreport.yml @@ -76,3 +76,11 @@ body: ``` validations: required: false + - type: textarea + id: packages + attributes: + label: Packages + description: Please list all 3rd party packages in use if applicable, including their versions. + placeholder: Discord.Addons.Hosting V5.1.0, Discord.InteractivityAddon V2.4.0, etc. + validations: + required: true From a5d3add1d69618b4eb6ab9edb2ae2988b93ce231 Mon Sep 17 00:00:00 2001 From: Brendan McShane Date: Thu, 3 Mar 2022 16:20:34 -0500 Subject: [PATCH 35/35] Fix error with flag params. (#2165) --- src/Discord.Net.Rest/API/Rest/UploadFileParams.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.Rest/API/Rest/UploadFileParams.cs b/src/Discord.Net.Rest/API/Rest/UploadFileParams.cs index 6340c3e38..67a690e4d 100644 --- a/src/Discord.Net.Rest/API/Rest/UploadFileParams.cs +++ b/src/Discord.Net.Rest/API/Rest/UploadFileParams.cs @@ -51,7 +51,7 @@ namespace Discord.API.Rest if (Stickers.IsSpecified) payload["sticker_ids"] = Stickers.Value; if (Flags.IsSpecified) - payload["flags"] = Flags; + payload["flags"] = Flags.Value; List attachments = new();