From 1f01a2d1fad0fe8d48fab267c116467514af0c5a Mon Sep 17 00:00:00 2001 From: Min Date: Wed, 22 Apr 2020 16:02:22 +1000 Subject: [PATCH 01/19] fix: nullcheck _shards before iterating (#1493) --- src/Discord.Net.WebSocket/DiscordShardedClient.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net.WebSocket/DiscordShardedClient.cs b/src/Discord.Net.WebSocket/DiscordShardedClient.cs index 0877abfd9..8359ca048 100644 --- a/src/Discord.Net.WebSocket/DiscordShardedClient.cs +++ b/src/Discord.Net.WebSocket/DiscordShardedClient.cs @@ -389,8 +389,11 @@ namespace Discord.WebSocket { if (disposing) { - foreach (var client in _shards) - client?.Dispose(); + if (_shards != null) + { + foreach (var client in _shards) + client?.Dispose(); + } _connectionGroupLock?.Dispose(); } From 106f346ddb8ada70ad2227d12e13be58d1234a17 Mon Sep 17 00:00:00 2001 From: Still Hsu <5843208+Still34@users.noreply.github.com> Date: Wed, 22 Apr 2020 14:04:10 +0800 Subject: [PATCH 02/19] docs: 2020 April Documentation Maintenance (#1484) * Add doc page for Named Arguments * Implement minor stylistic changes * Update docfx.json to support NS2.0 Signed-off-by: Still Hsu <5843208+Still34@users.noreply.github.com> * Fix broken xref in basic-operations * Fix broken crefs * Fix wordings in named argument * Fix misleading warning about long-running code * Fix misleading CommandService summary Signed-off-by: Still Hsu <5843208+Still34@users.noreply.github.com> * Update copyright year and version Signed-off-by: Still Hsu <5843208+Still34@users.noreply.github.com> * Escape example captions * Add warning regarding FlattenAsync for GetReactionUsersAsync * Fix a minor grammar mistake Co-authored-by: Joe4evr --- docs/docfx.json | 4 +- docs/faq/basics/basic-operations.md | 2 +- docs/guides/commands/intro.md | 8 +- docs/guides/commands/namedarguments.md | 79 +++++++++++++++++++ docs/guides/toc.yml | 2 + src/Discord.Net.Commands/CommandService.cs | 2 +- .../Entities/Channels/INestedChannel.cs | 8 +- .../Entities/Channels/ITextChannel.cs | 4 +- .../Entities/Guilds/IGuild.cs | 2 +- .../Entities/Messages/IMessage.cs | 29 +++++-- .../Entities/Messages/IUserMessage.cs | 2 +- .../Entities/Users/IGuildUser.cs | 4 +- src/Discord.Net.Core/Entities/Users/IUser.cs | 8 +- .../Extensions/MessageExtensions.cs | 6 +- .../Extensions/UserExtensions.cs | 4 +- src/Discord.Net.Core/IDiscordClient.cs | 2 +- src/Discord.Net.Core/RequestOptions.cs | 3 +- .../DataTypes/ChannelCreateAuditLogData.cs | 4 +- .../DataTypes/ChannelDeleteAuditLogData.cs | 4 +- .../AuditLogs/DataTypes/ChannelInfo.cs | 4 +- .../DiscordWebhookClient.cs | 2 +- 21 files changed, 139 insertions(+), 44 deletions(-) create mode 100644 docs/guides/commands/namedarguments.md diff --git a/docs/docfx.json b/docs/docfx.json index 17cf72388..759dc174f 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -9,7 +9,7 @@ "dest": "api", "filter": "filterConfig.yml", "properties": { - "TargetFramework": "netstandard1.3" + "TargetFramework": "netstandard2.0" } }], "build": { @@ -51,7 +51,7 @@ "overwrite": "_overwrites/**/**.md", "globalMetadata": { "_appTitle": "Discord.Net Documentation", - "_appFooter": "Discord.Net (c) 2015-2019 2.1.1", + "_appFooter": "Discord.Net (c) 2015-2020 2.2.0", "_enableSearch": true, "_appLogoPath": "marketing/logo/SVG/Logomark Purple.svg", "_appFaviconPath": "favicon.ico" diff --git a/docs/faq/basics/basic-operations.md b/docs/faq/basics/basic-operations.md index 93811977f..35c71709f 100644 --- a/docs/faq/basics/basic-operations.md +++ b/docs/faq/basics/basic-operations.md @@ -88,7 +88,7 @@ implement [IEmote] and are valid options. *** -[AddReactionAsync]: xref:Discord.IUserMessage.AddReactionAsync* +[AddReactionAsync]: xref:Discord.IMessage.AddReactionAsync* ## What is a "preemptive rate limit?" diff --git a/docs/guides/commands/intro.md b/docs/guides/commands/intro.md index f8ff1b596..abe7065c1 100644 --- a/docs/guides/commands/intro.md +++ b/docs/guides/commands/intro.md @@ -71,11 +71,11 @@ By now, your module should look like this: > [!WARNING] > **Avoid using long-running code** in your modules wherever possible. -> You should **not** be implementing very much logic into your -> modules, instead, outsource to a service for that. +> Long-running code, by default, within a command module +> can cause gateway thread to be blocked; therefore, interrupting +> the bot's connection to Discord. > -> If you are unfamiliar with Inversion of Control, it is recommended -> to read the MSDN article on [IoC] and [Dependency Injection]. +> You may read more about it in @FAQ.Commands.General . The next step to creating commands is actually creating the commands. diff --git a/docs/guides/commands/namedarguments.md b/docs/guides/commands/namedarguments.md new file mode 100644 index 000000000..890a8463f --- /dev/null +++ b/docs/guides/commands/namedarguments.md @@ -0,0 +1,79 @@ +--- +uid: Guides.Commands.NamedArguments +title: Named Arguments +--- + +# Named Arguments + +By default, arguments for commands are parsed positionally, meaning +that the order matters. But sometimes you may want to define a command +with many optional parameters, and it'd be easier for end-users +to only specify what they want to set, instead of needing them +to specify everything by hand. + +## Setting up Named Arguments + +In order to be able to specify different arguments by name, you have +to create a new class that contains all of the optional values that +the command will use, and apply an instance of +[NamedArgumentTypeAttribute] on it. + +### Example - Creating a Named Arguments Type + +```cs +[NamedArgumentType] +public class NamableArguments +{ + public string First { get; set; } + public string Second { get; set; } + public string Third { get; set; } + public string Fourth { get; set; } +} +``` + +## Usage in a Command + +The command where you want to use these values can be declared like so: +```cs +[Command("act")] +public async Task Act(int requiredArg, NamableArguments namedArgs) +``` + +The command can now be invoked as +`.act 42 first: Hello fourth: "A string with spaces must be wrapped in quotes" second: World`. + +A TypeReader for the named arguments container type is +automatically registered. +It's important that any other arguments that would be required +are placed before the container type. + +> [!IMPORTANT] +> A single command can have only __one__ parameter of a +> type annotated with [NamedArgumentTypeAttribute], and it +> **MUST** be the last parameter in the list. +> A command parameter of such an annotated type +> is automatically treated as if that parameter +> has [RemainderAttribute](xref:Discord.Commands.RemainderAttribute) +> applied. + +## Complex Types + +The TypeReader for Named Argument Types will look for a TypeReader +of every property type, meaning any other command parameter type +will work just the same. + +You can also read multiple values into a single property +by making that property an `IEnumerable`. So for example, if your +Named Argument Type has the following field, +```cs +public IEnumerable Numbers { get; set; } +``` +then the command can be invoked as +`.cmd numbers: "1, 2, 4, 8, 16, 32"` + +## Additional Notes + +The use of [`[OverrideTypeReader]`](xref:Discord.Commands.OverrideTypeReaderAttribute) +is also supported on the properties of a Named Argument Type. + +[NamedArgumentTypeAttribute]: xref:Discord.Commands.NamedArgumentTypeAttribute diff --git a/docs/guides/toc.yml b/docs/guides/toc.yml index 01c245301..a6c38768f 100644 --- a/docs/guides/toc.yml +++ b/docs/guides/toc.yml @@ -27,6 +27,8 @@ topicUid: Guides.Commands.Intro - name: TypeReaders topicUid: Guides.Commands.TypeReaders + - name: Named Arguments + topicUid: Guides.Commands.NamedArguments - name: Preconditions topicUid: Guides.Commands.Preconditions - name: Dependency Injection diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index d5c060fe4..1d4b0e15a 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -36,7 +36,7 @@ namespace Discord.Commands internal readonly AsyncEvent> _logEvent = new AsyncEvent>(); /// - /// Occurs when a command is successfully executed without any error. + /// Occurs when a command is executed. /// /// /// This event is fired when a command has been executed, successfully or not. When a command fails to diff --git a/src/Discord.Net.Core/Entities/Channels/INestedChannel.cs b/src/Discord.Net.Core/Entities/Channels/INestedChannel.cs index e38e1db41..2c9503db1 100644 --- a/src/Discord.Net.Core/Entities/Channels/INestedChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/INestedChannel.cs @@ -40,8 +40,8 @@ namespace Discord /// Creates a new invite to this channel. /// /// - /// The following example creates a new invite to this channel; the invite lasts for 12 hours and can only - /// be used 3 times throughout its lifespan. + /// The following example creates a new invite to this channel; the invite lasts for 12 hours and can only + /// be used 3 times throughout its lifespan. /// /// await guildChannel.CreateInviteAsync(maxAge: 43200, maxUses: 3); /// @@ -60,8 +60,8 @@ namespace Discord /// Gets a collection of all invites to this channel. /// B /// - /// The following example gets all of the invites that have been created in this channel and selects the - /// most used invite. + /// The following example gets all of the invites that have been created in this channel and selects the + /// most used invite. /// /// var invites = await channel.GetInvitesAsync(); /// if (invites.Count == 0) return; diff --git a/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs b/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs index 29c764e3f..a2baf6990 100644 --- a/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs @@ -30,7 +30,7 @@ namespace Discord /// Gets the current slow-mode delay for this channel. /// /// - /// An representing the time in seconds required before the user can send another + /// An representing the time in seconds required before the user can send another /// message; 0 if disabled. /// int SlowModeInterval { get; } @@ -39,7 +39,7 @@ namespace Discord /// Bulk-deletes multiple messages. /// /// - /// The following example gets 250 messages from the channel and deletes them. + /// The following example gets 250 messages from the channel and deletes them. /// /// var messages = await textChannel.GetMessagesAsync(250).FlattenAsync(); /// await textChannel.DeleteMessagesAsync(messages); diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs index a18e91b69..b39a49776 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs @@ -510,7 +510,7 @@ namespace Discord /// Creates a new text channel in this guild. /// /// - /// The following example creates a new text channel under an existing category named Wumpus with a set topic. + /// The following example creates a new text channel under an existing category named Wumpus with a set topic. /// /// diff --git a/src/Discord.Net.Core/Entities/Messages/IMessage.cs b/src/Discord.Net.Core/Entities/Messages/IMessage.cs index 05f505269..aac526831 100644 --- a/src/Discord.Net.Core/Entities/Messages/IMessage.cs +++ b/src/Discord.Net.Core/Entities/Messages/IMessage.cs @@ -161,7 +161,7 @@ namespace Discord /// Adds a reaction to this message. /// /// - /// The following example adds the reaction, πŸ’•, to the message. + /// The following example adds the reaction, πŸ’•, to the message. /// /// await msg.AddReactionAsync(new Emoji("\U0001f495")); /// @@ -177,7 +177,7 @@ namespace Discord /// Removes a reaction from message. /// /// - /// The following example removes the reaction, πŸ’•, added by the message author from the message. + /// The following example removes the reaction, πŸ’•, added by the message author from the message. /// /// await msg.RemoveReactionAsync(new Emoji("\U0001f495"), msg.Author); /// @@ -194,7 +194,7 @@ namespace Discord /// Removes a reaction from message. /// /// - /// The following example removes the reaction, πŸ’•, added by the user with ID 84291986575613952 from the message. + /// The following example removes the reaction, πŸ’•, added by the user with ID 84291986575613952 from the message. /// /// await msg.RemoveReactionAsync(new Emoji("\U0001f495"), 84291986575613952); /// @@ -219,8 +219,25 @@ namespace Discord /// /// Gets all users that reacted to a message with a given emote. /// + /// + /// + /// The returned collection is an asynchronous enumerable object; one must call + /// to access the users as a + /// collection. + /// + /// + /// Do not fetch too many users at once! This may cause unwanted preemptive rate limit or even actual + /// rate limit, causing your bot to freeze! + /// + /// This method will attempt to fetch the number of reactions specified under . + /// The library will attempt to split up the requests according to your and + /// . In other words, should the user request 500 reactions, + /// and the constant is 100, the request will + /// be split into 5 individual requests; thus returning 5 individual asynchronous responses, hence the need + /// of flattening. + /// /// - /// The following example gets the users that have reacted with the emoji πŸ’• to the message. + /// The following example gets the users that have reacted with the emoji πŸ’• to the message. /// /// var emoji = new Emoji("\U0001f495"); /// var reactedUsers = await message.GetReactionUsersAsync(emoji, 100).FlattenAsync(); @@ -230,9 +247,7 @@ namespace Discord /// The number of users to request. /// The options to be used when sending the request. /// - /// A paged collection containing a read-only collection of users that has reacted to this message. - /// Flattening the paginated response into a collection of users with - /// is required if you wish to access the users. + /// Paged collection of users. /// IAsyncEnumerable> GetReactionUsersAsync(IEmote emoji, int limit, RequestOptions options = null); } diff --git a/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs b/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs index be2523b21..bc52dd01c 100644 --- a/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs +++ b/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs @@ -17,7 +17,7 @@ namespace Discord /// method and what properties are available, please refer to . /// /// - /// The following example replaces the content of the message with Hello World!. + /// The following example replaces the content of the message with Hello World!. /// /// await msg.ModifyAsync(x => x.Content = "Hello World!"); /// diff --git a/src/Discord.Net.Core/Entities/Users/IGuildUser.cs b/src/Discord.Net.Core/Entities/Users/IGuildUser.cs index ae682afd5..60fa06cbd 100644 --- a/src/Discord.Net.Core/Entities/Users/IGuildUser.cs +++ b/src/Discord.Net.Core/Entities/Users/IGuildUser.cs @@ -72,8 +72,8 @@ namespace Discord /// Gets the level permissions granted to this user to a given channel. /// /// - /// The following example checks if the current user has the ability to send a message with attachment in - /// this channel; if so, uploads a file via . + /// The following example checks if the current user has the ability to send a message with attachment in + /// this channel; if so, uploads a file via . /// /// if (currentUser?.GetPermissions(targetChannel)?.AttachFiles) /// await targetChannel.SendFileAsync("fortnite.png"); diff --git a/src/Discord.Net.Core/Entities/Users/IUser.cs b/src/Discord.Net.Core/Entities/Users/IUser.cs index c59a75de1..c36fb2326 100644 --- a/src/Discord.Net.Core/Entities/Users/IUser.cs +++ b/src/Discord.Net.Core/Entities/Users/IUser.cs @@ -21,8 +21,8 @@ namespace Discord /// example). /// /// - /// The following example attempts to retrieve the user's current avatar and send it to a channel; if one is - /// not set, a default avatar for this user will be returned instead. + /// The following example attempts to retrieve the user's current avatar and send it to a channel; if one is + /// not set, a default avatar for this user will be returned instead. /// /// @@ -90,8 +90,8 @@ namespace Discord /// /// /// - /// The following example attempts to send a direct message to the target user and logs the incident should - /// it fail. + /// The following example attempts to send a direct message to the target user and logs the incident should + /// it fail. /// /// diff --git a/src/Discord.Net.Core/Extensions/MessageExtensions.cs b/src/Discord.Net.Core/Extensions/MessageExtensions.cs index 90ebea92f..64a1d89ab 100644 --- a/src/Discord.Net.Core/Extensions/MessageExtensions.cs +++ b/src/Discord.Net.Core/Extensions/MessageExtensions.cs @@ -39,7 +39,7 @@ namespace Discord /// /// A task that represents the asynchronous operation for adding a reaction to this message. /// - /// + /// /// public static async Task AddReactionsAsync(this IUserMessage msg, IEmote[] reactions, RequestOptions options = null) { @@ -51,7 +51,7 @@ namespace Discord /// /// /// This method does not bulk remove reactions! If you want to clear reactions from a message, - /// + /// /// /// /// @@ -64,7 +64,7 @@ namespace Discord /// /// A task that represents the asynchronous operation for removing a reaction to this message. /// - /// + /// /// public static async Task RemoveReactionsAsync(this IUserMessage msg, IUser user, IEmote[] reactions, RequestOptions options = null) { diff --git a/src/Discord.Net.Core/Extensions/UserExtensions.cs b/src/Discord.Net.Core/Extensions/UserExtensions.cs index f98bf7227..58c762090 100644 --- a/src/Discord.Net.Core/Extensions/UserExtensions.cs +++ b/src/Discord.Net.Core/Extensions/UserExtensions.cs @@ -44,8 +44,8 @@ namespace Discord /// Sends a file to this message channel with an optional caption. /// /// - /// The following example uploads a streamed image that will be called b1nzy.jpg embedded inside a - /// rich embed to the channel. + /// The following example uploads a streamed image that will be called b1nzy.jpg embedded inside a + /// rich embed to the channel. /// /// await channel.SendFileAsync(b1nzyStream, "b1nzy.jpg", /// embed: new EmbedBuilder {ImageUrl = "attachment://b1nzy.jpg"}.Build()); diff --git a/src/Discord.Net.Core/IDiscordClient.cs b/src/Discord.Net.Core/IDiscordClient.cs index e1c900680..f972cd71d 100644 --- a/src/Discord.Net.Core/IDiscordClient.cs +++ b/src/Discord.Net.Core/IDiscordClient.cs @@ -270,7 +270,7 @@ namespace Discord /// /// The options to be used when sending the request. /// - /// A task that represents the asynchronous get operation. The task result contains an + /// A task that represents the asynchronous get operation. The task result contains an /// that represents the number of shards that should be used with this account. /// Task GetRecommendedShardCountAsync(RequestOptions options = null); diff --git a/src/Discord.Net.Core/RequestOptions.cs b/src/Discord.Net.Core/RequestOptions.cs index 6aa0eea12..1b05df2a3 100644 --- a/src/Discord.Net.Core/RequestOptions.cs +++ b/src/Discord.Net.Core/RequestOptions.cs @@ -49,8 +49,7 @@ namespace Discord /// clock for rate-limiting. Defaults to true. /// /// - /// This property can also be set in . - /// + /// This property can also be set in . /// On a per-request basis, the system clock should only be disabled /// when millisecond precision is especially important, and the /// hosting system is known to have a desynced clock. diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelCreateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelCreateAuditLogData.cs index f432b4ca5..7a015d6bc 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelCreateAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelCreateAuditLogData.cs @@ -78,7 +78,7 @@ namespace Discord.Rest /// Gets the current slow-mode delay of the created channel. /// /// - /// An representing the time in seconds required before the user can send another + /// An representing the time in seconds required before the user can send another /// message; 0 if disabled. /// null if this is not mentioned in this entry. /// @@ -95,7 +95,7 @@ namespace Discord.Rest /// Gets the bit-rate that the clients in the created voice channel are requested to use. /// /// - /// An representing the bit-rate (bps) that the created voice channel defines and requests the + /// An representing the bit-rate (bps) that the created voice channel defines and requests the /// client(s) to use. /// null if this is not mentioned in this entry. /// diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelDeleteAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelDeleteAuditLogData.cs index 390749929..81ae7155b 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelDeleteAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelDeleteAuditLogData.cs @@ -71,7 +71,7 @@ namespace Discord.Rest /// Gets the slow-mode delay of the deleted channel. /// /// - /// An representing the time in seconds required before the user can send another + /// An representing the time in seconds required before the user can send another /// message; 0 if disabled. /// null if this is not mentioned in this entry. /// @@ -88,7 +88,7 @@ namespace Discord.Rest /// Gets the bit-rate of this channel if applicable. /// /// - /// An representing the bit-rate set of the voice channel. + /// An representing the bit-rate set of the voice channel. /// null if this is not mentioned in this entry. /// public int? Bitrate { get; } diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelInfo.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelInfo.cs index d6d2fb4b3..0284b63f5 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelInfo.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelInfo.cs @@ -32,7 +32,7 @@ namespace Discord.Rest /// Gets the current slow-mode delay of this channel. /// /// - /// An representing the time in seconds required before the user can send another + /// An representing the time in seconds required before the user can send another /// message; 0 if disabled. /// null if this is not mentioned in this entry. /// @@ -49,7 +49,7 @@ namespace Discord.Rest /// Gets the bit-rate of this channel if applicable. /// /// - /// An representing the bit-rate set for the voice channel; + /// An representing the bit-rate set for the voice channel; /// null if this is not mentioned in this entry. /// public int? Bitrate { get; } diff --git a/src/Discord.Net.Webhook/DiscordWebhookClient.cs b/src/Discord.Net.Webhook/DiscordWebhookClient.cs index 542ec7997..9c90df565 100644 --- a/src/Discord.Net.Webhook/DiscordWebhookClient.cs +++ b/src/Discord.Net.Webhook/DiscordWebhookClient.cs @@ -85,7 +85,7 @@ namespace Discord.Webhook } private static API.DiscordRestApiClient CreateApiClient(DiscordRestConfig config) => new API.DiscordRestApiClient(config.RestClientProvider, DiscordRestConfig.UserAgent); - /// Sends a message using to the channel for this 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) From 6d8e2165451cc0e08190ca7c0ffaf848f3de9335 Mon Sep 17 00:00:00 2001 From: Yamboy1 Date: Wed, 22 Apr 2020 18:10:38 +1200 Subject: [PATCH 03/19] docs: Small typo in documentation (#1460) --- src/Discord.Net.Commands/CommandServiceConfig.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.Commands/CommandServiceConfig.cs b/src/Discord.Net.Commands/CommandServiceConfig.cs index 2dedceaa5..3c62063c8 100644 --- a/src/Discord.Net.Commands/CommandServiceConfig.cs +++ b/src/Discord.Net.Commands/CommandServiceConfig.cs @@ -44,7 +44,7 @@ namespace Discord.Commands /// /// /// - /// QuotationMarkAliasMap = new Dictionary<char, char%gt;() + /// QuotationMarkAliasMap = new Dictionary<char, char>() /// { /// {'\"', '\"' }, /// {'β€œ', '”' }, From ed869bd78b8ae152805b449b759714839b429ce5 Mon Sep 17 00:00:00 2001 From: Monica S Date: Sat, 25 Apr 2020 12:12:57 +0100 Subject: [PATCH 04/19] [apibrk] change: Specify WebSocket close code (#1500) * API breaking change: Specify WebSocket close code Should fix #1479 and help overall with resuming sessions. * Also try to resume on missed heartbeats --- .../Net/WebSockets/IWebSocketClient.cs | 2 +- .../WS4NetClient.cs | 10 ++++----- src/Discord.Net.Rest/DiscordRestApiClient.cs | 8 +++---- .../DiscordSocketApiClient.cs | 18 +++++---------- .../DiscordSocketClient.cs | 6 ++--- .../GatewayReconnectException.cs | 22 +++++++++++++++++++ .../Net/DefaultWebSocketClient.cs | 13 ++++++----- 7 files changed, 48 insertions(+), 31 deletions(-) create mode 100644 src/Discord.Net.WebSocket/GatewayReconnectException.cs diff --git a/src/Discord.Net.Core/Net/WebSockets/IWebSocketClient.cs b/src/Discord.Net.Core/Net/WebSockets/IWebSocketClient.cs index 14b41cce1..6791af354 100644 --- a/src/Discord.Net.Core/Net/WebSockets/IWebSocketClient.cs +++ b/src/Discord.Net.Core/Net/WebSockets/IWebSocketClient.cs @@ -14,7 +14,7 @@ namespace Discord.Net.WebSockets void SetCancelToken(CancellationToken cancelToken); Task ConnectAsync(string host); - Task DisconnectAsync(); + Task DisconnectAsync(int closeCode = 1000); Task SendAsync(byte[] data, int index, int count, bool isText); } diff --git a/src/Discord.Net.Providers.WS4Net/WS4NetClient.cs b/src/Discord.Net.Providers.WS4Net/WS4NetClient.cs index ef99c8045..50f19b778 100644 --- a/src/Discord.Net.Providers.WS4Net/WS4NetClient.cs +++ b/src/Discord.Net.Providers.WS4Net/WS4NetClient.cs @@ -40,7 +40,7 @@ namespace Discord.Net.Providers.WS4Net { if (disposing) { - DisconnectInternalAsync(true).GetAwaiter().GetResult(); + DisconnectInternalAsync(isDisposing: true).GetAwaiter().GetResult(); _lock?.Dispose(); _cancelTokenSource?.Dispose(); } @@ -92,19 +92,19 @@ namespace Discord.Net.Providers.WS4Net _waitUntilConnect.Wait(_cancelToken); } - public async Task DisconnectAsync() + public async Task DisconnectAsync(int closeCode = 1000) { await _lock.WaitAsync().ConfigureAwait(false); try { - await DisconnectInternalAsync().ConfigureAwait(false); + await DisconnectInternalAsync(closeCode: closeCode).ConfigureAwait(false); } finally { _lock.Release(); } } - private Task DisconnectInternalAsync(bool isDisposing = false) + private Task DisconnectInternalAsync(int closeCode = 1000, bool isDisposing = false) { _disconnectCancelTokenSource.Cancel(); if (_client == null) @@ -112,7 +112,7 @@ namespace Discord.Net.Providers.WS4Net if (_client.State == WebSocketState.Open) { - try { _client.Close(1000, ""); } + try { _client.Close(closeCode, ""); } catch { } } diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index ff6d17240..3a46dcf21 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -47,7 +47,7 @@ namespace Discord.API internal ulong? CurrentUserId { get; set; } public RateLimitPrecision RateLimitPrecision { get; private set; } internal bool UseSystemClock { get; set; } - + internal JsonSerializer Serializer => _serializer; /// Unknown OAuth token type. @@ -164,7 +164,7 @@ namespace Discord.API try { _loginCancelToken?.Cancel(false); } catch { } - await DisconnectInternalAsync().ConfigureAwait(false); + await DisconnectInternalAsync(null).ConfigureAwait(false); await RequestQueue.ClearAsync().ConfigureAwait(false); await RequestQueue.SetCancelTokenAsync(CancellationToken.None).ConfigureAwait(false); @@ -175,7 +175,7 @@ namespace Discord.API } internal virtual Task ConnectInternalAsync() => Task.Delay(0); - internal virtual Task DisconnectInternalAsync() => Task.Delay(0); + internal virtual Task DisconnectInternalAsync(Exception ex = null) => Task.Delay(0); //Core internal Task SendAsync(string method, Expression> endpointExpr, BucketIds ids, @@ -1062,7 +1062,7 @@ namespace Discord.API { foreach (var roleId in args.RoleIds.Value) Preconditions.NotEqual(roleId, 0, nameof(roleId)); - } + } options = RequestOptions.CreateOrClone(options); diff --git a/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs b/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs index 9313f0711..ef97615e2 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs @@ -164,26 +164,17 @@ namespace Discord.API } } - public async Task DisconnectAsync() + public async Task DisconnectAsync(Exception ex = null) { await _stateLock.WaitAsync().ConfigureAwait(false); try { - await DisconnectInternalAsync().ConfigureAwait(false); - } - finally { _stateLock.Release(); } - } - public async Task DisconnectAsync(Exception ex) - { - await _stateLock.WaitAsync().ConfigureAwait(false); - try - { - await DisconnectInternalAsync().ConfigureAwait(false); + await DisconnectInternalAsync(ex).ConfigureAwait(false); } finally { _stateLock.Release(); } } /// This client is not configured with WebSocket support. - internal override async Task DisconnectInternalAsync() + internal override async Task DisconnectInternalAsync(Exception ex = null) { if (WebSocketClient == null) throw new NotSupportedException("This client is not configured with WebSocket support."); @@ -194,6 +185,9 @@ namespace Discord.API try { _connectCancelToken?.Cancel(false); } catch { } + if (ex is GatewayReconnectException) + await WebSocketClient.DisconnectAsync(4000); + else await WebSocketClient.DisconnectAsync().ConfigureAwait(false); ConnectionState = ConnectionState.Disconnected; diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index ed142d001..1819966b3 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -264,7 +264,7 @@ namespace Discord.WebSocket { await _gatewayLogger.DebugAsync("Disconnecting ApiClient").ConfigureAwait(false); - await ApiClient.DisconnectAsync().ConfigureAwait(false); + await ApiClient.DisconnectAsync(ex).ConfigureAwait(false); //Wait for tasks to complete await _gatewayLogger.DebugAsync("Waiting for heartbeater").ConfigureAwait(false); @@ -511,7 +511,7 @@ namespace Discord.WebSocket case GatewayOpCode.Reconnect: { await _gatewayLogger.DebugAsync("Received Reconnect").ConfigureAwait(false); - _connection.Error(new Exception("Server requested a reconnect")); + _connection.Error(new GatewayReconnectException("Server requested a reconnect")); } break; case GatewayOpCode.Dispatch: @@ -1689,7 +1689,7 @@ namespace Discord.WebSocket { if (ConnectionState == ConnectionState.Connected && (_guildDownloadTask?.IsCompleted ?? true)) { - _connection.Error(new Exception("Server missed last heartbeat")); + _connection.Error(new GatewayReconnectException("Server missed last heartbeat")); return; } } diff --git a/src/Discord.Net.WebSocket/GatewayReconnectException.cs b/src/Discord.Net.WebSocket/GatewayReconnectException.cs new file mode 100644 index 000000000..1a8024558 --- /dev/null +++ b/src/Discord.Net.WebSocket/GatewayReconnectException.cs @@ -0,0 +1,22 @@ +using System; + +namespace Discord.WebSocket +{ + /// + /// An exception thrown when the gateway client has been requested to + /// reconnect. + /// + public class GatewayReconnectException : Exception + { + /// + /// Creates a new instance of the + /// type. + /// + /// + /// The reason why the gateway has been requested to reconnect. + /// + public GatewayReconnectException(string message) + : base(message) + { } + } +} diff --git a/src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs b/src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs index 36a6fea4f..4723ae57a 100644 --- a/src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs +++ b/src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs @@ -44,7 +44,7 @@ namespace Discord.Net.WebSockets { if (disposing) { - DisconnectInternalAsync(true).GetAwaiter().GetResult(); + DisconnectInternalAsync(isDisposing: true).GetAwaiter().GetResult(); _disconnectTokenSource?.Dispose(); _cancelTokenSource?.Dispose(); _lock?.Dispose(); @@ -94,19 +94,19 @@ namespace Discord.Net.WebSockets _task = RunAsync(_cancelToken); } - public async Task DisconnectAsync() + public async Task DisconnectAsync(int closeCode = 1000) { await _lock.WaitAsync().ConfigureAwait(false); try { - await DisconnectInternalAsync().ConfigureAwait(false); + await DisconnectInternalAsync(closeCode: closeCode).ConfigureAwait(false); } finally { _lock.Release(); } } - private async Task DisconnectInternalAsync(bool isDisposing = false) + private async Task DisconnectInternalAsync(int closeCode = 1000, bool isDisposing = false) { try { _disconnectTokenSource.Cancel(false); } catch { } @@ -117,7 +117,8 @@ namespace Discord.Net.WebSockets { if (!isDisposing) { - try { await _client.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "", new CancellationToken()); } + var status = (WebSocketCloseStatus)closeCode; + try { await _client.CloseOutputAsync(status, "", new CancellationToken()); } catch { } } try { _client.Dispose(); } @@ -141,7 +142,7 @@ namespace Discord.Net.WebSockets await _lock.WaitAsync().ConfigureAwait(false); try { - await DisconnectInternalAsync(false); + await DisconnectInternalAsync(isDisposing: false); } finally { From 479e28358e488ef791788e2b74f0840ca5d1f043 Mon Sep 17 00:00:00 2001 From: NeKz Date: Thu, 7 May 2020 15:04:33 +0200 Subject: [PATCH 05/19] feature: Implement missing audit log types (#1458) * Implement missing audit log types * Use IUser properties --- .../Entities/AuditLogs/ActionType.cs | 26 +++++++++- .../API/Common/AuditLogOptions.cs | 7 +-- .../Entities/AuditLogs/AuditLogHelper.cs | 6 +++ .../AuditLogs/DataTypes/BotAddAuditLogData.cs | 32 +++++++++++++ .../DataTypes/MemberDisconnectAuditLogData.cs | 29 +++++++++++ .../DataTypes/MemberMoveAuditLogData.cs | 37 ++++++++++++++ .../MessageBulkDeleteAuditLogData.cs | 38 +++++++++++++++ .../DataTypes/MessageDeleteAuditLogData.cs | 15 +++--- .../DataTypes/MessagePinAuditLogData.cs | 48 +++++++++++++++++++ .../DataTypes/MessageUnpinAuditLogData.cs | 48 +++++++++++++++++++ 10 files changed, 276 insertions(+), 10 deletions(-) create mode 100644 src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BotAddAuditLogData.cs create mode 100644 src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberDisconnectAuditLogData.cs create mode 100644 src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberMoveAuditLogData.cs create mode 100644 src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageBulkDeleteAuditLogData.cs create mode 100644 src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessagePinAuditLogData.cs create mode 100644 src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageUnpinAuditLogData.cs diff --git a/src/Discord.Net.Core/Entities/AuditLogs/ActionType.cs b/src/Discord.Net.Core/Entities/AuditLogs/ActionType.cs index 2561a0970..1728b2021 100644 --- a/src/Discord.Net.Core/Entities/AuditLogs/ActionType.cs +++ b/src/Discord.Net.Core/Entities/AuditLogs/ActionType.cs @@ -61,6 +61,18 @@ namespace Discord /// A guild member's role collection was updated. /// MemberRoleUpdated = 25, + /// + /// A guild member moved to a voice channel. + /// + MemberMoved = 26, + /// + /// A guild member disconnected from a voice channel. + /// + MemberDisconnected = 27, + /// + /// A bot was added to this guild. + /// + BotAdded = 28, /// /// A role was created in this guild. @@ -117,6 +129,18 @@ namespace Discord /// /// A message was deleted from this guild. /// - MessageDeleted = 72 + MessageDeleted = 72, + /// + /// Multiple messages were deleted from this guild. + /// + MessageBulkDeleted = 73, + /// + /// A message was pinned from this guild. + /// + MessagePinned = 74, + /// + /// A message was unpinned from this guild. + /// + MessageUnpinned = 75, } } diff --git a/src/Discord.Net.Rest/API/Common/AuditLogOptions.cs b/src/Discord.Net.Rest/API/Common/AuditLogOptions.cs index 24141d90c..b666215e2 100644 --- a/src/Discord.Net.Rest/API/Common/AuditLogOptions.cs +++ b/src/Discord.Net.Rest/API/Common/AuditLogOptions.cs @@ -4,11 +4,12 @@ namespace Discord.API { internal class AuditLogOptions { - //Message delete [JsonProperty("count")] - public int? MessageDeleteCount { get; set; } + public int? Count { get; set; } [JsonProperty("channel_id")] - public ulong? MessageDeleteChannelId { get; set; } + public ulong? ChannelId { get; set; } + [JsonProperty("message_id")] + public ulong? MessageId { get; set; } //Prune [JsonProperty("delete_member_days")] diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/AuditLogHelper.cs b/src/Discord.Net.Rest/Entities/AuditLogs/AuditLogHelper.cs index 7936343f3..696917203 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/AuditLogHelper.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/AuditLogHelper.cs @@ -27,6 +27,9 @@ namespace Discord.Rest [ActionType.Unban] = UnbanAuditLogData.Create, [ActionType.MemberUpdated] = MemberUpdateAuditLogData.Create, [ActionType.MemberRoleUpdated] = MemberRoleAuditLogData.Create, + [ActionType.MemberMoved] = MemberMoveAuditLogData.Create, + [ActionType.MemberDisconnected] = MemberDisconnectAuditLogData.Create, + [ActionType.BotAdded] = BotAddAuditLogData.Create, [ActionType.RoleCreated] = RoleCreateAuditLogData.Create, [ActionType.RoleUpdated] = RoleUpdateAuditLogData.Create, @@ -45,6 +48,9 @@ namespace Discord.Rest [ActionType.EmojiDeleted] = EmoteDeleteAuditLogData.Create, [ActionType.MessageDeleted] = MessageDeleteAuditLogData.Create, + [ActionType.MessageBulkDeleted] = MessageBulkDeleteAuditLogData.Create, + [ActionType.MessagePinned] = MessagePinAuditLogData.Create, + [ActionType.MessageUnpinned] = MessageUnpinAuditLogData.Create, }; public static IAuditLogData CreateData(BaseDiscordClient discord, Model log, EntryModel entry) diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BotAddAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BotAddAuditLogData.cs new file mode 100644 index 000000000..0d12e4609 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BotAddAuditLogData.cs @@ -0,0 +1,32 @@ +using System.Linq; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + /// + /// Contains a piece of audit log data related to a adding a bot to a guild. + /// + public class BotAddAuditLogData : IAuditLogData + { + private BotAddAuditLogData(IUser bot) + { + Target = bot; + } + + internal static BotAddAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId); + return new BotAddAuditLogData(RestUser.Create(discord, userInfo)); + } + + /// + /// Gets the bot that was added. + /// + /// + /// A user object representing the bot. + /// + public IUser Target { get; } + } +} diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberDisconnectAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberDisconnectAuditLogData.cs new file mode 100644 index 000000000..b0374dc86 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberDisconnectAuditLogData.cs @@ -0,0 +1,29 @@ +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + /// + /// Contains a piece of audit log data related to disconnecting members from voice channels. + /// + public class MemberDisconnectAuditLogData : IAuditLogData + { + private MemberDisconnectAuditLogData(int count) + { + MemberCount = count; + } + + internal static MemberDisconnectAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + return new MemberDisconnectAuditLogData(entry.Options.Count.Value); + } + + /// + /// Gets the number of members that were disconnected. + /// + /// + /// An representing the number of members that were disconnected from a voice channel. + /// + public int MemberCount { get; } + } +} diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberMoveAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberMoveAuditLogData.cs new file mode 100644 index 000000000..f5373d34d --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberMoveAuditLogData.cs @@ -0,0 +1,37 @@ +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + /// + /// Contains a piece of audit log data related to moving members between voice channels. + /// + public class MemberMoveAuditLogData : IAuditLogData + { + private MemberMoveAuditLogData(ulong channelId, int count) + { + ChannelId = channelId; + MemberCount = count; + } + + internal static MemberMoveAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + return new MemberMoveAuditLogData(entry.Options.ChannelId.Value, entry.Options.Count.Value); + } + + /// + /// Gets the ID of the channel that the members were moved to. + /// + /// + /// A representing the snowflake identifier for the channel that the members were moved to. + /// + public ulong ChannelId { get; } + /// + /// Gets the number of members that were moved. + /// + /// + /// An representing the number of members that were moved to another voice channel. + /// + public int MemberCount { get; } + } +} diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageBulkDeleteAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageBulkDeleteAuditLogData.cs new file mode 100644 index 000000000..7a9846349 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageBulkDeleteAuditLogData.cs @@ -0,0 +1,38 @@ +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + /// + /// Contains a piece of audit log data related to message deletion(s). + /// + public class MessageBulkDeleteAuditLogData : IAuditLogData + { + private MessageBulkDeleteAuditLogData(ulong channelId, int count) + { + ChannelId = channelId; + MessageCount = count; + } + + internal static MessageBulkDeleteAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + return new MessageBulkDeleteAuditLogData(entry.TargetId.Value, entry.Options.Count.Value); + } + + /// + /// Gets the ID of the channel that the messages were deleted from. + /// + /// + /// A representing the snowflake identifier for the channel that the messages were + /// deleted from. + /// + public ulong ChannelId { get; } + /// + /// Gets the number of messages that were deleted. + /// + /// + /// An representing the number of messages that were deleted from the channel. + /// + public int MessageCount { get; } + } +} diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageDeleteAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageDeleteAuditLogData.cs index c6b2e1053..66b3f7d83 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageDeleteAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageDeleteAuditLogData.cs @@ -1,3 +1,5 @@ +using System.Linq; + using Model = Discord.API.AuditLog; using EntryModel = Discord.API.AuditLogEntry; @@ -8,16 +10,17 @@ namespace Discord.Rest /// public class MessageDeleteAuditLogData : IAuditLogData { - private MessageDeleteAuditLogData(ulong channelId, int count, ulong authorId) + private MessageDeleteAuditLogData(ulong channelId, int count, IUser user) { ChannelId = channelId; MessageCount = count; - AuthorId = authorId; + Target = user; } internal static MessageDeleteAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) { - return new MessageDeleteAuditLogData(entry.Options.MessageDeleteChannelId.Value, entry.Options.MessageDeleteCount.Value, entry.TargetId.Value); + var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId); + return new MessageDeleteAuditLogData(entry.Options.ChannelId.Value, entry.Options.Count.Value, RestUser.Create(discord, userInfo)); } /// @@ -36,11 +39,11 @@ namespace Discord.Rest /// public ulong ChannelId { get; } /// - /// Gets the author of the messages that were deleted. + /// Gets the user of the messages that were deleted. /// /// - /// A representing the snowflake identifier for the user that created the deleted messages. + /// A user object representing the user that created the deleted messages. /// - public ulong AuthorId { get; } + public IUser Target { get; } } } diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessagePinAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessagePinAuditLogData.cs new file mode 100644 index 000000000..020171152 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessagePinAuditLogData.cs @@ -0,0 +1,48 @@ +using System.Linq; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + /// + /// Contains a piece of audit log data related to a pinned message. + /// + public class MessagePinAuditLogData : IAuditLogData + { + private MessagePinAuditLogData(ulong messageId, ulong channelId, IUser user) + { + MessageId = messageId; + ChannelId = channelId; + Target = user; + } + + internal static MessagePinAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId); + return new MessagePinAuditLogData(entry.Options.MessageId.Value, entry.Options.ChannelId.Value, RestUser.Create(discord, userInfo)); + } + + /// + /// Gets the ID of the messages that was pinned. + /// + /// + /// A representing the snowflake identifier for the messages that was pinned. + /// + public ulong MessageId { get; } + /// + /// Gets the ID of the channel that the message was pinned from. + /// + /// + /// A representing the snowflake identifier for the channel that the message was pinned from. + /// + public ulong ChannelId { get; } + /// + /// Gets the user of the message that was pinned. + /// + /// + /// A user object representing the user that created the pinned message. + /// + public IUser Target { get; } + } +} diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageUnpinAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageUnpinAuditLogData.cs new file mode 100644 index 000000000..1b3ff96f3 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageUnpinAuditLogData.cs @@ -0,0 +1,48 @@ +using System.Linq; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + /// + /// Contains a piece of audit log data related to an unpinned message. + /// + public class MessageUnpinAuditLogData : IAuditLogData + { + private MessageUnpinAuditLogData(ulong messageId, ulong channelId, IUser user) + { + MessageId = messageId; + ChannelId = channelId; + Target = user; + } + + internal static MessageUnpinAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId); + return new MessageUnpinAuditLogData(entry.Options.MessageId.Value, entry.Options.ChannelId.Value, RestUser.Create(discord, userInfo)); + } + + /// + /// Gets the ID of the messages that was unpinned. + /// + /// + /// A representing the snowflake identifier for the messages that was unpinned. + /// + public ulong MessageId { get; } + /// + /// Gets the ID of the channel that the message was unpinned from. + /// + /// + /// A representing the snowflake identifier for the channel that the message was unpinned from. + /// + public ulong ChannelId { get; } + /// + /// Gets the user of the message that was unpinned. + /// + /// + /// A user object representing the user that created the unpinned message. + /// + public IUser Target { get; } + } +} From 41543a80840e7af4356e720d13d3b9d87633f4f2 Mon Sep 17 00:00:00 2001 From: NeKz Date: Thu, 7 May 2020 15:09:46 +0200 Subject: [PATCH 06/19] fix: Fix Deserialization in Audit Log Data Types (#1509) * Get overwrite id and type from options * Create overwrites via API model --- .../DataTypes/ChannelCreateAuditLogData.cs | 17 +++++------------ .../DataTypes/OverwriteDeleteAuditLogData.cs | 11 ++++++----- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelCreateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelCreateAuditLogData.cs index 7a015d6bc..5c2f81ae6 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelCreateAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelCreateAuditLogData.cs @@ -25,7 +25,6 @@ namespace Discord.Rest internal static ChannelCreateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) { var changes = entry.Changes; - var overwrites = new List(); var overwritesModel = changes.FirstOrDefault(x => x.ChangedProperty == "permission_overwrites"); var typeModel = changes.FirstOrDefault(x => x.ChangedProperty == "type"); @@ -34,23 +33,17 @@ namespace Discord.Rest var nsfwModel = changes.FirstOrDefault(x => x.ChangedProperty == "nsfw"); var bitrateModel = changes.FirstOrDefault(x => x.ChangedProperty == "bitrate"); + var overwrites = overwritesModel.NewValue.ToObject(discord.ApiClient.Serializer) + .Select(x => new Overwrite(x.TargetId, x.TargetType, new OverwritePermissions(x.Allow, x.Deny))) + .ToList(); var type = typeModel.NewValue.ToObject(discord.ApiClient.Serializer); var name = nameModel.NewValue.ToObject(discord.ApiClient.Serializer); int? rateLimitPerUser = rateLimitPerUserModel?.NewValue?.ToObject(discord.ApiClient.Serializer); bool? nsfw = nsfwModel?.NewValue?.ToObject(discord.ApiClient.Serializer); int? bitrate = bitrateModel?.NewValue?.ToObject(discord.ApiClient.Serializer); + var id = entry.TargetId.Value; - foreach (var overwrite in overwritesModel.NewValue) - { - var deny = overwrite["deny"].ToObject(discord.ApiClient.Serializer); - var permType = overwrite["type"].ToObject(discord.ApiClient.Serializer); - var id = overwrite["id"].ToObject(discord.ApiClient.Serializer); - var allow = overwrite["allow"].ToObject(discord.ApiClient.Serializer); - - overwrites.Add(new Overwrite(id, permType, new OverwritePermissions(allow, deny))); - } - - return new ChannelCreateAuditLogData(entry.TargetId.Value, name, type, rateLimitPerUser, nsfw, bitrate, overwrites.ToReadOnlyCollection()); + return new ChannelCreateAuditLogData(id, name, type, rateLimitPerUser, nsfw, bitrate, overwrites.ToReadOnlyCollection()); } /// diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteDeleteAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteDeleteAuditLogData.cs index a193e76ce..dc8948d37 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteDeleteAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteDeleteAuditLogData.cs @@ -21,16 +21,17 @@ namespace Discord.Rest var changes = entry.Changes; var denyModel = changes.FirstOrDefault(x => x.ChangedProperty == "deny"); - var typeModel = changes.FirstOrDefault(x => x.ChangedProperty == "type"); - var idModel = changes.FirstOrDefault(x => x.ChangedProperty == "id"); var allowModel = changes.FirstOrDefault(x => x.ChangedProperty == "allow"); var deny = denyModel.OldValue.ToObject(discord.ApiClient.Serializer); - var type = typeModel.OldValue.ToObject(discord.ApiClient.Serializer); - var id = idModel.OldValue.ToObject(discord.ApiClient.Serializer); var allow = allowModel.OldValue.ToObject(discord.ApiClient.Serializer); - return new OverwriteDeleteAuditLogData(entry.TargetId.Value, new Overwrite(id, type, new OverwritePermissions(allow, deny))); + var permissions = new OverwritePermissions(allow, deny); + + var id = entry.Options.OverwriteTargetId.Value; + var type = entry.Options.OverwriteType; + + return new OverwriteDeleteAuditLogData(entry.TargetId.Value, new Overwrite(id, type, permissions)); } /// From f8b2b5627eca485191362c0346dc448d9853b451 Mon Sep 17 00:00:00 2001 From: Paulo Date: Thu, 7 May 2020 10:12:08 -0300 Subject: [PATCH 07/19] fix: Move content check for ModifyMessage (#1503) good catch, thanks! --- src/Discord.Net.Rest/DiscordRestApiClient.cs | 9 ++------- src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs | 5 +++++ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index 3a46dcf21..a726ef75d 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -602,13 +602,8 @@ namespace Discord.API Preconditions.NotEqual(channelId, 0, nameof(channelId)); Preconditions.NotEqual(messageId, 0, nameof(messageId)); Preconditions.NotNull(args, nameof(args)); - if (args.Content.IsSpecified) - { - if (!args.Embed.IsSpecified) - Preconditions.NotNullOrEmpty(args.Content, nameof(args.Content)); - if (args.Content.Value?.Length > DiscordConfig.MaxMessageSize) - throw new ArgumentOutOfRangeException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content)); - } + if (args.Content.IsSpecified && args.Content.Value?.Length > DiscordConfig.MaxMessageSize) + throw new ArgumentOutOfRangeException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content)); options = RequestOptions.CreateOrClone(options); var ids = new BucketIds(channelId: channelId); diff --git a/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs b/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs index 75892defb..b29eca62e 100644 --- a/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs +++ b/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs @@ -32,6 +32,11 @@ namespace Discord.Rest var args = new MessageProperties(); func(args); + bool hasText = args.Content.IsSpecified ? !string.IsNullOrEmpty(args.Content.Value) : !string.IsNullOrEmpty(msg.Content); + bool hasEmbed = args.Embed.IsSpecified ? args.Embed.Value != null : msg.Embeds.Any(); + if (!hasText && !hasEmbed) + Preconditions.NotNullOrEmpty(args.Content.IsSpecified ? args.Content.Value : string.Empty, nameof(args.Content)); + var apiArgs = new API.Rest.ModifyMessageParams { Content = args.Content, From 89b6b7e1a5cc3ab6264d8606ee17908f4abdadd5 Mon Sep 17 00:00:00 2001 From: Chris Johnston <16418643+Chris-Johnston@users.noreply.github.com> Date: Thu, 7 May 2020 06:13:57 -0700 Subject: [PATCH 08/19] feature: Include allowed mentions payload on message creation (#1455) * Feature: Allowed mentions object on msg create (interface breaking) This change implements the AllowedMentions object for the payload of message creation. By default, the mentions behavior remains unchanged, the message content is parsed for all mentionable entities and they are all notified. If this payload is not null, it will use the content of the allowed_mentions field to determine if a role is notified, or just displayed. This change is interface breaking. This follows the conventions of keeping RequestOptions as the last argument, but could break some users who specify each of these arguments without using named arguments. * lint: remove commented-out code This change removes the commented-out code which was added during testing from the previous commit. * fix interface break: reorder allowedMentions arg so that it's after options This change modifies the order of the AllowedMentions argument of SendMessageAsync so that this addition shouldn't be interface breaking. The downside to this change is that it breaks the convention followed by other methods, where the RequestOptions argument is normally last. * docs: fix typo in allowedMentions arg doc * fix interface break arg from IRestMessageChannel * docs: update xmldoc for allowedMentions args * fix interface breaking arg order for ISocketMessageChannel * fix mocked classes that weren't updated * fix: RestDMChannel#SendMessageAsync bug, allowed mentions always null This change fixes a bug that was introduced while testing changes to the interface of the SendMessageAsync method to try and retain interface compatibility * docs: update xmldoc for AllowedMentions type * docs: reword xmldoc of AllowedMentionTypes type * docs: fix typo * fix: validate that User/Role flags and UserIds/RoleIds lists are mutually exclusive This change adds validation to SendMessageAsync which checks that the User flag is mutually exclusive with the list of UserIds, and that the Role flag is also mutually exclusive with the list of RoleIds * docs: reword summaries for AllowedMentions type * Add util properties for specifying all or no mentions Adds read only properties which specify that all mentions or no mentions will notify users. These settings might be more common than others, so this would make them easier to use. * docs: Resolve PR comments for documentation issues/typos --- src/Discord.Net.Commands/ModuleBase.cs | 8 ++- .../Entities/Channels/IMessageChannel.cs | 6 +- .../Entities/Messages/AllowedMentionTypes.cs | 24 +++++++ .../Entities/Messages/AllowedMentions.cs | 64 +++++++++++++++++++ .../Extensions/UserExtensions.cs | 9 ++- src/Discord.Net.Rest/API/Common/Message.cs | 2 + .../API/Rest/CreateMessageParams.cs | 4 +- .../Entities/Channels/ChannelHelper.cs | 23 ++++++- .../Entities/Channels/IRestMessageChannel.cs | 6 +- .../Entities/Channels/RestDMChannel.cs | 8 +-- .../Entities/Channels/RestGroupChannel.cs | 9 +-- .../Entities/Channels/RestTextChannel.cs | 8 +-- .../Entities/Messages/AllowedMentions.cs | 15 +++++ .../Extensions/EntityExtensions.cs | 19 ++++++ .../Channels/ISocketMessageChannel.cs | 6 +- .../Entities/Channels/SocketDMChannel.cs | 8 +-- .../Entities/Channels/SocketGroupChannel.cs | 8 +-- .../Entities/Channels/SocketTextChannel.cs | 8 +-- .../MockedEntities/MockedDMChannel.cs | 2 +- .../MockedEntities/MockedGroupChannel.cs | 2 +- .../MockedEntities/MockedTextChannel.cs | 2 +- 21 files changed, 204 insertions(+), 37 deletions(-) create mode 100644 src/Discord.Net.Core/Entities/Messages/AllowedMentionTypes.cs create mode 100644 src/Discord.Net.Core/Entities/Messages/AllowedMentions.cs create mode 100644 src/Discord.Net.Rest/Entities/Messages/AllowedMentions.cs diff --git a/src/Discord.Net.Commands/ModuleBase.cs b/src/Discord.Net.Commands/ModuleBase.cs index 9cd4ea15d..ec1722c70 100644 --- a/src/Discord.Net.Commands/ModuleBase.cs +++ b/src/Discord.Net.Commands/ModuleBase.cs @@ -31,9 +31,13 @@ namespace Discord.Commands /// /// Specifies if Discord should read this aloud using text-to-speech. /// An embed to be displayed alongside the . - protected virtual async Task ReplyAsync(string message = null, bool isTTS = false, Embed embed = null, RequestOptions options = null) + /// + /// Specifies if notifications are sent for mentioned users and roles in the . + /// If null, all mentioned roles and users will be notified. + /// + protected virtual async Task ReplyAsync(string message = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null) { - return await Context.Channel.SendMessageAsync(message, isTTS, embed, options).ConfigureAwait(false); + return await Context.Channel.SendMessageAsync(message, isTTS, embed, options, allowedMentions).ConfigureAwait(false); } /// /// The method to execute before executing the command. diff --git a/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs b/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs index b5aa69d55..f5b986295 100644 --- a/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs @@ -23,11 +23,15 @@ namespace Discord /// 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. + /// /// /// 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); + Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null); /// /// Sends a file to this message channel with an optional caption. /// diff --git a/src/Discord.Net.Core/Entities/Messages/AllowedMentionTypes.cs b/src/Discord.Net.Core/Entities/Messages/AllowedMentionTypes.cs new file mode 100644 index 000000000..3ce6531b7 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Messages/AllowedMentionTypes.cs @@ -0,0 +1,24 @@ +using System; + +namespace Discord +{ + /// + /// Specifies the type of mentions that will be notified from the message content. + /// + [Flags] + public enum AllowedMentionTypes + { + /// + /// Controls role mentions. + /// + Roles, + /// + /// Controls user mentions. + /// + Users, + /// + /// Controls @everyone and @here mentions. + /// + Everyone, + } +} diff --git a/src/Discord.Net.Core/Entities/Messages/AllowedMentions.cs b/src/Discord.Net.Core/Entities/Messages/AllowedMentions.cs new file mode 100644 index 000000000..9b168bbd0 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Messages/AllowedMentions.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; + +namespace Discord +{ + /// + /// Defines which mentions and types of mentions that will notify users from the message content. + /// + public class AllowedMentions + { + private static readonly Lazy none = new Lazy(() => new AllowedMentions()); + private static readonly Lazy all = new Lazy(() => + new AllowedMentions(AllowedMentionTypes.Everyone | AllowedMentionTypes.Users | AllowedMentionTypes.Roles)); + + /// + /// Gets a value which indicates that no mentions in the message content should notify users. + /// + public static AllowedMentions None => none.Value; + + /// + /// Gets a value which indicates that all mentions in the message content should notify users. + /// + public static AllowedMentions All => all.Value; + + /// + /// Gets or sets the type of mentions that will be parsed from the message content. + /// + /// + /// The flag is mutually exclusive with the + /// property, and the flag is mutually exclusive with the + /// property. + /// If null, only the ids specified in and will be mentioned. + /// + public AllowedMentionTypes? AllowedTypes { get; set; } + + /// + /// Gets or sets the list of all role ids that will be mentioned. + /// This property is mutually exclusive with the + /// flag of the property. If the flag is set, the value of this property + /// must be null or empty. + /// + public List RoleIds { get; set; } + + /// + /// Gets or sets the list of all user ids that will be mentioned. + /// This property is mutually exclusive with the + /// flag of the property. If the flag is set, the value of this property + /// must be null or empty. + /// + public List UserIds { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The types of mentions to parse from the message content. + /// If null, only the ids specified in and will be mentioned. + /// + public AllowedMentions(AllowedMentionTypes? allowedTypes = null) + { + AllowedTypes = allowedTypes; + } + } +} diff --git a/src/Discord.Net.Core/Extensions/UserExtensions.cs b/src/Discord.Net.Core/Extensions/UserExtensions.cs index 58c762090..90f26828a 100644 --- a/src/Discord.Net.Core/Extensions/UserExtensions.cs +++ b/src/Discord.Net.Core/Extensions/UserExtensions.cs @@ -28,6 +28,10 @@ namespace Discord /// 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. + /// /// /// A task that represents the asynchronous send operation. The task result contains the sent message. /// @@ -35,9 +39,10 @@ namespace Discord string text = null, bool isTTS = false, Embed embed = null, - RequestOptions options = null) + RequestOptions options = null, + AllowedMentions allowedMentions = null) { - return await (await user.GetOrCreateDMChannelAsync().ConfigureAwait(false)).SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); + return await (await user.GetOrCreateDMChannelAsync().ConfigureAwait(false)).SendMessageAsync(text, isTTS, embed, options, allowedMentions).ConfigureAwait(false); } /// diff --git a/src/Discord.Net.Rest/API/Common/Message.cs b/src/Discord.Net.Rest/API/Common/Message.cs index f20035685..b4529d457 100644 --- a/src/Discord.Net.Rest/API/Common/Message.cs +++ b/src/Discord.Net.Rest/API/Common/Message.cs @@ -54,5 +54,7 @@ namespace Discord.API public Optional Reference { get; set; } [JsonProperty("flags")] public Optional Flags { get; set; } + [JsonProperty("allowed_mentions")] + public Optional AllowedMentions { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Rest/CreateMessageParams.cs b/src/Discord.Net.Rest/API/Rest/CreateMessageParams.cs index d77bff8ca..4b56658d6 100644 --- a/src/Discord.Net.Rest/API/Rest/CreateMessageParams.cs +++ b/src/Discord.Net.Rest/API/Rest/CreateMessageParams.cs @@ -1,4 +1,4 @@ -ο»Ώ#pragma warning disable CS1591 +#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API.Rest @@ -15,6 +15,8 @@ namespace Discord.API.Rest public Optional IsTTS { get; set; } [JsonProperty("embed")] public Optional Embed { get; set; } + [JsonProperty("allowed_mentions")] + public Optional AllowedMentions { 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 5fb150cda..aa90b2eee 100644 --- a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs +++ b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs @@ -167,9 +167,28 @@ namespace Discord.Rest /// Message content is too long, length must be less or equal to . public static async Task SendMessageAsync(IMessageChannel channel, BaseDiscordClient client, - string text, bool isTTS, Embed embed, RequestOptions options) + string text, bool isTTS, Embed embed, AllowedMentions allowedMentions, RequestOptions options) { - var args = new CreateMessageParams(text) { IsTTS = isTTS, Embed = embed?.ToModel() }; + 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."); + + // check that user flag and user Id list are exclusive, same with role flag and role Id list + if (allowedMentions != null && allowedMentions.AllowedTypes.HasValue) + { + if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Users) && + allowedMentions.UserIds != null && allowedMentions.UserIds.Count > 0) + { + throw new ArgumentException("The Users flag is mutually exclusive with the list of User Ids.", nameof(allowedMentions)); + } + + if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Roles) && + allowedMentions.RoleIds != null && allowedMentions.RoleIds.Count > 0) + { + throw new ArgumentException("The Roles flag is mutually exclusive with the list of Role Ids.", nameof(allowedMentions)); + } + } + + var args = new CreateMessageParams(text) { IsTTS = isTTS, Embed = embed?.ToModel(), AllowedMentions = allowedMentions?.ToModel() }; var model = await client.ApiClient.CreateMessageAsync(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 a28170eed..195fa92df 100644 --- a/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs @@ -20,11 +20,15 @@ namespace Discord.Rest /// 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. + /// /// /// 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); + new Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null); /// /// Sends a file to this message channel with an optional caption. /// diff --git a/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs index 446410b70..732af2d81 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs @@ -93,8 +93,8 @@ 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) - => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, options); /// /// @@ -206,8 +206,8 @@ namespace Discord.Rest async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler) => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false); /// - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) - => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions) + => await SendMessageAsync(text, isTTS, embed, options, allowedMentions).ConfigureAwait(false); //IChannel /// diff --git a/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs index 5cfe03f15..3c21bd95f 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs @@ -95,8 +95,8 @@ 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) - => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, options); /// /// @@ -183,8 +183,9 @@ namespace Discord.Rest async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler) => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false); - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) - => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); + + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions) + => await SendMessageAsync(text, isTTS, embed, options, allowedMentions).ConfigureAwait(false); //IAudioChannel /// diff --git a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs index dc86327bd..cecd0a4d2 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs @@ -101,8 +101,8 @@ 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) - => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, options); /// /// @@ -273,8 +273,8 @@ namespace Discord.Rest async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler) => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false); /// - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) - => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions) + => await SendMessageAsync(text, isTTS, embed, options, allowedMentions).ConfigureAwait(false); //IGuildChannel /// diff --git a/src/Discord.Net.Rest/Entities/Messages/AllowedMentions.cs b/src/Discord.Net.Rest/Entities/Messages/AllowedMentions.cs new file mode 100644 index 000000000..5ab96032f --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Messages/AllowedMentions.cs @@ -0,0 +1,15 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + public class AllowedMentions + { + [JsonProperty("parse")] + public Optional Parse { get; set; } + // Roles and Users have a max size of 100 + [JsonProperty("roles")] + public Optional Roles { get; set; } + [JsonProperty("users")] + public Optional Users { get; set; } + } +} diff --git a/src/Discord.Net.Rest/Extensions/EntityExtensions.cs b/src/Discord.Net.Rest/Extensions/EntityExtensions.cs index e265f991f..abdfc9d4f 100644 --- a/src/Discord.Net.Rest/Extensions/EntityExtensions.cs +++ b/src/Discord.Net.Rest/Extensions/EntityExtensions.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; @@ -60,6 +61,24 @@ namespace Discord.Rest model.Video = entity.Video.Value.ToModel(); return model; } + public static API.AllowedMentions ToModel(this AllowedMentions entity) + { + return new API.AllowedMentions() + { + Parse = entity.AllowedTypes?.EnumerateMentionTypes().ToArray(), + Roles = entity.RoleIds?.ToArray(), + Users = entity.UserIds?.ToArray(), + }; + } + public static IEnumerable EnumerateMentionTypes(this AllowedMentionTypes mentionTypes) + { + if (mentionTypes.HasFlag(AllowedMentionTypes.Everyone)) + yield return "everyone"; + if (mentionTypes.HasFlag(AllowedMentionTypes.Roles)) + yield return "roles"; + if (mentionTypes.HasFlag(AllowedMentionTypes.Users)) + yield return "users"; + } public static EmbedAuthor ToEntity(this API.EmbedAuthor model) { return new EmbedAuthor(model.Name, model.Url, model.IconUrl, model.ProxyIconUrl); diff --git a/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs index b88d0106b..378478dcc 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs @@ -29,11 +29,15 @@ namespace Discord.WebSocket /// 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. + /// /// /// 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); + new Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null); /// /// Sends a file to this message channel with an optional caption. /// diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs index 838fb8ef2..11259a31e 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs @@ -135,8 +135,8 @@ 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) - => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, options); /// public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false) @@ -235,8 +235,8 @@ namespace Discord.WebSocket async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler) => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false); /// - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) - => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions) + => await SendMessageAsync(text, isTTS, embed, options, allowedMentions).ConfigureAwait(false); //IChannel /// diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs index 26fcbe83c..c57c37db2 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs @@ -163,8 +163,8 @@ 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) - => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, options); /// public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false) @@ -299,8 +299,8 @@ namespace Discord.WebSocket async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler) => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false); /// - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) - => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions) + => await SendMessageAsync(text, isTTS, embed, options, allowedMentions).ConfigureAwait(false); //IAudioChannel /// diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs index ca7ca11dc..1b3b5bcd7 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs @@ -161,8 +161,8 @@ 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) - => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, options); /// public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false) @@ -308,8 +308,8 @@ namespace Discord.WebSocket async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler) => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false); /// - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) - => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions) + => await SendMessageAsync(text, isTTS, embed, options, allowedMentions).ConfigureAwait(false); // INestedChannel /// diff --git a/test/Discord.Net.Tests.Unit/MockedEntities/MockedDMChannel.cs b/test/Discord.Net.Tests.Unit/MockedEntities/MockedDMChannel.cs index 724bc84ef..c8d68fb4d 100644 --- a/test/Discord.Net.Tests.Unit/MockedEntities/MockedDMChannel.cs +++ b/test/Discord.Net.Tests.Unit/MockedEntities/MockedDMChannel.cs @@ -83,7 +83,7 @@ namespace Discord throw new NotImplementedException(); } - public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null) + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null) { throw new NotImplementedException(); } diff --git a/test/Discord.Net.Tests.Unit/MockedEntities/MockedGroupChannel.cs b/test/Discord.Net.Tests.Unit/MockedEntities/MockedGroupChannel.cs index 8b4e8b0d0..5a26b713f 100644 --- a/test/Discord.Net.Tests.Unit/MockedEntities/MockedGroupChannel.cs +++ b/test/Discord.Net.Tests.Unit/MockedEntities/MockedGroupChannel.cs @@ -91,7 +91,7 @@ namespace Discord throw new NotImplementedException(); } - public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null) + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null) { throw new NotImplementedException(); } diff --git a/test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs b/test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs index ca84219fd..a57c72899 100644 --- a/test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs +++ b/test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs @@ -177,7 +177,7 @@ namespace Discord throw new NotImplementedException(); } - public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null) + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null) { throw new NotImplementedException(); } From b6c981227d5adcb3af12e402af83ed92d76c0586 Mon Sep 17 00:00:00 2001 From: Arnav Borborah Date: Thu, 7 May 2020 09:14:37 -0400 Subject: [PATCH 09/19] docs: Use `Timeout.Infinite` instead of -1 so that intent is clearer (#1443) --- samples/01_basic_ping_bot/Program.cs | 3 ++- samples/02_commands_framework/Program.cs | 3 ++- samples/03_sharded_client/Program.cs | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/samples/01_basic_ping_bot/Program.cs b/samples/01_basic_ping_bot/Program.cs index 4d6674e97..7fbe04993 100644 --- a/samples/01_basic_ping_bot/Program.cs +++ b/samples/01_basic_ping_bot/Program.cs @@ -1,4 +1,5 @@ using System; +using System.Threading; using System.Threading.Tasks; using Discord; using Discord.WebSocket; @@ -43,7 +44,7 @@ namespace _01_basic_ping_bot await _client.StartAsync(); // Block the program until it is closed. - await Task.Delay(-1); + await Task.Delay(Timeout.Infinite); } private Task LogAsync(LogMessage log) diff --git a/samples/02_commands_framework/Program.cs b/samples/02_commands_framework/Program.cs index ccbc8e165..67cb87764 100644 --- a/samples/02_commands_framework/Program.cs +++ b/samples/02_commands_framework/Program.cs @@ -1,5 +1,6 @@ using System; using System.Net.Http; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Discord; @@ -45,7 +46,7 @@ namespace _02_commands_framework // Here we initialize the logic required to register our commands. await services.GetRequiredService().InitializeAsync(); - await Task.Delay(-1); + await Task.Delay(Timeout.Infinite); } } diff --git a/samples/03_sharded_client/Program.cs b/samples/03_sharded_client/Program.cs index 7a2f99168..753f400a1 100644 --- a/samples/03_sharded_client/Program.cs +++ b/samples/03_sharded_client/Program.cs @@ -1,4 +1,5 @@ using System; +using System.Threading; using System.Threading.Tasks; using _03_sharded_client.Services; using Discord; @@ -45,7 +46,7 @@ namespace _03_sharded_client await client.LoginAsync(TokenType.Bot, Environment.GetEnvironmentVariable("token")); await client.StartAsync(); - await Task.Delay(-1); + await Task.Delay(Timeout.Infinite); } } From c68cc85895341640b6a16611b8efdc589479b93b Mon Sep 17 00:00:00 2001 From: Joe4evr Date: Thu, 7 May 2020 15:16:57 +0200 Subject: [PATCH 10/19] feature: Add cache purging methods (#1478) --- .../Extensions/CollectionExtensions.cs | 4 ++-- src/Discord.Net.WebSocket/ClientState.cs | 23 +++++++++++++++++++ .../DiscordSocketClient.cs | 12 ++++++++++ .../Entities/Guilds/SocketGuild.cs | 22 ++++++++++++++++++ 4 files changed, 59 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net.Core/Extensions/CollectionExtensions.cs b/src/Discord.Net.Core/Extensions/CollectionExtensions.cs index e5d6025c2..f6ba7624d 100644 --- a/src/Discord.Net.Core/Extensions/CollectionExtensions.cs +++ b/src/Discord.Net.Core/Extensions/CollectionExtensions.cs @@ -1,4 +1,4 @@ -ο»Ώusing System; +using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; @@ -15,7 +15,7 @@ namespace Discord //public static IReadOnlyCollection ToReadOnlyCollection(this IReadOnlyDictionary source) // => new CollectionWrapper(source.Select(x => x.Value), () => source.Count); public static IReadOnlyCollection ToReadOnlyCollection(this IDictionary source) - => new CollectionWrapper(source.Select(x => x.Value), () => source.Count); + => new CollectionWrapper(source.Values, () => source.Count); public static IReadOnlyCollection ToReadOnlyCollection(this IEnumerable query, IReadOnlyCollection source) => new CollectionWrapper(query, () => source.Count); public static IReadOnlyCollection ToReadOnlyCollection(this IEnumerable query, Func countFunc) diff --git a/src/Discord.Net.WebSocket/ClientState.cs b/src/Discord.Net.WebSocket/ClientState.cs index dad185d66..f2e370d02 100644 --- a/src/Discord.Net.WebSocket/ClientState.cs +++ b/src/Discord.Net.WebSocket/ClientState.cs @@ -82,6 +82,20 @@ namespace Discord.WebSocket } return null; } + internal void PurgeAllChannels() + { + foreach (var guild in _guilds.Values) + guild.PurgeChannelCache(this); + + PurgeDMChannels(); + } + internal void PurgeDMChannels() + { + foreach (var channel in _dmChannels.Values) + _channels.TryRemove(channel.Id, out _); + + _dmChannels.Clear(); + } internal SocketGuild GetGuild(ulong id) { @@ -96,7 +110,11 @@ namespace Discord.WebSocket internal SocketGuild RemoveGuild(ulong id) { if (_guilds.TryRemove(id, out SocketGuild guild)) + { + guild.PurgeChannelCache(this); + guild.PurgeGuildUserCache(); return guild; + } return null; } @@ -116,5 +134,10 @@ namespace Discord.WebSocket return user; return null; } + internal void PurgeUsers() + { + foreach (var guild in _guilds.Values) + guild.PurgeGuildUserCache(); + } } } diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index 1819966b3..20c97bc98 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -306,6 +306,14 @@ namespace Discord.WebSocket /// public override SocketChannel GetChannel(ulong id) => State.GetChannel(id); + /// + /// Clears all cached channels from the client. + /// + public void PurgeChannelCache() => State.PurgeAllChannels(); + /// + /// Clears cached DM channels from the client. + /// + public void PurgeDMChannelCache() => State.PurgeDMChannels(); /// public override SocketUser GetUser(ulong id) @@ -313,6 +321,10 @@ namespace Discord.WebSocket /// public override SocketUser GetUser(string username, string discriminator) => State.Users.FirstOrDefault(x => x.Discriminator == discriminator && x.Username == username); + /// + /// Clears cached users from the client. + /// + public void PurgeUserCache() => State.PurgeUsers(); internal SocketGlobalUser GetOrCreateUser(ClientState state, Discord.API.User model) { return state.GetOrAddUser(model.Id, x => diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index da9a316eb..fb0a56c24 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -623,6 +623,13 @@ namespace Discord.WebSocket return state.RemoveChannel(id) as SocketGuildChannel; return null; } + internal void PurgeChannelCache(ClientState state) + { + foreach (var channelId in _channels) + state.RemoveChannel(channelId); + + _channels.Clear(); + } //Voice Regions /// @@ -797,6 +804,21 @@ namespace Discord.WebSocket } return null; } + internal void PurgeGuildUserCache() + { + var members = Users; + var self = CurrentUser; + _members.Clear(); + _members.TryAdd(self.Id, self); + + DownloadedMemberCount = _members.Count; + + foreach (var member in members) + { + if (member.Id != self.Id) + member.GlobalUser.RemoveRef(Discord); + } + } /// public async Task DownloadUsersAsync() From 03af8e0bb4b1b6fd43c7264064a7f6501346d657 Mon Sep 17 00:00:00 2001 From: TheKingEagle <30922258+rmsoftware-development@users.noreply.github.com> Date: Thu, 7 May 2020 09:19:15 -0400 Subject: [PATCH 11/19] fix: Call GuildAvailableAsync for dispatch(GUILD_CREATE) case (#1473) * Fix for Issue #1471 This change will allow `GuildAvailable` to fire when the client joins a new guild, as well as properly update `IsConnected`. * Removed unnecessary statement; --- src/Discord.Net.WebSocket/DiscordSocketClient.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index 20c97bc98..be7432bc3 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -640,6 +640,7 @@ namespace Discord.WebSocket if (guild != null) { await TimedInvokeAsync(_joinedGuildEvent, nameof(JoinedGuild), guild).ConfigureAwait(false); + await GuildAvailableAsync(guild).ConfigureAwait(false); } else { From 0c16d2f538a7cb9a683800014881dc60ec34dbb3 Mon Sep 17 00:00:00 2001 From: Joe4evr Date: Thu, 7 May 2020 15:21:30 +0200 Subject: [PATCH 12/19] misc: Use '??=' (rebased) (#1391) Co-authored-by: Christopher Felegy --- Discord.Net.targets | 1 + src/Discord.Net.Core/Utils/Comparers.cs | 11 +++++------ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Discord.Net.targets b/Discord.Net.targets index a5ab756d9..adb42f7a9 100644 --- a/Discord.Net.targets +++ b/Discord.Net.targets @@ -2,6 +2,7 @@ 2.3.0 dev + latest Discord.Net Contributors discord;discordapp https://github.com/RogueException/Discord.Net diff --git a/src/Discord.Net.Core/Utils/Comparers.cs b/src/Discord.Net.Core/Utils/Comparers.cs index 40500ffe3..7ec9f5c74 100644 --- a/src/Discord.Net.Core/Utils/Comparers.cs +++ b/src/Discord.Net.Core/Utils/Comparers.cs @@ -8,27 +8,26 @@ namespace Discord /// public static class DiscordComparers { - // TODO: simplify with '??=' slated for C# 8.0 /// /// Gets an to be used to compare users. /// - public static IEqualityComparer UserComparer => _userComparer ?? (_userComparer = new EntityEqualityComparer()); + public static IEqualityComparer UserComparer => _userComparer ??= new EntityEqualityComparer(); /// /// Gets an to be used to compare guilds. /// - public static IEqualityComparer GuildComparer => _guildComparer ?? (_guildComparer = new EntityEqualityComparer()); + public static IEqualityComparer GuildComparer => _guildComparer ??= new EntityEqualityComparer(); /// /// Gets an to be used to compare channels. /// - public static IEqualityComparer ChannelComparer => _channelComparer ?? (_channelComparer = new EntityEqualityComparer()); + public static IEqualityComparer ChannelComparer => _channelComparer ??= new EntityEqualityComparer(); /// /// Gets an to be used to compare roles. /// - public static IEqualityComparer RoleComparer => _roleComparer ?? (_roleComparer = new EntityEqualityComparer()); + public static IEqualityComparer RoleComparer => _roleComparer ??= new EntityEqualityComparer(); /// /// Gets an to be used to compare messages. /// - public static IEqualityComparer MessageComparer => _messageComparer ?? (_messageComparer = new EntityEqualityComparer()); + public static IEqualityComparer MessageComparer => _messageComparer ??= new EntityEqualityComparer(); private static IEqualityComparer _userComparer; private static IEqualityComparer _guildComparer; From d294678ed59f142a31aaec629cf39c65298511d5 Mon Sep 17 00:00:00 2001 From: FiniteReality Date: Mon, 18 May 2020 16:48:56 +0100 Subject: [PATCH 13/19] fix: use UtcNow when computing reset tick --- src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs b/src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs index 72dd1642d..09a12ee11 100644 --- a/src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs +++ b/src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs @@ -37,7 +37,7 @@ namespace Discord.Net.Queue _resetTick = null; LastAttemptAt = DateTimeOffset.UtcNow; } - + static int nextId = 0; public async Task SendAsync(RestRequest request) { @@ -249,7 +249,7 @@ namespace Discord.Net.Queue } else if (info.ResetAfter.HasValue && (request.Options.UseSystemClock.HasValue ? !request.Options.UseSystemClock.Value : false)) { - resetTick = DateTimeOffset.Now.Add(info.ResetAfter.Value); + resetTick = DateTimeOffset.UtcNow.Add(info.ResetAfter.Value); } else if (info.Reset.HasValue) { From 08d9834e2cb1ec37275d7c3b2ec7f32f0ce56d24 Mon Sep 17 00:00:00 2001 From: FiniteReality Date: Mon, 18 May 2020 18:01:23 +0100 Subject: [PATCH 14/19] fix: Ensure resetAt is in the future If the current reset time is in the past, then somebody else in the current bucket must have made a request before we were able to. To prevent accidental ratelimits, we fall-back to the second sleep branch, as if the reset time wasn't specified at all. Additionally Extracts the minimum sleep time to a constant, and also bumps it to 750ms. --- src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs b/src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs index 09a12ee11..771923cd4 100644 --- a/src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs +++ b/src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs @@ -13,6 +13,8 @@ namespace Discord.Net.Queue { internal class RequestBucket { + private const int MinimumSleepTimeMs = 750; + private readonly object _lock; private readonly RequestQueue _queue; private int _semaphore; @@ -183,10 +185,11 @@ namespace Discord.Net.Queue ThrowRetryLimit(request); - if (resetAt.HasValue) + if (resetAt.HasValue && resetAt > DateTimeOffset.UtcNow) { if (resetAt > timeoutAt) ThrowRetryLimit(request); + int millis = (int)Math.Ceiling((resetAt.Value - DateTimeOffset.UtcNow).TotalMilliseconds); #if DEBUG_LIMITS Debug.WriteLine($"[{id}] Sleeping {millis} ms (Pre-emptive)"); @@ -196,12 +199,12 @@ namespace Discord.Net.Queue } else { - if ((timeoutAt.Value - DateTimeOffset.UtcNow).TotalMilliseconds < 500.0) + if ((timeoutAt.Value - DateTimeOffset.UtcNow).TotalMilliseconds < MinimumSleepTimeMs) ThrowRetryLimit(request); #if DEBUG_LIMITS - Debug.WriteLine($"[{id}] Sleeping 500* ms (Pre-emptive)"); + Debug.WriteLine($"[{id}] Sleeping {MinimumSleepTimeMs}* ms (Pre-emptive)"); #endif - await Task.Delay(500, request.Options.CancelToken).ConfigureAwait(false); + await Task.Delay(MinimumSleepTimeMs, request.Options.CancelToken).ConfigureAwait(false); } continue; } From 91b270a0ce76849376f88d204fcb53ddd834070e Mon Sep 17 00:00:00 2001 From: Paulo Date: Wed, 20 May 2020 18:25:49 -0300 Subject: [PATCH 15/19] fix: handle GUILD_DELETE behavior correctly (#1542) --- src/Discord.Net.WebSocket/DiscordSocketClient.cs | 12 +----------- .../Entities/Guilds/SocketGuild.cs | 5 +++-- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index be7432bc3..b56498061 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -1771,17 +1771,7 @@ namespace Discord.WebSocket return guild; } internal SocketGuild RemoveGuild(ulong id) - { - var guild = State.RemoveGuild(id); - if (guild != null) - { - foreach (var _ in guild.Channels) - State.RemoveChannel(id); - foreach (var user in guild.Users) - user.GlobalUser.RemoveRef(this); - } - return guild; - } + => State.RemoveGuild(id); /// Unexpected channel type is created. internal ISocketPrivateChannel AddPrivateChannel(API.Channel model, ClientState state) diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index fb0a56c24..e556853f2 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -809,13 +809,14 @@ namespace Discord.WebSocket var members = Users; var self = CurrentUser; _members.Clear(); - _members.TryAdd(self.Id, self); + if (self != null) + _members.TryAdd(self.Id, self); DownloadedMemberCount = _members.Count; foreach (var member in members) { - if (member.Id != self.Id) + if (member.Id != self?.Id) member.GlobalUser.RemoveRef(Discord); } } From a6c1e4c23f71682cba8da2a19038d257b83bd6be Mon Sep 17 00:00:00 2001 From: Matt Smith Date: Wed, 20 May 2020 16:28:23 -0500 Subject: [PATCH 16/19] (ifcbrk) feature: news channel publishing (#1530) * Added PublishAsync to Messages. * Added missing implementation. * 1. Aligned with naming standards 2. Clarified xml docs 3. Properly threw exceptions instead of failing silently. * Additional documentation included. * Removed un-needed comments. Co-authored-by: Matt Smith --- .../Entities/Messages/IUserMessage.cs | 15 +++++++++++++++ src/Discord.Net.Rest/DiscordRestApiClient.cs | 9 +++++++++ .../Entities/Messages/MessageHelper.cs | 15 +++++++++++++++ .../Entities/Messages/RestMessage.cs | 2 +- .../Entities/Messages/RestUserMessage.cs | 12 ++++++++++++ .../Entities/Messages/SocketUserMessage.cs | 16 ++++++++++++++-- 6 files changed, 66 insertions(+), 3 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs b/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs index bc52dd01c..e2fb25aae 100644 --- a/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs +++ b/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs @@ -57,6 +57,21 @@ namespace Discord /// Task UnpinAsync(RequestOptions options = null); + /// + /// Publishes (crossposts) this message. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous operation for publishing this message. + /// + /// + /// + /// This call will throw an if attempted in a non-news channel. + /// + /// This method will publish (crosspost) the message. Please note, publishing (crossposting), is only available in news channels. + /// + Task CrosspostAsync(RequestOptions options = null); + /// /// Transforms this message's text into a human-readable form by resolving its tags. /// diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index a726ef75d..732cb5f17 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -695,6 +695,15 @@ namespace Discord.API var ids = new BucketIds(channelId: channelId); await SendAsync("POST", () => $"channels/{channelId}/typing", ids, options: options).ConfigureAwait(false); } + public async Task CrosspostAsync(ulong channelId, ulong messageId, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotEqual(messageId, 0, nameof(messageId)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(channelId: channelId); + await SendAsync("POST", () => $"channels/{channelId}/messages/{messageId}/crosspost", ids, options: options).ConfigureAwait(false); + } //Channel Permissions public async Task ModifyChannelPermissionsAsync(ulong channelId, ulong targetId, ModifyChannelPermissionsParams args, RequestOptions options = null) diff --git a/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs b/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs index b29eca62e..57f8b2509 100644 --- a/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs +++ b/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs @@ -44,8 +44,10 @@ namespace Discord.Rest }; return await client.ApiClient.ModifyMessageAsync(msg.Channel.Id, msg.Id, apiArgs, options).ConfigureAwait(false); } + public static Task DeleteAsync(IMessage msg, BaseDiscordClient client, RequestOptions options) => DeleteAsync(msg.Channel.Id, msg.Id, client, options); + public static async Task DeleteAsync(ulong channelId, ulong msgId, BaseDiscordClient client, RequestOptions options) { @@ -115,6 +117,7 @@ namespace Discord.Rest { await client.ApiClient.AddPinAsync(msg.Channel.Id, msg.Id, options).ConfigureAwait(false); } + public static async Task UnpinAsync(IMessage msg, BaseDiscordClient client, RequestOptions options) { @@ -240,6 +243,7 @@ namespace Discord.Rest return tags.ToImmutable(); } + private static int? FindIndex(IReadOnlyList tags, int index) { int i = 0; @@ -253,6 +257,7 @@ namespace Discord.Rest return null; //Overlaps tag before this return i; } + public static ImmutableArray FilterTagsByKey(TagType type, ImmutableArray tags) { return tags @@ -260,6 +265,7 @@ namespace Discord.Rest .Select(x => x.Key) .ToImmutableArray(); } + public static ImmutableArray FilterTagsByValue(TagType type, ImmutableArray tags) { return tags @@ -279,5 +285,14 @@ namespace Discord.Rest return MessageSource.Bot; return MessageSource.User; } + + public static Task CrosspostAsync(IMessage msg, BaseDiscordClient client, RequestOptions options) + => CrosspostAsync(msg.Channel.Id, msg.Id, client, options); + + public static async Task CrosspostAsync(ulong channelId, ulong msgId, BaseDiscordClient client, + RequestOptions options) + { + await client.ApiClient.CrosspostAsync(channelId, msgId, options).ConfigureAwait(false); + } } } diff --git a/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs b/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs index f457f4f7a..b4a33c76c 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs @@ -165,7 +165,7 @@ namespace Discord.Rest IReadOnlyCollection IMessage.Embeds => Embeds; /// IReadOnlyCollection IMessage.MentionedUserIds => MentionedUsers.Select(x => x.Id).ToImmutableArray(); - + /// public IReadOnlyDictionary Reactions => _reactions.ToDictionary(x => x.Emote, x => new ReactionMetadata { ReactionCount = x.Count, IsMe = x.Me }); diff --git a/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs b/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs index 7d652687a..ad2a65615 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs @@ -148,6 +148,18 @@ namespace Discord.Rest TagHandling roleHandling = TagHandling.Name, TagHandling everyoneHandling = TagHandling.Ignore, TagHandling emojiHandling = TagHandling.Name) => MentionUtils.Resolve(this, 0, userHandling, channelHandling, roleHandling, everyoneHandling, emojiHandling); + /// + /// This operation may only be called on a channel. + public async Task CrosspostAsync(RequestOptions options = null) + { + if (!(Channel is RestNewsChannel)) + { + throw new InvalidOperationException("Publishing (crossposting) is only valid in news channels."); + } + + await MessageHelper.CrosspostAsync(this, Discord, options); + } + private string DebuggerDisplay => $"{Author}: {Content} ({Id}{(Attachments.Count > 0 ? $", {Attachments.Count} Attachments" : "")})"; } } diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs index b26dfe5fb..e1f0f74dc 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs @@ -123,7 +123,7 @@ namespace Discord.WebSocket model.Content = text; } } - + /// /// Only the author of a message may modify the message. /// Message content is too long, length must be less or equal to . @@ -147,7 +147,19 @@ namespace Discord.WebSocket public string Resolve(TagHandling userHandling = TagHandling.Name, TagHandling channelHandling = TagHandling.Name, TagHandling roleHandling = TagHandling.Name, TagHandling everyoneHandling = TagHandling.Ignore, TagHandling emojiHandling = TagHandling.Name) => MentionUtils.Resolve(this, 0, userHandling, channelHandling, roleHandling, everyoneHandling, emojiHandling); - + + /// + /// This operation may only be called on a channel. + public async Task CrosspostAsync(RequestOptions options = null) + { + if (!(Channel is SocketNewsChannel)) + { + throw new InvalidOperationException("Publishing (crossposting) is only valid in news channels."); + } + + await MessageHelper.CrosspostAsync(this, Discord, options); + } + private string DebuggerDisplay => $"{Author}: {Content} ({Id}{(Attachments.Count > 0 ? $", {Attachments.Count} Attachments" : "")})"; internal new SocketUserMessage Clone() => MemberwiseClone() as SocketUserMessage; } From 758578955ec577cb54df47f874e2a5ebdaf5a86b Mon Sep 17 00:00:00 2001 From: moiph Date: Sun, 24 May 2020 20:36:05 -0700 Subject: [PATCH 17/19] misc: update webhook regex to support discord.com (#1551) * Updating webhook regex for discord.com Updates webhook URL regex matching for discordapp.com and discord.com * Fixing comment * Whitespace --- src/Discord.Net.Webhook/DiscordWebhookClient.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Discord.Net.Webhook/DiscordWebhookClient.cs b/src/Discord.Net.Webhook/DiscordWebhookClient.cs index 9c90df565..353345ded 100644 --- a/src/Discord.Net.Webhook/DiscordWebhookClient.cs +++ b/src/Discord.Net.Webhook/DiscordWebhookClient.cs @@ -33,7 +33,7 @@ namespace Discord.Webhook : this(webhookUrl, new DiscordRestConfig()) { } // regex pattern to match webhook urls - private static Regex WebhookUrlRegex = new Regex(@"^.*discordapp\.com\/api\/webhooks\/([\d]+)\/([a-z0-9_-]+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static Regex WebhookUrlRegex = new Regex(@"^.*(discord|discordapp)\.com\/api\/webhooks\/([\d]+)\/([a-z0-9_-]+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase); /// Creates a new Webhook Discord client. public DiscordWebhookClient(ulong webhookId, string webhookToken, DiscordRestConfig config) @@ -132,13 +132,13 @@ namespace Discord.Webhook if (match != null) { // ensure that the first group is a ulong, set the _webhookId - // 0th group is always the entire match, so start at index 1 - if (!(match.Groups[1].Success && ulong.TryParse(match.Groups[1].Value, NumberStyles.None, CultureInfo.InvariantCulture, out webhookId))) + // 0th group is always the entire match, and 1 is the domain; so start at index 2 + if (!(match.Groups[2].Success && ulong.TryParse(match.Groups[2].Value, NumberStyles.None, CultureInfo.InvariantCulture, out webhookId))) throw ex("The webhook Id could not be parsed."); - if (!match.Groups[2].Success) + if (!match.Groups[3].Success) throw ex("The webhook token could not be parsed."); - webhookToken = match.Groups[2].Value; + webhookToken = match.Groups[3].Value; } else throw ex(); From 30b5a833d25e794ecbf31ef3490d3458d0d721db Mon Sep 17 00:00:00 2001 From: Paulo Date: Mon, 25 May 2020 00:37:21 -0300 Subject: [PATCH 18/19] feature: Add GetUsersAsync to SocketGuild (#1549) * Add GetUsersAsync to SocketGuild * Fix IGuild return * Do not download unless needed --- .../Entities/Guilds/IGuild.cs | 3 ++ .../Entities/Guilds/SocketGuild.cs | 28 +++++++++++++++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs index b39a49776..cdf0118c4 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs @@ -683,6 +683,9 @@ namespace Discord /// /// Downloads all users for this guild if the current list is incomplete. /// + /// + /// This method downloads all users found within this guild throught the Gateway and caches them. + /// /// /// A task that represents the asynchronous download operation. /// diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index e556853f2..cdba2b67d 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -821,6 +821,25 @@ namespace Discord.WebSocket } } + /// + /// Gets a collection of all users in this guild. + /// + /// + /// This method retrieves all users found within this guild throught REST. + /// Users returned by this method are not cached. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a collection of guild + /// users found within this guild. + /// + public IAsyncEnumerable> GetUsersAsync(RequestOptions options = null) + { + if (HasAllMembers) + return ImmutableArray.Create(Users).ToAsyncEnumerable>(); + return GuildHelper.GetUsersAsync(this, Discord, null, null, options); + } + /// public async Task DownloadUsersAsync() { @@ -1185,8 +1204,13 @@ namespace Discord.WebSocket => await CreateRoleAsync(name, permissions, color, isHoisted, isMentionable, options).ConfigureAwait(false); /// - Task> IGuild.GetUsersAsync(CacheMode mode, RequestOptions options) - => Task.FromResult>(Users); + async Task> IGuild.GetUsersAsync(CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload && !HasAllMembers) + return (await GetUsersAsync(options).FlattenAsync().ConfigureAwait(false)).ToImmutableArray(); + else + return Users; + } /// async Task IGuild.AddGuildUserAsync(ulong userId, string accessToken, Action func, RequestOptions options) From 323a6775ee496e07329d7e2f35eaa38cd0992ccc Mon Sep 17 00:00:00 2001 From: Paulo Date: Mon, 25 May 2020 00:38:25 -0300 Subject: [PATCH 19/19] misc: MutualGuilds optimization (#1545) * Check Dictionary Check Dictionary instead of creating a new IReadOnlyCollection and looping in it * Add Remark to MutualGuilds --- src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs index 09c4165f4..b830ce79c 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs @@ -44,8 +44,11 @@ namespace Discord.WebSocket /// /// Gets mutual guilds shared with this user. /// + /// + /// This property will only include guilds in the same . + /// public IReadOnlyCollection MutualGuilds - => Discord.Guilds.Where(g => g.Users.Any(u => u.Id == Id)).ToImmutableArray(); + => Discord.Guilds.Where(g => g.GetUser(Id) != null).ToImmutableArray(); internal SocketUser(DiscordSocketClient discord, ulong id) : base(discord, id)