From 7b353f428c27de64316534371a7b09acdc28fc6d Mon Sep 17 00:00:00 2001 From: VollRahm Date: Thu, 5 Nov 2020 01:11:28 +0100 Subject: [PATCH] - Updated the project to Discord.NET 2.3.0-dev --- .../Builders/ModuleClassBuilder.cs | 3 +- .../Discord.Net.Commands/CommandService.cs | 2 +- .../CommandServiceConfig.cs | 2 +- .../Discord.Net.Commands/ModuleBase.cs | 8 +- Discord.NET/Discord.Net.Core/AssemblyInfo.cs | 10 - Discord.NET/Discord.Net.Core/DiscordConfig.cs | 4 +- .../Entities/AuditLogs/ActionType.cs | 26 ++- .../Channels/GuildChannelProperties.cs | 8 +- .../Entities/Channels/IMessageChannel.cs | 18 +- .../Entities/Channels/INestedChannel.cs | 8 +- .../Entities/Channels/ITextChannel.cs | 4 +- .../Entities/Guilds/IGuild.cs | 21 ++- .../Entities/Messages/AllowedMentionTypes.cs | 34 ++++ .../Entities/Messages/AllowedMentions.cs | 64 +++++++ .../Entities/Messages/IMessage.cs | 45 ++++- .../Entities/Messages/IUserMessage.cs | 17 +- .../Permissions/OverwritePermissions.cs | 22 ++- .../Entities/Users/IGuildUser.cs | 4 +- .../Entities/Users/IPresence.cs | 4 + .../Discord.Net.Core/Entities/Users/IUser.cs | 8 +- .../Extensions/CollectionExtensions.cs | 4 +- .../Extensions/MessageExtensions.cs | 8 +- .../Extensions/UserExtensions.cs | 13 +- .../Discord.Net.Core/GatewayIntents.cs | 43 +++++ .../Discord.Net.Core/IDiscordClient.cs | 2 +- Discord.NET/Discord.Net.Core/Net/BucketId.cs | 118 ++++++++++++ .../Discord.Net.Core/Net/HttpException.cs | 4 +- .../Net/WebSocketClosedException.cs | 2 +- .../Net/WebSockets/IWebSocketClient.cs | 2 +- .../Discord.Net.Core/RequestOptions.cs | 6 +- Discord.NET/Discord.Net.Core/TokenType.cs | 1 + .../Discord.Net.Core/Utils/Comparers.cs | 28 ++- .../API/Common/AuditLogOptions.cs | 7 +- .../Discord.Net.Rest/API/Common/Message.cs | 2 + .../Discord.Net.Rest/API/Common/Presence.cs | 5 + .../API/Rest/CreateGuildChannelParams.cs | 4 + .../API/Rest/CreateMessageParams.cs | 4 +- .../API/Rest/CreateWebhookMessageParams.cs | 4 +- .../API/Rest/SearchGuildMembersParams.cs | 9 + .../API/Rest/UploadFileParams.cs | 3 + .../API/Rest/UploadWebhookFileParams.cs | 3 + .../Discord.Net.Rest/BaseDiscordClient.cs | 4 +- .../Discord.Net.Rest/DiscordRestApiClient.cs | 171 ++++++++++++------ .../Entities/AuditLogs/AuditLogHelper.cs | 6 + .../AuditLogs/DataTypes/BotAddAuditLogData.cs | 32 ++++ .../DataTypes/ChannelCreateAuditLogData.cs | 21 +-- .../DataTypes/ChannelDeleteAuditLogData.cs | 4 +- .../AuditLogs/DataTypes/ChannelInfo.cs | 4 +- .../DataTypes/InviteCreateAuditLogData.cs | 14 +- .../DataTypes/InviteDeleteAuditLogData.cs | 14 +- .../DataTypes/MemberDisconnectAuditLogData.cs | 29 +++ .../DataTypes/MemberMoveAuditLogData.cs | 37 ++++ .../MessageBulkDeleteAuditLogData.cs | 38 ++++ .../DataTypes/MessageDeleteAuditLogData.cs | 15 +- .../DataTypes/MessagePinAuditLogData.cs | 48 +++++ .../DataTypes/MessageUnpinAuditLogData.cs | 48 +++++ .../DataTypes/OverwriteDeleteAuditLogData.cs | 11 +- .../Entities/Channels/ChannelHelper.cs | 97 ++++++++-- .../Entities/Channels/IRestMessageChannel.cs | 24 ++- .../Entities/Channels/RestDMChannel.cs | 24 +-- .../Entities/Channels/RestGroupChannel.cs | 25 +-- .../Entities/Channels/RestTextChannel.cs | 27 +-- .../Entities/Guilds/GuildHelper.cs | 68 +++++-- .../Entities/Guilds/RestGuild.cs | 25 +++ .../Entities/Messages/AllowedMentions.cs | 15 ++ .../Entities/Messages/MessageHelper.cs | 25 +++ .../Entities/Messages/RestMessage.cs | 8 +- .../Entities/Messages/RestUserMessage.cs | 29 ++- .../Entities/Users/RestUser.cs | 2 + .../Extensions/EntityExtensions.cs | 19 ++ .../Net/Queue/ClientBucket.cs | 14 +- .../Net/Queue/RequestQueue.cs | 39 +++- .../Net/Queue/RequestQueueBucket.cs | 87 ++++++--- .../Discord.Net.Rest/Net/RateLimitInfo.cs | 8 +- .../API/Gateway/IdentifyParams.cs | 4 +- .../RemoveAllReactionsForEmoteEvent.cs | 16 ++ .../Discord.Net.WebSocket/AssemblyInfo.cs | 5 - .../BaseSocketClient.Events.cs | 22 +++ .../Discord.Net.WebSocket/ClientState.cs | 23 +++ .../ConnectionManager.cs | 13 +- .../DiscordShardedClient.cs | 8 +- .../DiscordSocketApiClient.cs | 28 ++- .../DiscordSocketClient.Events.cs | 8 +- .../DiscordSocketClient.cs | 94 +++++++--- .../DiscordSocketConfig.cs | 38 +++- .../Channels/ISocketMessageChannel.cs | 22 ++- .../Entities/Channels/SocketChannelHelper.cs | 43 +++-- .../Entities/Channels/SocketDMChannel.cs | 24 +-- .../Entities/Channels/SocketGroupChannel.cs | 24 +-- .../Entities/Channels/SocketGuildChannel.cs | 20 -- .../Entities/Channels/SocketTextChannel.cs | 24 +-- .../Entities/Guilds/SocketGuild.cs | 85 ++++++++- .../Entities/Messages/MessageCache.cs | 14 +- .../Entities/Messages/SocketMessage.cs | 11 +- .../Entities/Messages/SocketUserMessage.cs | 43 +++-- .../Entities/Users/SocketGuildUser.cs | 2 + .../Entities/Users/SocketPresence.cs | 31 +++- .../Entities/Users/SocketUnknownUser.cs | 2 +- .../Entities/Users/SocketUser.cs | 7 +- .../Entities/Users/SocketWebhookUser.cs | 2 +- .../GatewayReconnectException.cs | 22 +++ .../Net/DefaultWebSocketClient.cs | 13 +- .../DiscordWebhookClient.cs | 30 +-- .../WebhookClientHelper.cs | 12 +- 104 files changed, 1814 insertions(+), 492 deletions(-) delete mode 100644 Discord.NET/Discord.Net.Core/AssemblyInfo.cs create mode 100644 Discord.NET/Discord.Net.Core/Entities/Messages/AllowedMentionTypes.cs create mode 100644 Discord.NET/Discord.Net.Core/Entities/Messages/AllowedMentions.cs create mode 100644 Discord.NET/Discord.Net.Core/GatewayIntents.cs create mode 100644 Discord.NET/Discord.Net.Core/Net/BucketId.cs create mode 100644 Discord.NET/Discord.Net.Rest/API/Rest/SearchGuildMembersParams.cs create mode 100644 Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BotAddAuditLogData.cs create mode 100644 Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberDisconnectAuditLogData.cs create mode 100644 Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberMoveAuditLogData.cs create mode 100644 Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageBulkDeleteAuditLogData.cs create mode 100644 Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessagePinAuditLogData.cs create mode 100644 Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageUnpinAuditLogData.cs create mode 100644 Discord.NET/Discord.Net.Rest/Entities/Messages/AllowedMentions.cs create mode 100644 Discord.NET/Discord.Net.WebSocket/API/Gateway/RemoveAllReactionsForEmoteEvent.cs delete mode 100644 Discord.NET/Discord.Net.WebSocket/AssemblyInfo.cs create mode 100644 Discord.NET/Discord.Net.WebSocket/GatewayReconnectException.cs diff --git a/Discord.NET/Discord.Net.Commands/Builders/ModuleClassBuilder.cs b/Discord.NET/Discord.Net.Commands/Builders/ModuleClassBuilder.cs index aec8dcb..28037b0 100644 --- a/Discord.NET/Discord.Net.Commands/Builders/ModuleClassBuilder.cs +++ b/Discord.NET/Discord.Net.Commands/Builders/ModuleClassBuilder.cs @@ -135,7 +135,8 @@ namespace Discord.Commands if (builder.Name == null) builder.Name = typeInfo.Name; - var validCommands = typeInfo.DeclaredMethods.Where(IsValidCommandDefinition); + // Get all methods (including from inherited members), that are valid commands + var validCommands = typeInfo.GetMethods().Where(IsValidCommandDefinition); foreach (var method in validCommands) { diff --git a/Discord.NET/Discord.Net.Commands/CommandService.cs b/Discord.NET/Discord.Net.Commands/CommandService.cs index d5c060f..1d4b0e1 100644 --- a/Discord.NET/Discord.Net.Commands/CommandService.cs +++ b/Discord.NET/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/Discord.NET/Discord.Net.Commands/CommandServiceConfig.cs b/Discord.NET/Discord.Net.Commands/CommandServiceConfig.cs index 2dedcea..3c62063 100644 --- a/Discord.NET/Discord.Net.Commands/CommandServiceConfig.cs +++ b/Discord.NET/Discord.Net.Commands/CommandServiceConfig.cs @@ -44,7 +44,7 @@ namespace Discord.Commands /// /// /// - /// QuotationMarkAliasMap = new Dictionary<char, char%gt;() + /// QuotationMarkAliasMap = new Dictionary<char, char>() /// { /// {'\"', '\"' }, /// {'β€œ', '”' }, diff --git a/Discord.NET/Discord.Net.Commands/ModuleBase.cs b/Discord.NET/Discord.Net.Commands/ModuleBase.cs index 9cd4ea1..ec1722c 100644 --- a/Discord.NET/Discord.Net.Commands/ModuleBase.cs +++ b/Discord.NET/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/Discord.NET/Discord.Net.Core/AssemblyInfo.cs b/Discord.NET/Discord.Net.Core/AssemblyInfo.cs deleted file mode 100644 index b7c60f3..0000000 --- a/Discord.NET/Discord.Net.Core/AssemblyInfo.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Runtime.CompilerServices; - -[assembly: InternalsVisibleTo("Discord.Net.Relay")] -[assembly: InternalsVisibleTo("Discord.Net.Rest")] -[assembly: InternalsVisibleTo("Discord.Net.Rpc")] -[assembly: InternalsVisibleTo("Discord.Net.WebSocket")] -[assembly: InternalsVisibleTo("Discord.Net.Webhook")] -[assembly: InternalsVisibleTo("Discord.Net.Commands")] -[assembly: InternalsVisibleTo("Discord.Net.Tests")] -[assembly: InternalsVisibleTo("Discord.Net.Tests.Unit")] diff --git a/Discord.NET/Discord.Net.Core/DiscordConfig.cs b/Discord.NET/Discord.Net.Core/DiscordConfig.cs index 51970a7..429ad7b 100644 --- a/Discord.NET/Discord.Net.Core/DiscordConfig.cs +++ b/Discord.NET/Discord.Net.Core/DiscordConfig.cs @@ -13,7 +13,7 @@ namespace Discord /// /// An representing the API version that Discord.Net uses to communicate with Discord. /// A list of available API version can be seen on the official - /// Discord API documentation + /// Discord API documentation /// . /// public const int APIVersion = 6; @@ -50,7 +50,7 @@ namespace Discord /// /// The Discord API URL using . /// - public static readonly string APIUrl = $"https://discordapp.com/api/v{APIVersion}/"; + public static readonly string APIUrl = $"https://discord.com/api/v{APIVersion}/"; /// /// Returns the base Discord CDN URL. /// diff --git a/Discord.NET/Discord.Net.Core/Entities/AuditLogs/ActionType.cs b/Discord.NET/Discord.Net.Core/Entities/AuditLogs/ActionType.cs index 2561a09..1728b20 100644 --- a/Discord.NET/Discord.Net.Core/Entities/AuditLogs/ActionType.cs +++ b/Discord.NET/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/Discord.NET/Discord.Net.Core/Entities/Channels/GuildChannelProperties.cs b/Discord.NET/Discord.Net.Core/Entities/Channels/GuildChannelProperties.cs index 9552b0a..339d6ff 100644 --- a/Discord.NET/Discord.Net.Core/Entities/Channels/GuildChannelProperties.cs +++ b/Discord.NET/Discord.Net.Core/Entities/Channels/GuildChannelProperties.cs @@ -1,3 +1,5 @@ +using System.Collections.Generic; + namespace Discord { /// @@ -26,9 +28,13 @@ namespace Discord /// /// /// Setting this value to a category's snowflake identifier will change or set this channel's parent to the - /// specified channel; setting this value to 0 will detach this channel from its parent if one + /// specified channel; setting this value to will detach this channel from its parent if one /// is set. /// public Optional CategoryId { get; set; } + /// + /// Gets or sets the permission overwrites for this channel. + /// + public Optional> PermissionOverwrites { get; set; } } } diff --git a/Discord.NET/Discord.Net.Core/Entities/Channels/IMessageChannel.cs b/Discord.NET/Discord.Net.Core/Entities/Channels/IMessageChannel.cs index b5aa69d..030a278 100644 --- a/Discord.NET/Discord.Net.Core/Entities/Channels/IMessageChannel.cs +++ b/Discord.NET/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. /// @@ -55,11 +59,15 @@ namespace Discord /// The to be sent. /// The options to be used when sending the request. /// Whether the message attachment should be hidden as a spoiler. + /// + /// Specifies if notifications are sent for mentioned users and roles in the message . + /// If null, all mentioned roles and users will be notified. + /// /// /// A task that represents an asynchronous send operation for delivering the message. The task result /// contains the sent message. /// - Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false); + Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null); /// /// Sends a file to this message channel with an optional caption. /// @@ -84,11 +92,15 @@ namespace Discord /// The to be sent. /// The options to be used when sending the request. /// Whether the message attachment should be hidden as a spoiler. + /// + /// Specifies if notifications are sent for mentioned users and roles in the message . + /// If null, all mentioned roles and users will be notified. + /// /// /// A task that represents an asynchronous send operation for delivering the message. The task result /// contains the sent message. /// - Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false); + Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null); /// /// Gets a message from this message channel. diff --git a/Discord.NET/Discord.Net.Core/Entities/Channels/INestedChannel.cs b/Discord.NET/Discord.Net.Core/Entities/Channels/INestedChannel.cs index e38e1db..2c9503d 100644 --- a/Discord.NET/Discord.Net.Core/Entities/Channels/INestedChannel.cs +++ b/Discord.NET/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/Discord.NET/Discord.Net.Core/Entities/Channels/ITextChannel.cs b/Discord.NET/Discord.Net.Core/Entities/Channels/ITextChannel.cs index 29c764e..a2baf69 100644 --- a/Discord.NET/Discord.Net.Core/Entities/Channels/ITextChannel.cs +++ b/Discord.NET/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/Discord.NET/Discord.Net.Core/Entities/Guilds/IGuild.cs b/Discord.NET/Discord.Net.Core/Entities/Guilds/IGuild.cs index a18e91b..81b5e8d 100644 --- a/Discord.NET/Discord.Net.Core/Entities/Guilds/IGuild.cs +++ b/Discord.NET/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. /// /// @@ -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. /// @@ -707,6 +710,22 @@ namespace Discord /// be or has been removed from this guild. /// Task PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null); + /// + /// Gets a collection of users in this guild that the name or nickname starts with the + /// provided at . + /// + /// + /// The can not be higher than . + /// + /// The partial name or nickname to search. + /// The maximum number of users to be gotten. + /// The that determines whether the object should be fetched from cache. + /// 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 that the name or nickname starts with the provided at . + /// + Task> SearchUsersAsync(string query, int limit = DiscordConfig.MaxUsersPerBatch, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); /// /// Gets the specified number of audit log entries for this guild. diff --git a/Discord.NET/Discord.Net.Core/Entities/Messages/AllowedMentionTypes.cs b/Discord.NET/Discord.Net.Core/Entities/Messages/AllowedMentionTypes.cs new file mode 100644 index 0000000..ecd872d --- /dev/null +++ b/Discord.NET/Discord.Net.Core/Entities/Messages/AllowedMentionTypes.cs @@ -0,0 +1,34 @@ +using System; + +namespace Discord +{ + /// + /// Specifies the type of mentions that will be notified from the message content. + /// + [Flags] + public enum AllowedMentionTypes + { + /// + /// No flag is set. + /// + /// + /// This flag is not used to control mentions. + /// + /// It will always be present and does not mean mentions will not be allowed. + /// + /// + None = 0, + /// + /// Controls role mentions. + /// + Roles = 1, + /// + /// Controls user mentions. + /// + Users = 2, + /// + /// Controls @everyone and @here mentions. + /// + Everyone = 4, + } +} diff --git a/Discord.NET/Discord.Net.Core/Entities/Messages/AllowedMentions.cs b/Discord.NET/Discord.Net.Core/Entities/Messages/AllowedMentions.cs new file mode 100644 index 0000000..d52feaa --- /dev/null +++ b/Discord.NET/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; } = new List(); + + /// + /// 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; } = new List(); + + /// + /// 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/Discord.NET/Discord.Net.Core/Entities/Messages/IMessage.cs b/Discord.NET/Discord.Net.Core/Entities/Messages/IMessage.cs index 05f5052..b74e333 100644 --- a/Discord.NET/Discord.Net.Core/Entities/Messages/IMessage.cs +++ b/Discord.NET/Discord.Net.Core/Entities/Messages/IMessage.cs @@ -39,6 +39,13 @@ namespace Discord /// bool IsSuppressed { get; } /// + /// Gets the value that indicates whether this message mentioned everyone. + /// + /// + /// true if this message mentioned everyone; otherwise false. + /// + bool MentionedEveryone { get; } + /// /// Gets the content for this message. /// /// @@ -161,7 +168,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 +184,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 +201,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); /// @@ -215,12 +222,38 @@ namespace Discord /// A task that represents the asynchronous removal operation. /// Task RemoveAllReactionsAsync(RequestOptions options = null); + /// + /// Removes all reactions with a specific emoji from this message. + /// + /// The emoji used to react to this message. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous removal operation. + /// + Task RemoveAllReactionsForEmoteAsync(IEmote emote, RequestOptions options = null); /// /// 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 +263,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/Discord.NET/Discord.Net.Core/Entities/Messages/IUserMessage.cs b/Discord.NET/Discord.Net.Core/Entities/Messages/IUserMessage.cs index be2523b..e2fb25a 100644 --- a/Discord.NET/Discord.Net.Core/Entities/Messages/IUserMessage.cs +++ b/Discord.NET/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!"); /// @@ -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/Discord.NET/Discord.Net.Core/Entities/Permissions/OverwritePermissions.cs b/Discord.NET/Discord.Net.Core/Entities/Permissions/OverwritePermissions.cs index 04bb2f6..7876d49 100644 --- a/Discord.NET/Discord.Net.Core/Entities/Permissions/OverwritePermissions.cs +++ b/Discord.NET/Discord.Net.Core/Entities/Permissions/OverwritePermissions.cs @@ -76,6 +76,10 @@ namespace Discord public PermValue MoveMembers => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.MoveMembers); /// If Allowed, a user may use voice-activity-detection rather than push-to-talk. public PermValue UseVAD => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.UseVAD); + /// If Allowed, a user may use priority speaker in a voice channel. + public PermValue PrioritySpeaker => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.PrioritySpeaker); + /// If Allowed, a user may go live in a voice channel. + public PermValue Stream => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.Stream); /// If Allowed, a user may adjust role permissions. This also implicitly grants all other permissions. public PermValue ManageRoles => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.ManageRoles); @@ -109,7 +113,9 @@ namespace Discord PermValue? moveMembers = null, PermValue? useVoiceActivation = null, PermValue? manageRoles = null, - PermValue? manageWebhooks = null) + PermValue? manageWebhooks = null, + PermValue? prioritySpeaker = null, + PermValue? stream = null) { Permissions.SetValue(ref allowValue, ref denyValue, createInstantInvite, ChannelPermission.CreateInstantInvite); Permissions.SetValue(ref allowValue, ref denyValue, manageChannel, ChannelPermission.ManageChannels); @@ -129,6 +135,8 @@ namespace Discord Permissions.SetValue(ref allowValue, ref denyValue, deafenMembers, ChannelPermission.DeafenMembers); Permissions.SetValue(ref allowValue, ref denyValue, moveMembers, ChannelPermission.MoveMembers); Permissions.SetValue(ref allowValue, ref denyValue, useVoiceActivation, ChannelPermission.UseVAD); + Permissions.SetValue(ref allowValue, ref denyValue, prioritySpeaker, ChannelPermission.PrioritySpeaker); + Permissions.SetValue(ref allowValue, ref denyValue, stream, ChannelPermission.Stream); Permissions.SetValue(ref allowValue, ref denyValue, manageRoles, ChannelPermission.ManageRoles); Permissions.SetValue(ref allowValue, ref denyValue, manageWebhooks, ChannelPermission.ManageWebhooks); @@ -159,10 +167,12 @@ namespace Discord PermValue moveMembers = PermValue.Inherit, PermValue useVoiceActivation = PermValue.Inherit, PermValue manageRoles = PermValue.Inherit, - PermValue manageWebhooks = PermValue.Inherit) + PermValue manageWebhooks = PermValue.Inherit, + PermValue prioritySpeaker = PermValue.Inherit, + PermValue stream = PermValue.Inherit) : this(0, 0, createInstantInvite, manageChannel, addReactions, viewChannel, sendMessages, sendTTSMessages, manageMessages, embedLinks, attachFiles, readMessageHistory, mentionEveryone, useExternalEmojis, connect, speak, muteMembers, deafenMembers, - moveMembers, useVoiceActivation, manageRoles, manageWebhooks) { } + moveMembers, useVoiceActivation, manageRoles, manageWebhooks, prioritySpeaker, stream) { } /// /// Initializes a new from the current one, changing the provided @@ -188,10 +198,12 @@ namespace Discord PermValue? moveMembers = null, PermValue? useVoiceActivation = null, PermValue? manageRoles = null, - PermValue? manageWebhooks = null) + PermValue? manageWebhooks = null, + PermValue? prioritySpeaker = null, + PermValue? stream = null) => new OverwritePermissions(AllowValue, DenyValue, createInstantInvite, manageChannel, addReactions, viewChannel, sendMessages, sendTTSMessages, manageMessages, embedLinks, attachFiles, readMessageHistory, mentionEveryone, useExternalEmojis, connect, speak, muteMembers, deafenMembers, - moveMembers, useVoiceActivation, manageRoles, manageWebhooks); + moveMembers, useVoiceActivation, manageRoles, manageWebhooks, prioritySpeaker, stream); /// /// Creates a of all the values that are allowed. diff --git a/Discord.NET/Discord.Net.Core/Entities/Users/IGuildUser.cs b/Discord.NET/Discord.Net.Core/Entities/Users/IGuildUser.cs index ae682af..92b146e 100644 --- a/Discord.NET/Discord.Net.Core/Entities/Users/IGuildUser.cs +++ b/Discord.NET/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/Discord.NET/Discord.Net.Core/Entities/Users/IPresence.cs b/Discord.NET/Discord.Net.Core/Entities/Users/IPresence.cs index 620eb90..a17ac0d 100644 --- a/Discord.NET/Discord.Net.Core/Entities/Users/IPresence.cs +++ b/Discord.NET/Discord.Net.Core/Entities/Users/IPresence.cs @@ -19,5 +19,9 @@ namespace Discord /// Gets the set of clients where this user is currently active. /// IImmutableSet ActiveClients { get; } + /// + /// Gets the list of activities that this user currently has available. + /// + IImmutableList Activities { get; } } } diff --git a/Discord.NET/Discord.Net.Core/Entities/Users/IUser.cs b/Discord.NET/Discord.Net.Core/Entities/Users/IUser.cs index c59a75d..c36fb23 100644 --- a/Discord.NET/Discord.Net.Core/Entities/Users/IUser.cs +++ b/Discord.NET/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/Discord.NET/Discord.Net.Core/Extensions/CollectionExtensions.cs b/Discord.NET/Discord.Net.Core/Extensions/CollectionExtensions.cs index e5d6025..f6ba762 100644 --- a/Discord.NET/Discord.Net.Core/Extensions/CollectionExtensions.cs +++ b/Discord.NET/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/Discord.NET/Discord.Net.Core/Extensions/MessageExtensions.cs b/Discord.NET/Discord.Net.Core/Extensions/MessageExtensions.cs index 90ebea9..e44e397 100644 --- a/Discord.NET/Discord.Net.Core/Extensions/MessageExtensions.cs +++ b/Discord.NET/Discord.Net.Core/Extensions/MessageExtensions.cs @@ -17,7 +17,7 @@ namespace Discord public static string GetJumpUrl(this IMessage msg) { var channel = msg.Channel; - return $"https://discordapp.com/channels/{(channel is IDMChannel ? "@me" : $"{(channel as ITextChannel).GuildId}")}/{channel.Id}/{msg.Id}"; + return $"https://discord.com/channels/{(channel is IDMChannel ? "@me" : $"{(channel as ITextChannel).GuildId}")}/{channel.Id}/{msg.Id}"; } /// @@ -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/Discord.NET/Discord.Net.Core/Extensions/UserExtensions.cs b/Discord.NET/Discord.Net.Core/Extensions/UserExtensions.cs index f98bf72..90f2682 100644 --- a/Discord.NET/Discord.Net.Core/Extensions/UserExtensions.cs +++ b/Discord.NET/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,17 +39,18 @@ 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); } /// /// 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/Discord.NET/Discord.Net.Core/GatewayIntents.cs b/Discord.NET/Discord.Net.Core/GatewayIntents.cs new file mode 100644 index 0000000..f3dc5ce --- /dev/null +++ b/Discord.NET/Discord.Net.Core/GatewayIntents.cs @@ -0,0 +1,43 @@ +using System; + +namespace Discord +{ + [Flags] + public enum GatewayIntents + { + /// This intent includes no events + None = 0, + /// This intent includes GUILD_CREATE, GUILD_UPDATE, GUILD_DELETE, GUILD_ROLE_CREATE, GUILD_ROLE_UPDATE, GUILD_ROLE_DELETE, CHANNEL_CREATE, CHANNEL_UPDATE, CHANNEL_DELETE, CHANNEL_PINS_UPDATE + Guilds = 1 << 0, + /// This intent includes GUILD_MEMBER_ADD, GUILD_MEMBER_UPDATE, GUILD_MEMBER_REMOVE + /// This is a privileged intent and must be enabled in the Developer Portal. + GuildMembers = 1 << 1, + /// This intent includes GUILD_BAN_ADD, GUILD_BAN_REMOVE + GuildBans = 1 << 2, + /// This intent includes GUILD_EMOJIS_UPDATE + GuildEmojis = 1 << 3, + /// This intent includes GUILD_INTEGRATIONS_UPDATE + GuildIntegrations = 1 << 4, + /// This intent includes WEBHOOKS_UPDATE + GuildWebhooks = 1 << 5, + /// This intent includes INVITE_CREATE, INVITE_DELETE + GuildInvites = 1 << 6, + /// This intent includes VOICE_STATE_UPDATE + GuildVoiceStates = 1 << 7, + /// This intent includes PRESENCE_UPDATE + /// This is a privileged intent and must be enabled in the Developer Portal. + GuildPresences = 1 << 8, + /// This intent includes MESSAGE_CREATE, MESSAGE_UPDATE, MESSAGE_DELETE, MESSAGE_DELETE_BULK + GuildMessages = 1 << 9, + /// This intent includes MESSAGE_REACTION_ADD, MESSAGE_REACTION_REMOVE, MESSAGE_REACTION_REMOVE_ALL, MESSAGE_REACTION_REMOVE_EMOJI + GuildMessageReactions = 1 << 10, + /// This intent includes TYPING_START + GuildMessageTyping = 1 << 11, + /// This intent includes CHANNEL_CREATE, MESSAGE_CREATE, MESSAGE_UPDATE, MESSAGE_DELETE, CHANNEL_PINS_UPDATE + DirectMessages = 1 << 12, + /// This intent includes MESSAGE_REACTION_ADD, MESSAGE_REACTION_REMOVE, MESSAGE_REACTION_REMOVE_ALL, MESSAGE_REACTION_REMOVE_EMOJI + DirectMessageReactions = 1 << 13, + /// This intent includes TYPING_START + DirectMessageTyping = 1 << 14, + } +} diff --git a/Discord.NET/Discord.Net.Core/IDiscordClient.cs b/Discord.NET/Discord.Net.Core/IDiscordClient.cs index e1c9006..f972cd7 100644 --- a/Discord.NET/Discord.Net.Core/IDiscordClient.cs +++ b/Discord.NET/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/Discord.NET/Discord.Net.Core/Net/BucketId.cs b/Discord.NET/Discord.Net.Core/Net/BucketId.cs new file mode 100644 index 0000000..96281a0 --- /dev/null +++ b/Discord.NET/Discord.Net.Core/Net/BucketId.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; + +namespace Discord.Net +{ + /// + /// Represents a ratelimit bucket. + /// + public class BucketId : IEquatable + { + /// + /// Gets the http method used to make the request if available. + /// + public string HttpMethod { get; } + /// + /// Gets the endpoint that is going to be requested if available. + /// + public string Endpoint { get; } + /// + /// Gets the major parameters of the route. + /// + public IOrderedEnumerable> MajorParameters { get; } + /// + /// Gets the hash of this bucket. + /// + /// + /// The hash is provided by Discord to group ratelimits. + /// + public string BucketHash { get; } + /// + /// Gets if this bucket is a hash type. + /// + public bool IsHashBucket { get => BucketHash != null; } + + private BucketId(string httpMethod, string endpoint, IEnumerable> majorParameters, string bucketHash) + { + HttpMethod = httpMethod; + Endpoint = endpoint; + MajorParameters = majorParameters.OrderBy(x => x.Key); + BucketHash = bucketHash; + } + + /// + /// Creates a new based on the + /// and . + /// + /// Http method used to make the request. + /// Endpoint that is going to receive requests. + /// Major parameters of the route of this endpoint. + /// + /// A based on the + /// and the with the provided data. + /// + public static BucketId Create(string httpMethod, string endpoint, Dictionary majorParams) + { + Preconditions.NotNullOrWhitespace(endpoint, nameof(endpoint)); + majorParams ??= new Dictionary(); + return new BucketId(httpMethod, endpoint, majorParams, null); + } + + /// + /// Creates a new based on a + /// and a previous . + /// + /// Bucket hash provided by Discord. + /// that is going to be upgraded to a hash type. + /// + /// A based on the + /// and . + /// + public static BucketId Create(string hash, BucketId oldBucket) + { + Preconditions.NotNullOrWhitespace(hash, nameof(hash)); + Preconditions.NotNull(oldBucket, nameof(oldBucket)); + return new BucketId(null, null, oldBucket.MajorParameters, hash); + } + + /// + /// Gets the string that will define this bucket as a hash based one. + /// + /// + /// A that defines this bucket as a hash based one. + /// + public string GetBucketHash() + => IsHashBucket ? $"{BucketHash}:{string.Join("/", MajorParameters.Select(x => x.Value))}" : null; + + /// + /// Gets the string that will define this bucket as an endpoint based one. + /// + /// + /// A that defines this bucket as an endpoint based one. + /// + public string GetUniqueEndpoint() + => HttpMethod != null ? $"{HttpMethod} {Endpoint}" : Endpoint; + + public override bool Equals(object obj) + => Equals(obj as BucketId); + + public override int GetHashCode() + => IsHashBucket ? (BucketHash, string.Join("/", MajorParameters.Select(x => x.Value))).GetHashCode() : (HttpMethod, Endpoint).GetHashCode(); + + public override string ToString() + => GetBucketHash() ?? GetUniqueEndpoint(); + + public bool Equals(BucketId other) + { + if (other is null) + return false; + if (ReferenceEquals(this, other)) + return true; + if (GetType() != other.GetType()) + return false; + return ToString() == other.ToString(); + } + } +} diff --git a/Discord.NET/Discord.Net.Core/Net/HttpException.cs b/Discord.NET/Discord.Net.Core/Net/HttpException.cs index d36bd66..ff9cf91 100644 --- a/Discord.NET/Discord.Net.Core/Net/HttpException.cs +++ b/Discord.NET/Discord.Net.Core/Net/HttpException.cs @@ -13,7 +13,7 @@ namespace Discord.Net /// /// /// An - /// HTTP status code + /// HTTP status code /// from Discord. /// public HttpStatusCode HttpCode { get; } @@ -22,7 +22,7 @@ namespace Discord.Net /// /// /// A - /// JSON error code + /// JSON error code /// from Discord, or null if none. /// public int? DiscordCode { get; } diff --git a/Discord.NET/Discord.Net.Core/Net/WebSocketClosedException.cs b/Discord.NET/Discord.Net.Core/Net/WebSocketClosedException.cs index 6e2564f..c743cd6 100644 --- a/Discord.NET/Discord.Net.Core/Net/WebSocketClosedException.cs +++ b/Discord.NET/Discord.Net.Core/Net/WebSocketClosedException.cs @@ -11,7 +11,7 @@ namespace Discord.Net /// /// /// A - /// close code + /// close code /// from Discord. /// public int CloseCode { get; } diff --git a/Discord.NET/Discord.Net.Core/Net/WebSockets/IWebSocketClient.cs b/Discord.NET/Discord.Net.Core/Net/WebSockets/IWebSocketClient.cs index 14b41cc..6791af3 100644 --- a/Discord.NET/Discord.Net.Core/Net/WebSockets/IWebSocketClient.cs +++ b/Discord.NET/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/Discord.NET/Discord.Net.Core/RequestOptions.cs b/Discord.NET/Discord.Net.Core/RequestOptions.cs index 6aa0eea..ad0a4e3 100644 --- a/Discord.NET/Discord.Net.Core/RequestOptions.cs +++ b/Discord.NET/Discord.Net.Core/RequestOptions.cs @@ -1,3 +1,4 @@ +using Discord.Net; using System.Threading; namespace Discord @@ -49,8 +50,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. @@ -58,7 +58,7 @@ namespace Discord public bool? UseSystemClock { get; set; } internal bool IgnoreState { get; set; } - internal string BucketId { get; set; } + internal BucketId BucketId { get; set; } internal bool IsClientBucket { get; set; } internal bool IsReactionBucket { get; set; } diff --git a/Discord.NET/Discord.Net.Core/TokenType.cs b/Discord.NET/Discord.Net.Core/TokenType.cs index 76e2f2c..8ca3f03 100644 --- a/Discord.NET/Discord.Net.Core/TokenType.cs +++ b/Discord.NET/Discord.Net.Core/TokenType.cs @@ -5,6 +5,7 @@ namespace Discord /// Specifies the type of token to use with the client. public enum TokenType { + [Obsolete("User logins are deprecated and may result in a ToS strike against your account - please see https://github.com/RogueException/Discord.Net/issues/827", error: true)] User, /// /// An OAuth2 token type. diff --git a/Discord.NET/Discord.Net.Core/Utils/Comparers.cs b/Discord.NET/Discord.Net.Core/Utils/Comparers.cs index 40500ff..3c7b8aa 100644 --- a/Discord.NET/Discord.Net.Core/Utils/Comparers.cs +++ b/Discord.NET/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; @@ -42,16 +41,13 @@ namespace Discord { public override bool Equals(TEntity x, TEntity y) { - bool xNull = x == null; - bool yNull = y == null; - - if (xNull && yNull) - return true; - - if (xNull ^ yNull) - return false; - - return x.Id.Equals(y.Id); + return (x, y) switch + { + (null, null) => true, + (null, _) => false, + (_, null) => false, + var (l, r) => l.Id.Equals(r.Id) + }; } public override int GetHashCode(TEntity obj) diff --git a/Discord.NET/Discord.Net.Rest/API/Common/AuditLogOptions.cs b/Discord.NET/Discord.Net.Rest/API/Common/AuditLogOptions.cs index 24141d9..b666215 100644 --- a/Discord.NET/Discord.Net.Rest/API/Common/AuditLogOptions.cs +++ b/Discord.NET/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/Discord.NET/Discord.Net.Rest/API/Common/Message.cs b/Discord.NET/Discord.Net.Rest/API/Common/Message.cs index f200356..b4529d4 100644 --- a/Discord.NET/Discord.Net.Rest/API/Common/Message.cs +++ b/Discord.NET/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/Discord.NET/Discord.Net.Rest/API/Common/Presence.cs b/Discord.NET/Discord.Net.Rest/API/Common/Presence.cs index 22526e8..b37ad42 100644 --- a/Discord.NET/Discord.Net.Rest/API/Common/Presence.cs +++ b/Discord.NET/Discord.Net.Rest/API/Common/Presence.cs @@ -1,5 +1,6 @@ #pragma warning disable CS1591 using Newtonsoft.Json; +using System; using System.Collections.Generic; namespace Discord.API @@ -26,5 +27,9 @@ namespace Discord.API // "client_status": { "desktop": "dnd", "mobile": "dnd" } [JsonProperty("client_status")] public Optional> ClientStatus { get; set; } + [JsonProperty("activities")] + public List Activities { get; set; } + [JsonProperty("premium_since")] + public Optional PremiumSince { get; set; } } } diff --git a/Discord.NET/Discord.Net.Rest/API/Rest/CreateGuildChannelParams.cs b/Discord.NET/Discord.Net.Rest/API/Rest/CreateGuildChannelParams.cs index a102bd3..aec43db 100644 --- a/Discord.NET/Discord.Net.Rest/API/Rest/CreateGuildChannelParams.cs +++ b/Discord.NET/Discord.Net.Rest/API/Rest/CreateGuildChannelParams.cs @@ -14,12 +14,16 @@ namespace Discord.API.Rest public Optional CategoryId { get; set; } [JsonProperty("position")] public Optional Position { get; set; } + [JsonProperty("permission_overwrites")] + public Optional Overwrites { get; set; } //Text channels [JsonProperty("topic")] public Optional Topic { get; set; } [JsonProperty("nsfw")] public Optional IsNsfw { get; set; } + [JsonProperty("rate_limit_per_user")] + public Optional SlowModeInterval { get; set; } //Voice channels [JsonProperty("bitrate")] diff --git a/Discord.NET/Discord.Net.Rest/API/Rest/CreateMessageParams.cs b/Discord.NET/Discord.Net.Rest/API/Rest/CreateMessageParams.cs index d77bff8..4b56658 100644 --- a/Discord.NET/Discord.Net.Rest/API/Rest/CreateMessageParams.cs +++ b/Discord.NET/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/Discord.NET/Discord.Net.Rest/API/Rest/CreateWebhookMessageParams.cs b/Discord.NET/Discord.Net.Rest/API/Rest/CreateWebhookMessageParams.cs index 970a302..0a4f80a 100644 --- a/Discord.NET/Discord.Net.Rest/API/Rest/CreateWebhookMessageParams.cs +++ b/Discord.NET/Discord.Net.Rest/API/Rest/CreateWebhookMessageParams.cs @@ -1,4 +1,4 @@ -ο»Ώ#pragma warning disable CS1591 +#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API.Rest @@ -19,6 +19,8 @@ namespace Discord.API.Rest public Optional Username { get; set; } [JsonProperty("avatar_url")] public Optional AvatarUrl { get; set; } + [JsonProperty("allowed_mentions")] + public Optional AllowedMentions { get; set; } public CreateWebhookMessageParams(string content) { diff --git a/Discord.NET/Discord.Net.Rest/API/Rest/SearchGuildMembersParams.cs b/Discord.NET/Discord.Net.Rest/API/Rest/SearchGuildMembersParams.cs new file mode 100644 index 0000000..7c933ff --- /dev/null +++ b/Discord.NET/Discord.Net.Rest/API/Rest/SearchGuildMembersParams.cs @@ -0,0 +1,9 @@ +#pragma warning disable CS1591 +namespace Discord.API.Rest +{ + internal class SearchGuildMembersParams + { + public string Query { get; set; } + public Optional Limit { get; set; } + } +} diff --git a/Discord.NET/Discord.Net.Rest/API/Rest/UploadFileParams.cs b/Discord.NET/Discord.Net.Rest/API/Rest/UploadFileParams.cs index 7ba21d0..64535e6 100644 --- a/Discord.NET/Discord.Net.Rest/API/Rest/UploadFileParams.cs +++ b/Discord.NET/Discord.Net.Rest/API/Rest/UploadFileParams.cs @@ -19,6 +19,7 @@ namespace Discord.API.Rest public Optional Nonce { get; set; } public Optional IsTTS { get; set; } public Optional Embed { get; set; } + public Optional AllowedMentions { get; set; } public bool IsSpoiler { get; set; } = false; public UploadFileParams(Stream file) @@ -43,6 +44,8 @@ namespace Discord.API.Rest payload["nonce"] = Nonce.Value; if (Embed.IsSpecified) payload["embed"] = Embed.Value; + if (AllowedMentions.IsSpecified) + payload["allowed_mentions"] = AllowedMentions.Value; if (IsSpoiler) payload["hasSpoiler"] = IsSpoiler.ToString(); diff --git a/Discord.NET/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs b/Discord.NET/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs index 26153c2..8da7681 100644 --- a/Discord.NET/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs +++ b/Discord.NET/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs @@ -21,6 +21,7 @@ namespace Discord.API.Rest public Optional Username { get; set; } public Optional AvatarUrl { get; set; } public Optional Embeds { get; set; } + public Optional AllowedMentions { get; set; } public bool IsSpoiler { get; set; } = false; @@ -51,6 +52,8 @@ namespace Discord.API.Rest payload["avatar_url"] = AvatarUrl.Value; if (Embeds.IsSpecified) payload["embeds"] = Embeds.Value; + if (AllowedMentions.IsSpecified) + payload["allowed_mentions"] = AllowedMentions.Value; var json = new StringBuilder(); using (var text = new StringWriter(json)) diff --git a/Discord.NET/Discord.Net.Rest/BaseDiscordClient.cs b/Discord.NET/Discord.Net.Rest/BaseDiscordClient.cs index 1837e38..58b4292 100644 --- a/Discord.NET/Discord.Net.Rest/BaseDiscordClient.cs +++ b/Discord.NET/Discord.Net.Rest/BaseDiscordClient.cs @@ -49,9 +49,9 @@ namespace Discord.Rest ApiClient.RequestQueue.RateLimitTriggered += async (id, info) => { if (info == null) - await _restLogger.VerboseAsync($"Preemptive Rate limit triggered: {id ?? "null"}").ConfigureAwait(false); + await _restLogger.VerboseAsync($"Preemptive Rate limit triggered: {id?.ToString() ?? "null"}").ConfigureAwait(false); else - await _restLogger.WarningAsync($"Rate limit triggered: {id ?? "null"}").ConfigureAwait(false); + await _restLogger.WarningAsync($"Rate limit triggered: {id?.ToString() ?? "null"}").ConfigureAwait(false); }; ApiClient.SentRequest += async (method, endpoint, millis) => await _restLogger.VerboseAsync($"{method} {endpoint}: {millis} ms").ConfigureAwait(false); } diff --git a/Discord.NET/Discord.Net.Rest/DiscordRestApiClient.cs b/Discord.NET/Discord.Net.Rest/DiscordRestApiClient.cs index e7e2bca..83aa6c7 100644 --- a/Discord.NET/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/Discord.NET/Discord.Net.Rest/DiscordRestApiClient.cs @@ -24,7 +24,7 @@ namespace Discord.API { internal class DiscordRestApiClient : IDisposable { - private static readonly ConcurrentDictionary> _bucketIdGenerators = new ConcurrentDictionary>(); + private static readonly ConcurrentDictionary> _bucketIdGenerators = new ConcurrentDictionary>(); public event Func SentRequest { add { _sentRequestEvent.Add(value); } remove { _sentRequestEvent.Remove(value); } } private readonly AsyncEvent> _sentRequestEvent = new AsyncEvent>(); @@ -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. @@ -80,17 +80,13 @@ namespace Discord.API /// Unknown OAuth token type. internal static string GetPrefixedToken(TokenType tokenType, string token) { - switch (tokenType) + return tokenType switch { - case TokenType.Bot: - return $"Bot {token}"; - case TokenType.Bearer: - return $"Bearer {token}"; - case TokenType.User: - return token; - default: - throw new ArgumentException(message: "Unknown OAuth token type.", paramName: nameof(tokenType)); - } + default(TokenType) => token, + TokenType.Bot => $"Bot {token}", + TokenType.Bearer => $"Bearer {token}", + _ => throw new ArgumentException(message: "Unknown OAuth token type.", paramName: nameof(tokenType)), + }; } internal virtual void Dispose(bool disposing) { @@ -127,16 +123,15 @@ namespace Discord.API { _loginCancelToken?.Dispose(); _loginCancelToken = new CancellationTokenSource(); - AuthTokenType = TokenType.User; + AuthToken = null; await RequestQueue.SetCancelTokenAsync(_loginCancelToken.Token).ConfigureAwait(false); RestClient.SetCancelToken(_loginCancelToken.Token); AuthTokenType = tokenType; - AuthToken = token; - var temp = GetPrefixedToken(AuthTokenType, AuthToken); + AuthToken = token?.TrimEnd(); if (tokenType != TokenType.Webhook) - RestClient.SetHeader("authorization", temp); + RestClient.SetHeader("authorization", GetPrefixedToken(AuthTokenType, AuthToken)); LoginState = LoginState.LoggedIn; } @@ -165,7 +160,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); @@ -176,18 +171,18 @@ 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, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null) - => SendAsync(method, GetEndpoint(endpointExpr), GetBucketId(ids, endpointExpr, funcName), clientBucket, options); + => SendAsync(method, GetEndpoint(endpointExpr), GetBucketId(method, ids, endpointExpr, funcName), clientBucket, options); public async Task SendAsync(string method, string endpoint, - string bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) + BucketId bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) { options = options ?? new RequestOptions(); options.HeaderOnly = true; - options.BucketId = AuthTokenType == TokenType.User ? ClientBucket.Get(clientBucket).Id : bucketId; + options.BucketId = bucketId; var request = new RestRequest(RestClient, method, endpoint, options); await SendInternalAsync(method, endpoint, request).ConfigureAwait(false); @@ -195,13 +190,13 @@ namespace Discord.API internal Task SendJsonAsync(string method, Expression> endpointExpr, object payload, BucketIds ids, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null) - => SendJsonAsync(method, GetEndpoint(endpointExpr), payload, GetBucketId(ids, endpointExpr, funcName), clientBucket, options); + => SendJsonAsync(method, GetEndpoint(endpointExpr), payload, GetBucketId(method, ids, endpointExpr, funcName), clientBucket, options); public async Task SendJsonAsync(string method, string endpoint, object payload, - string bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) + BucketId bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) { options = options ?? new RequestOptions(); options.HeaderOnly = true; - options.BucketId = AuthTokenType == TokenType.User ? ClientBucket.Get(clientBucket).Id : bucketId; + options.BucketId = bucketId; string json = payload != null ? SerializeJson(payload) : null; var request = new JsonRestRequest(RestClient, method, endpoint, json, options); @@ -210,13 +205,13 @@ namespace Discord.API internal Task SendMultipartAsync(string method, Expression> endpointExpr, IReadOnlyDictionary multipartArgs, BucketIds ids, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null) - => SendMultipartAsync(method, GetEndpoint(endpointExpr), multipartArgs, GetBucketId(ids, endpointExpr, funcName), clientBucket, options); + => SendMultipartAsync(method, GetEndpoint(endpointExpr), multipartArgs, GetBucketId(method, ids, endpointExpr, funcName), clientBucket, options); public async Task SendMultipartAsync(string method, string endpoint, IReadOnlyDictionary multipartArgs, - string bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) + BucketId bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) { options = options ?? new RequestOptions(); options.HeaderOnly = true; - options.BucketId = AuthTokenType == TokenType.User ? ClientBucket.Get(clientBucket).Id : bucketId; + options.BucketId = bucketId; var request = new MultipartRestRequest(RestClient, method, endpoint, multipartArgs, options); await SendInternalAsync(method, endpoint, request).ConfigureAwait(false); @@ -224,12 +219,12 @@ namespace Discord.API internal Task SendAsync(string method, Expression> endpointExpr, BucketIds ids, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null) where TResponse : class - => SendAsync(method, GetEndpoint(endpointExpr), GetBucketId(ids, endpointExpr, funcName), clientBucket, options); + => SendAsync(method, GetEndpoint(endpointExpr), GetBucketId(method, ids, endpointExpr, funcName), clientBucket, options); public async Task SendAsync(string method, string endpoint, - string bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) where TResponse : class + BucketId bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) where TResponse : class { options = options ?? new RequestOptions(); - options.BucketId = AuthTokenType == TokenType.User ? ClientBucket.Get(clientBucket).Id : bucketId; + options.BucketId = bucketId; var request = new RestRequest(RestClient, method, endpoint, options); return DeserializeJson(await SendInternalAsync(method, endpoint, request).ConfigureAwait(false)); @@ -237,12 +232,12 @@ namespace Discord.API internal Task SendJsonAsync(string method, Expression> endpointExpr, object payload, BucketIds ids, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null) where TResponse : class - => SendJsonAsync(method, GetEndpoint(endpointExpr), payload, GetBucketId(ids, endpointExpr, funcName), clientBucket, options); + => SendJsonAsync(method, GetEndpoint(endpointExpr), payload, GetBucketId(method, ids, endpointExpr, funcName), clientBucket, options); public async Task SendJsonAsync(string method, string endpoint, object payload, - string bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) where TResponse : class + BucketId bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) where TResponse : class { options = options ?? new RequestOptions(); - options.BucketId = AuthTokenType == TokenType.User ? ClientBucket.Get(clientBucket).Id : bucketId; + options.BucketId = bucketId; string json = payload != null ? SerializeJson(payload) : null; var request = new JsonRestRequest(RestClient, method, endpoint, json, options); @@ -251,13 +246,12 @@ namespace Discord.API internal Task SendMultipartAsync(string method, Expression> endpointExpr, IReadOnlyDictionary multipartArgs, BucketIds ids, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null) - => SendMultipartAsync(method, GetEndpoint(endpointExpr), multipartArgs, GetBucketId(ids, endpointExpr, funcName), clientBucket, options); + => SendMultipartAsync(method, GetEndpoint(endpointExpr), multipartArgs, GetBucketId(method, ids, endpointExpr, funcName), clientBucket, options); public async Task SendMultipartAsync(string method, string endpoint, IReadOnlyDictionary multipartArgs, - string bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) + BucketId bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) { options = options ?? new RequestOptions(); - - options.BucketId = AuthTokenType == TokenType.User ? ClientBucket.Get(clientBucket).Id : bucketId; + options.BucketId = bucketId; var request = new MultipartRestRequest(RestClient, method, endpoint, multipartArgs, options); return DeserializeJson(await SendInternalAsync(method, endpoint, request).ConfigureAwait(false)); @@ -526,7 +520,8 @@ namespace Discord.API throw new ArgumentException(message: $"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", paramName: nameof(args.Content)); options = RequestOptions.CreateOrClone(options); - return await SendJsonAsync("POST", () => $"webhooks/{webhookId}/{AuthToken}?wait=true", args, new BucketIds(), clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); + var ids = new BucketIds(webhookId: webhookId); + return await SendJsonAsync("POST", () => $"webhooks/{webhookId}/{AuthToken}?wait=true", args, ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); } /// Message content is too long, length must be less or equal to . public async Task UploadFileAsync(ulong channelId, UploadFileParams args, RequestOptions options = null) @@ -565,7 +560,8 @@ namespace Discord.API throw new ArgumentOutOfRangeException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content)); } - return await SendMultipartAsync("POST", () => $"webhooks/{webhookId}/{AuthToken}?wait=true", args.ToDictionary(), new BucketIds(), clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); + var ids = new BucketIds(webhookId: webhookId); + return await SendMultipartAsync("POST", () => $"webhooks/{webhookId}/{AuthToken}?wait=true", args.ToDictionary(), ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); } public async Task DeleteMessageAsync(ulong channelId, ulong messageId, RequestOptions options = null) { @@ -604,13 +600,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); @@ -667,6 +658,18 @@ namespace Discord.API await SendAsync("DELETE", () => $"channels/{channelId}/messages/{messageId}/reactions", ids, options: options).ConfigureAwait(false); } + public async Task RemoveAllReactionsForEmoteAsync(ulong channelId, ulong messageId, string emoji, RequestOptions options = null) + { + Preconditions.NotEqual(channelId, 0, nameof(channelId)); + Preconditions.NotEqual(messageId, 0, nameof(messageId)); + Preconditions.NotNullOrWhitespace(emoji, nameof(emoji)); + + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(channelId: channelId); + + await SendAsync("DELETE", () => $"channels/{channelId}/messages/{messageId}/reactions/{emoji}", ids, options: options).ConfigureAwait(false); + } public async Task> GetReactionUsersAsync(ulong channelId, ulong messageId, string emoji, GetReactionUsersParams args, RequestOptions options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); @@ -702,6 +705,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) @@ -862,8 +874,12 @@ namespace Discord.API Preconditions.NotEqual(guildId, 0, nameof(guildId)); options = RequestOptions.CreateOrClone(options); - var ids = new BucketIds(guildId: guildId); - return await SendAsync("GET", () => $"guilds/{guildId}/bans/{userId}", ids, options: options).ConfigureAwait(false); + try + { + var ids = new BucketIds(guildId: guildId); + return await SendAsync("GET", () => $"guilds/{guildId}/bans/{userId}", ids, options: options).ConfigureAwait(false); + } + catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.NotFound) { return null; } } /// /// and must not be equal to zero. @@ -1064,7 +1080,7 @@ namespace Discord.API { foreach (var roleId in args.RoleIds.Value) Preconditions.NotEqual(roleId, 0, nameof(roleId)); - } + } options = RequestOptions.CreateOrClone(options); @@ -1134,6 +1150,22 @@ namespace Discord.API await SendJsonAsync("PATCH", () => $"guilds/{guildId}/members/{userId}", args, ids, options: options).ConfigureAwait(false); } } + public async Task> SearchGuildMembersAsync(ulong guildId, SearchGuildMembersParams args, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotNull(args, nameof(args)); + Preconditions.GreaterThan(args.Limit, 0, nameof(args.Limit)); + Preconditions.AtMost(args.Limit, DiscordConfig.MaxUsersPerBatch, nameof(args.Limit)); + Preconditions.NotNullOrEmpty(args.Query, nameof(args.Query)); + options = RequestOptions.CreateOrClone(options); + + int limit = args.Limit.GetValueOrDefault(DiscordConfig.MaxUsersPerBatch); + string query = args.Query; + + var ids = new BucketIds(guildId: guildId); + Expression> endpoint = () => $"guilds/{guildId}/members/search?limit={limit}&query={query}"; + return await SendAsync>("GET", endpoint, ids, options: options).ConfigureAwait(false); + } //Guild Roles public async Task> GetGuildRolesAsync(ulong guildId, RequestOptions options = null) @@ -1144,13 +1176,13 @@ namespace Discord.API var ids = new BucketIds(guildId: guildId); return await SendAsync>("GET", () => $"guilds/{guildId}/roles", ids, options: options).ConfigureAwait(false); } - public async Task CreateGuildRoleAsync(ulong guildId, RequestOptions options = null) + public async Task CreateGuildRoleAsync(ulong guildId, Rest.ModifyGuildRoleParams args, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); options = RequestOptions.CreateOrClone(options); var ids = new BucketIds(guildId: guildId); - return await SendAsync("POST", () => $"guilds/{guildId}/roles", ids, options: options).ConfigureAwait(false); + return await SendJsonAsync("POST", () => $"guilds/{guildId}/roles", args, ids, options: options).ConfigureAwait(false); } public async Task DeleteGuildRoleAsync(ulong guildId, ulong roleId, RequestOptions options = null) { @@ -1440,21 +1472,39 @@ namespace Discord.API { public ulong GuildId { get; internal set; } public ulong ChannelId { get; internal set; } + public ulong WebhookId { get; internal set; } + public string HttpMethod { get; internal set; } - internal BucketIds(ulong guildId = 0, ulong channelId = 0) + internal BucketIds(ulong guildId = 0, ulong channelId = 0, ulong webhookId = 0) { GuildId = guildId; ChannelId = channelId; + WebhookId = webhookId; } + internal object[] ToArray() - => new object[] { GuildId, ChannelId }; + => new object[] { HttpMethod, GuildId, ChannelId, WebhookId }; + + internal Dictionary ToMajorParametersDictionary() + { + var dict = new Dictionary(); + if (GuildId != 0) + dict["GuildId"] = GuildId.ToString(); + if (ChannelId != 0) + dict["ChannelId"] = ChannelId.ToString(); + if (WebhookId != 0) + dict["WebhookId"] = WebhookId.ToString(); + return dict; + } internal static int? GetIndex(string name) { switch (name) { - case "guildId": return 0; - case "channelId": return 1; + case "httpMethod": return 0; + case "guildId": return 1; + case "channelId": return 2; + case "webhookId": return 3; default: return null; } @@ -1465,18 +1515,19 @@ namespace Discord.API { return endpointExpr.Compile()(); } - private static string GetBucketId(BucketIds ids, Expression> endpointExpr, string callingMethod) + private static BucketId GetBucketId(string httpMethod, BucketIds ids, Expression> endpointExpr, string callingMethod) { + ids.HttpMethod ??= httpMethod; return _bucketIdGenerators.GetOrAdd(callingMethod, x => CreateBucketId(endpointExpr))(ids); } - private static Func CreateBucketId(Expression> endpoint) + private static Func CreateBucketId(Expression> endpoint) { try { //Is this a constant string? if (endpoint.Body.NodeType == ExpressionType.Constant) - return x => (endpoint.Body as ConstantExpression).Value.ToString(); + return x => BucketId.Create(x.HttpMethod, (endpoint.Body as ConstantExpression).Value.ToString(), x.ToMajorParametersDictionary()); var builder = new StringBuilder(); var methodCall = endpoint.Body as MethodCallExpression; @@ -1513,7 +1564,7 @@ namespace Discord.API var mappedId = BucketIds.GetIndex(fieldName); - if(!mappedId.HasValue && rightIndex != endIndex && format.Length > rightIndex + 1 && format[rightIndex + 1] == '/') //Ignore the next slash + if (!mappedId.HasValue && rightIndex != endIndex && format.Length > rightIndex + 1 && format[rightIndex + 1] == '/') //Ignore the next slash rightIndex++; if (mappedId.HasValue) @@ -1526,7 +1577,7 @@ namespace Discord.API format = builder.ToString(); - return x => string.Format(format, x.ToArray()); + return x => BucketId.Create(x.HttpMethod, string.Format(format, x.ToArray()), x.ToMajorParametersDictionary()); } catch (Exception ex) { diff --git a/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/AuditLogHelper.cs b/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/AuditLogHelper.cs index 7936343..6969172 100644 --- a/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/AuditLogHelper.cs +++ b/Discord.NET/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/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BotAddAuditLogData.cs b/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BotAddAuditLogData.cs new file mode 100644 index 0000000..0d12e46 --- /dev/null +++ b/Discord.NET/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/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelCreateAuditLogData.cs b/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelCreateAuditLogData.cs index f432b4c..5c2f81a 100644 --- a/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelCreateAuditLogData.cs +++ b/Discord.NET/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()); } /// @@ -78,7 +71,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 +88,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/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelDeleteAuditLogData.cs b/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelDeleteAuditLogData.cs index 3907499..81ae715 100644 --- a/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelDeleteAuditLogData.cs +++ b/Discord.NET/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/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelInfo.cs b/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelInfo.cs index d6d2fb4..0284b63 100644 --- a/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelInfo.cs +++ b/Discord.NET/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/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteCreateAuditLogData.cs b/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteCreateAuditLogData.cs index 215a3c1..b177b24 100644 --- a/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteCreateAuditLogData.cs +++ b/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteCreateAuditLogData.cs @@ -36,13 +36,17 @@ namespace Discord.Rest var maxAge = maxAgeModel.NewValue.ToObject(discord.ApiClient.Serializer); var code = codeModel.NewValue.ToObject(discord.ApiClient.Serializer); var temporary = temporaryModel.NewValue.ToObject(discord.ApiClient.Serializer); - var inviterId = inviterIdModel.NewValue.ToObject(discord.ApiClient.Serializer); var channelId = channelIdModel.NewValue.ToObject(discord.ApiClient.Serializer); var uses = usesModel.NewValue.ToObject(discord.ApiClient.Serializer); var maxUses = maxUsesModel.NewValue.ToObject(discord.ApiClient.Serializer); - var inviterInfo = log.Users.FirstOrDefault(x => x.Id == inviterId); - var inviter = RestUser.Create(discord, inviterInfo); + RestUser inviter = null; + if (inviterIdModel != null) + { + var inviterId = inviterIdModel.NewValue.ToObject(discord.ApiClient.Serializer); + var inviterInfo = log.Users.FirstOrDefault(x => x.Id == inviterId); + inviter = RestUser.Create(discord, inviterInfo); + } return new InviteCreateAuditLogData(maxAge, code, temporary, inviter, channelId, uses, maxUses); } @@ -70,10 +74,10 @@ namespace Discord.Rest /// public bool Temporary { get; } /// - /// Gets the user that created this invite. + /// Gets the user that created this invite if available. /// /// - /// A user that created this invite. + /// A user that created this invite or . /// public IUser Creator { get; } /// diff --git a/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteDeleteAuditLogData.cs b/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteDeleteAuditLogData.cs index 5e49bb6..9d0aed1 100644 --- a/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteDeleteAuditLogData.cs +++ b/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteDeleteAuditLogData.cs @@ -36,13 +36,17 @@ namespace Discord.Rest var maxAge = maxAgeModel.OldValue.ToObject(discord.ApiClient.Serializer); var code = codeModel.OldValue.ToObject(discord.ApiClient.Serializer); var temporary = temporaryModel.OldValue.ToObject(discord.ApiClient.Serializer); - var inviterId = inviterIdModel.OldValue.ToObject(discord.ApiClient.Serializer); var channelId = channelIdModel.OldValue.ToObject(discord.ApiClient.Serializer); var uses = usesModel.OldValue.ToObject(discord.ApiClient.Serializer); var maxUses = maxUsesModel.OldValue.ToObject(discord.ApiClient.Serializer); - var inviterInfo = log.Users.FirstOrDefault(x => x.Id == inviterId); - var inviter = RestUser.Create(discord, inviterInfo); + RestUser inviter = null; + if (inviterIdModel != null) + { + var inviterId = inviterIdModel.OldValue.ToObject(discord.ApiClient.Serializer); + var inviterInfo = log.Users.FirstOrDefault(x => x.Id == inviterId); + inviter = RestUser.Create(discord, inviterInfo); + } return new InviteDeleteAuditLogData(maxAge, code, temporary, inviter, channelId, uses, maxUses); } @@ -70,10 +74,10 @@ namespace Discord.Rest /// public bool Temporary { get; } /// - /// Gets the user that created this invite. + /// Gets the user that created this invite if available. /// /// - /// A user that created this invite. + /// A user that created this invite or . /// public IUser Creator { get; } /// diff --git a/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberDisconnectAuditLogData.cs b/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberDisconnectAuditLogData.cs new file mode 100644 index 0000000..b0374dc --- /dev/null +++ b/Discord.NET/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/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberMoveAuditLogData.cs b/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberMoveAuditLogData.cs new file mode 100644 index 0000000..f5373d3 --- /dev/null +++ b/Discord.NET/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/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageBulkDeleteAuditLogData.cs b/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageBulkDeleteAuditLogData.cs new file mode 100644 index 0000000..7a98463 --- /dev/null +++ b/Discord.NET/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/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageDeleteAuditLogData.cs b/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageDeleteAuditLogData.cs index c6b2e10..66b3f7d 100644 --- a/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageDeleteAuditLogData.cs +++ b/Discord.NET/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/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessagePinAuditLogData.cs b/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessagePinAuditLogData.cs new file mode 100644 index 0000000..0201711 --- /dev/null +++ b/Discord.NET/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/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageUnpinAuditLogData.cs b/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageUnpinAuditLogData.cs new file mode 100644 index 0000000..1b3ff96 --- /dev/null +++ b/Discord.NET/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; } + } +} diff --git a/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteDeleteAuditLogData.cs b/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteDeleteAuditLogData.cs index a193e76..dc8948d 100644 --- a/Discord.NET/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteDeleteAuditLogData.cs +++ b/Discord.NET/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)); } /// diff --git a/Discord.NET/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs b/Discord.NET/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs index 5fb150c..6494f7f 100644 --- a/Discord.NET/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs +++ b/Discord.NET/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs @@ -28,7 +28,16 @@ namespace Discord.Rest { Name = args.Name, Position = args.Position, - CategoryId = args.CategoryId + CategoryId = args.CategoryId, + Overwrites = args.PermissionOverwrites.IsSpecified + ? args.PermissionOverwrites.Value.Select(overwrite => new API.Overwrite + { + TargetId = overwrite.TargetId, + TargetType = overwrite.TargetType, + Allow = overwrite.Permissions.AllowValue, + Deny = overwrite.Permissions.DenyValue + }).ToArray() + : Optional.Create(), }; return await client.ApiClient.ModifyGuildChannelAsync(channel.Id, apiArgs, options).ConfigureAwait(false); } @@ -46,6 +55,15 @@ namespace Discord.Rest Topic = args.Topic, IsNsfw = args.IsNsfw, SlowModeInterval = args.SlowModeInterval, + Overwrites = args.PermissionOverwrites.IsSpecified + ? args.PermissionOverwrites.Value.Select(overwrite => new API.Overwrite + { + TargetId = overwrite.TargetId, + TargetType = overwrite.TargetType, + Allow = overwrite.Permissions.AllowValue, + Deny = overwrite.Permissions.DenyValue + }).ToArray() + : Optional.Create(), }; return await client.ApiClient.ModifyGuildChannelAsync(channel.Id, apiArgs, options).ConfigureAwait(false); } @@ -61,7 +79,16 @@ namespace Discord.Rest Name = args.Name, Position = args.Position, CategoryId = args.CategoryId, - UserLimit = args.UserLimit.IsSpecified ? (args.UserLimit.Value ?? 0) : Optional.Create() + UserLimit = args.UserLimit.IsSpecified ? (args.UserLimit.Value ?? 0) : Optional.Create(), + Overwrites = args.PermissionOverwrites.IsSpecified + ? args.PermissionOverwrites.Value.Select(overwrite => new API.Overwrite + { + TargetId = overwrite.TargetId, + TargetType = overwrite.TargetType, + Allow = overwrite.Permissions.AllowValue, + Deny = overwrite.Permissions.DenyValue + }).ToArray() + : Optional.Create(), }; return await client.ApiClient.ModifyGuildChannelAsync(channel.Id, apiArgs, options).ConfigureAwait(false); } @@ -109,12 +136,19 @@ namespace Discord.Rest public static IAsyncEnumerable> GetMessagesAsync(IMessageChannel channel, BaseDiscordClient client, ulong? fromMessageId, Direction dir, int limit, RequestOptions options) { - if (dir == Direction.Around) - throw new NotImplementedException(); //TODO: Impl - var guildId = (channel as IGuildChannel)?.GuildId; var guild = guildId != null ? (client as IDiscordClient).GetGuildAsync(guildId.Value, CacheMode.CacheOnly).Result : null; + if (dir == Direction.Around && limit > DiscordConfig.MaxMessagesPerBatch) + { + int around = limit / 2; + if (fromMessageId.HasValue) + return GetMessagesAsync(channel, client, fromMessageId.Value + 1, Direction.Before, around + 1, options) //Need to include the message itself + .Concat(GetMessagesAsync(channel, client, fromMessageId, Direction.After, around, options)); + else //Shouldn't happen since there's no public overload for ulong? and Direction + return GetMessagesAsync(channel, client, null, Direction.Before, around + 1, options); + } + return new PagedAsyncEnumerable( DiscordConfig.MaxMessagesPerBatch, async (info, ct) => @@ -167,9 +201,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); } @@ -199,18 +252,37 @@ namespace Discord.Rest /// An I/O error occurred while opening the file. /// Message content is too long, length must be less or equal to . public static async Task SendFileAsync(IMessageChannel channel, BaseDiscordClient client, - string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler) + string filePath, string text, bool isTTS, Embed embed, AllowedMentions allowedMentions, RequestOptions options, bool isSpoiler) { string filename = Path.GetFileName(filePath); using (var file = File.OpenRead(filePath)) - return await SendFileAsync(channel, client, file, filename, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false); + return await SendFileAsync(channel, client, file, filename, text, isTTS, embed, allowedMentions, options, isSpoiler).ConfigureAwait(false); } /// Message content is too long, length must be less or equal to . public static async Task SendFileAsync(IMessageChannel channel, BaseDiscordClient client, - Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler) + Stream stream, string filename, string text, bool isTTS, Embed embed, AllowedMentions allowedMentions, RequestOptions options, bool isSpoiler) { - var args = new UploadFileParams(stream) { Filename = filename, Content = text, IsTTS = isTTS, Embed = embed != null ? embed.ToModel() : Optional.Unspecified, IsSpoiler = isSpoiler }; + 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 UploadFileParams(stream) { Filename = filename, Content = text, IsTTS = isTTS, Embed = embed?.ToModel() ?? Optional.Unspecified, AllowedMentions = allowedMentions?.ToModel() ?? Optional.Unspecified, IsSpoiler = isSpoiler }; var model = await client.ApiClient.UploadFileAsync(channel.Id, args, options).ConfigureAwait(false); return RestUserMessage.Create(client, channel, client.CurrentUser, model); } @@ -368,7 +440,8 @@ namespace Discord.Rest var apiArgs = new ModifyGuildChannelParams { Overwrites = category.PermissionOverwrites - .Select(overwrite => new API.Overwrite{ + .Select(overwrite => new API.Overwrite + { TargetId = overwrite.TargetId, TargetType = overwrite.TargetType, Allow = overwrite.Permissions.AllowValue, diff --git a/Discord.NET/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs b/Discord.NET/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs index a28170e..d02b293 100644 --- a/Discord.NET/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs +++ b/Discord.NET/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs @@ -20,17 +20,21 @@ 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. /// /// /// This method follows the same behavior as described in - /// . Please visit + /// . Please visit /// its documentation for more details on this method. /// /// The file path of the file. @@ -38,16 +42,21 @@ namespace Discord.Rest /// Whether the message should be read aloud by Discord or not. /// The to be sent. /// The options to be used when sending the request. + /// Whether the message attachment should be hidden as a spoiler. + /// + /// Specifies if notifications are sent for mentioned users and roles in the message . + /// If null, all mentioned roles and users will be notified. + /// /// /// A task that represents an asynchronous send operation for delivering the message. The task result /// contains the sent message. /// - new Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false); + new Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null); /// /// Sends a file to this message channel with an optional caption. /// /// - /// This method follows the same behavior as described in . + /// This method follows the same behavior as described in . /// Please visit its documentation for more details on this method. /// /// The of the file to be sent. @@ -56,11 +65,16 @@ namespace Discord.Rest /// Whether the message should be read aloud by Discord or not. /// The to be sent. /// The options to be used when sending the request. + /// Whether the message attachment should be hidden as a spoiler. + /// + /// Specifies if notifications are sent for mentioned users and roles in the message . + /// If null, all mentioned roles and users will be notified. + /// /// /// A task that represents an asynchronous send operation for delivering the message. The task result /// contains the sent message. /// - new Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false); + new Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null); /// /// Gets a message from this message channel. diff --git a/Discord.NET/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs b/Discord.NET/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs index 446410b..0f29f9d 100644 --- a/Discord.NET/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs +++ b/Discord.NET/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); /// /// @@ -121,12 +121,12 @@ namespace Discord.Rest /// is in an invalid format. /// An I/O error occurred while opening the file. /// Message content is too long, length must be less or equal to . - public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false) - => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options, isSpoiler); + public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null) + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, options, isSpoiler); /// /// Message content is too long, length must be less or equal to . - public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false) - => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options, isSpoiler); + public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null) + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, options, isSpoiler); /// public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) @@ -200,14 +200,14 @@ namespace Discord.Rest async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) => await GetPinnedMessagesAsync(options).ConfigureAwait(false); /// - async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler) - => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions) + => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions).ConfigureAwait(false); /// - 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.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions) + => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions).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/Discord.NET/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs b/Discord.NET/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs index 5cfe03f..4361fd2 100644 --- a/Discord.NET/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs +++ b/Discord.NET/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); /// /// @@ -123,12 +123,12 @@ namespace Discord.Rest /// is in an invalid format. /// An I/O error occurred while opening the file. /// Message content is too long, length must be less or equal to . - public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false) - => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options, isSpoiler); + public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null) + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, options, isSpoiler); /// /// Message content is too long, length must be less or equal to . - public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false) - => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options, isSpoiler); + public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null) + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, options, isSpoiler); /// public Task TriggerTypingAsync(RequestOptions options = null) @@ -178,13 +178,14 @@ namespace Discord.Rest async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) => await GetPinnedMessagesAsync(options).ConfigureAwait(false); - async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler) - => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions) + => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions).ConfigureAwait(false); - 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.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions) + => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions).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/Discord.NET/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs b/Discord.NET/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs index dc86327..a85ef4f 100644 --- a/Discord.NET/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs +++ b/Discord.NET/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs @@ -42,7 +42,8 @@ namespace Discord.Rest base.Update(model); CategoryId = model.CategoryId; Topic = model.Topic.Value; - SlowModeInterval = model.SlowMode.Value; + if (model.SlowMode.IsSpecified) + SlowModeInterval = model.SlowMode.Value; IsNsfw = model.Nsfw.GetValueOrDefault(); } @@ -101,8 +102,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); /// /// @@ -129,13 +130,13 @@ namespace Discord.Rest /// is in an invalid format. /// An I/O error occurred while opening the file. /// Message content is too long, length must be less or equal to . - public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false) - => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options, isSpoiler); + public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null) + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, options, isSpoiler); /// /// Message content is too long, length must be less or equal to . - public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false) - => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options, isSpoiler); + public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null) + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, options, isSpoiler); /// public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) @@ -266,15 +267,15 @@ namespace Discord.Rest => await GetPinnedMessagesAsync(options).ConfigureAwait(false); /// - async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler) - => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions) + => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions).ConfigureAwait(false); /// - 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.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions) + => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions).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/Discord.NET/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs b/Discord.NET/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs index 790b1e5..ecb45fd 100644 --- a/Discord.NET/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs +++ b/Discord.NET/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs @@ -132,7 +132,7 @@ namespace Discord.Rest public static async Task GetBanAsync(IGuild guild, BaseDiscordClient client, ulong userId, RequestOptions options) { var model = await client.ApiClient.GetGuildBanAsync(guild.Id, userId, options).ConfigureAwait(false); - return RestBan.Create(client, model); + return model == null ? null : RestBan.Create(client, model); } public static async Task AddBanAsync(IGuild guild, BaseDiscordClient client, @@ -176,7 +176,17 @@ namespace Discord.Rest CategoryId = props.CategoryId, Topic = props.Topic, IsNsfw = props.IsNsfw, - Position = props.Position + Position = props.Position, + SlowModeInterval = props.SlowModeInterval, + Overwrites = props.PermissionOverwrites.IsSpecified + ? props.PermissionOverwrites.Value.Select(overwrite => new API.Overwrite + { + TargetId = overwrite.TargetId, + TargetType = overwrite.TargetType, + Allow = overwrite.Permissions.AllowValue, + Deny = overwrite.Permissions.DenyValue + }).ToArray() + : Optional.Create(), }; var model = await client.ApiClient.CreateGuildChannelAsync(guild.Id, args, options).ConfigureAwait(false); return RestTextChannel.Create(client, guild, model); @@ -195,7 +205,16 @@ namespace Discord.Rest CategoryId = props.CategoryId, Bitrate = props.Bitrate, UserLimit = props.UserLimit, - Position = props.Position + Position = props.Position, + Overwrites = props.PermissionOverwrites.IsSpecified + ? props.PermissionOverwrites.Value.Select(overwrite => new API.Overwrite + { + TargetId = overwrite.TargetId, + TargetType = overwrite.TargetType, + Allow = overwrite.Permissions.AllowValue, + Deny = overwrite.Permissions.DenyValue + }).ToArray() + : Optional.Create(), }; var model = await client.ApiClient.CreateGuildChannelAsync(guild.Id, args, options).ConfigureAwait(false); return RestVoiceChannel.Create(client, guild, model); @@ -211,7 +230,16 @@ namespace Discord.Rest var args = new CreateGuildChannelParams(name, ChannelType.Category) { - Position = props.Position + Position = props.Position, + Overwrites = props.PermissionOverwrites.IsSpecified + ? props.PermissionOverwrites.Value.Select(overwrite => new API.Overwrite + { + TargetId = overwrite.TargetId, + TargetType = overwrite.TargetType, + Allow = overwrite.Permissions.AllowValue, + Deny = overwrite.Permissions.DenyValue + }).ToArray() + : Optional.Create(), }; var model = await client.ApiClient.CreateGuildChannelAsync(guild.Id, args, options).ConfigureAwait(false); @@ -264,19 +292,18 @@ namespace Discord.Rest { if (name == null) throw new ArgumentNullException(paramName: nameof(name)); - var model = await client.ApiClient.CreateGuildRoleAsync(guild.Id, options).ConfigureAwait(false); - var role = RestRole.Create(client, guild, model); - - await role.ModifyAsync(x => + var createGuildRoleParams = new API.Rest.ModifyGuildRoleParams { - x.Name = name; - x.Permissions = (permissions ?? role.Permissions); - x.Color = (color ?? Color.Default); - x.Hoist = isHoisted; - x.Mentionable = isMentionable; - }, options).ConfigureAwait(false); + Color = color?.RawValue ?? Optional.Create(), + Hoist = isHoisted, + Mentionable = isMentionable, + Name = name, + Permissions = permissions?.RawValue ?? Optional.Create() + }; - return role; + var model = await client.ApiClient.CreateGuildRoleAsync(guild.Id, createGuildRoleParams, options).ConfigureAwait(false); + + return RestRole.Create(client, guild, model); } //Users @@ -387,6 +414,17 @@ namespace Discord.Rest model = await client.ApiClient.BeginGuildPruneAsync(guild.Id, args, options).ConfigureAwait(false); return model.Pruned; } + public static async Task> SearchUsersAsync(IGuild guild, BaseDiscordClient client, + string query, int? limit, RequestOptions options) + { + var apiArgs = new SearchGuildMembersParams + { + Query = query, + Limit = limit ?? Optional.Create() + }; + var models = await client.ApiClient.SearchGuildMembersAsync(guild.Id, apiArgs, options).ConfigureAwait(false); + return models.Select(x => RestGuildUser.Create(client, guild, x)).ToImmutableArray(); + } // Audit logs public static IAsyncEnumerable> GetAuditLogsAsync(IGuild guild, BaseDiscordClient client, diff --git a/Discord.NET/Discord.Net.Rest/Entities/Guilds/RestGuild.cs b/Discord.NET/Discord.Net.Rest/Entities/Guilds/RestGuild.cs index 900f504..f0b5be0 100644 --- a/Discord.NET/Discord.Net.Rest/Entities/Guilds/RestGuild.cs +++ b/Discord.NET/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -634,6 +634,23 @@ namespace Discord.Rest public Task PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null) => GuildHelper.PruneUsersAsync(this, Discord, days, simulate, options); + /// + /// Gets a collection of users in this guild that the name or nickname starts with the + /// provided at . + /// + /// + /// The can not be higher than . + /// + /// The partial name or nickname to search. + /// The maximum number of users to be gotten. + /// 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 that the name or nickname starts with the provided at . + /// + public Task> SearchUsersAsync(string query, int limit = DiscordConfig.MaxUsersPerBatch, RequestOptions options = null) + => GuildHelper.SearchUsersAsync(this, Discord, query, limit, options); + //Audit logs /// /// Gets the specified number of audit log entries for this guild. @@ -884,6 +901,14 @@ namespace Discord.Rest /// Downloading users is not supported for a REST-based guild. Task IGuild.DownloadUsersAsync() => throw new NotSupportedException(); + /// + async Task> IGuild.SearchUsersAsync(string query, int limit, CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return await SearchUsersAsync(query, limit, options).ConfigureAwait(false); + else + return ImmutableArray.Create(); + } async Task> IGuild.GetAuditLogsAsync(int limit, CacheMode cacheMode, RequestOptions options, ulong? beforeId, ulong? userId, ActionType? actionType) diff --git a/Discord.NET/Discord.Net.Rest/Entities/Messages/AllowedMentions.cs b/Discord.NET/Discord.Net.Rest/Entities/Messages/AllowedMentions.cs new file mode 100644 index 0000000..5ab9603 --- /dev/null +++ b/Discord.NET/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/Discord.NET/Discord.Net.Rest/Entities/Messages/MessageHelper.cs b/Discord.NET/Discord.Net.Rest/Entities/Messages/MessageHelper.cs index 75892de..d6a718b 100644 --- a/Discord.NET/Discord.Net.Rest/Entities/Messages/MessageHelper.cs +++ b/Discord.NET/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, @@ -39,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) { @@ -71,6 +78,11 @@ namespace Discord.Rest await client.ApiClient.RemoveAllReactionsAsync(msg.Channel.Id, msg.Id, options).ConfigureAwait(false); } + public static async Task RemoveAllReactionsForEmoteAsync(IMessage msg, IEmote emote, BaseDiscordClient client, RequestOptions options) + { + await client.ApiClient.RemoveAllReactionsForEmoteAsync(msg.Channel.Id, msg.Id, emote is Emote e ? $"{e.Name}:{e.Id}" : emote.Name, options).ConfigureAwait(false); + } + public static IAsyncEnumerable> GetReactionUsersAsync(IMessage msg, IEmote emote, int? limit, BaseDiscordClient client, RequestOptions options) { @@ -110,6 +122,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) { @@ -235,6 +248,7 @@ namespace Discord.Rest return tags.ToImmutable(); } + private static int? FindIndex(IReadOnlyList tags, int index) { int i = 0; @@ -248,6 +262,7 @@ namespace Discord.Rest return null; //Overlaps tag before this return i; } + public static ImmutableArray FilterTagsByKey(TagType type, ImmutableArray tags) { return tags @@ -255,6 +270,7 @@ namespace Discord.Rest .Select(x => x.Key) .ToImmutableArray(); } + public static ImmutableArray FilterTagsByValue(TagType type, ImmutableArray tags) { return tags @@ -274,5 +290,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/Discord.NET/Discord.Net.Rest/Entities/Messages/RestMessage.cs b/Discord.NET/Discord.Net.Rest/Entities/Messages/RestMessage.cs index f457f4f..2456e65 100644 --- a/Discord.NET/Discord.Net.Rest/Entities/Messages/RestMessage.cs +++ b/Discord.NET/Discord.Net.Rest/Entities/Messages/RestMessage.cs @@ -37,6 +37,9 @@ namespace Discord.Rest public virtual bool IsSuppressed => false; /// public virtual DateTimeOffset? EditedTimestamp => null; + /// + public virtual bool MentionedEveryone => false; + /// /// Gets a collection of the 's on the message. /// @@ -165,7 +168,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 }); @@ -182,6 +185,9 @@ namespace Discord.Rest public Task RemoveAllReactionsAsync(RequestOptions options = null) => MessageHelper.RemoveAllReactionsAsync(this, Discord, options); /// + public Task RemoveAllReactionsForEmoteAsync(IEmote emote, RequestOptions options = null) + => MessageHelper.RemoveAllReactionsForEmoteAsync(this, emote, Discord, options); + /// public IAsyncEnumerable> GetReactionUsersAsync(IEmote emote, int limit, RequestOptions options = null) => MessageHelper.GetReactionUsersAsync(this, emote, limit, Discord, options); } diff --git a/Discord.NET/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs b/Discord.NET/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs index 7d65268..be955b1 100644 --- a/Discord.NET/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs +++ b/Discord.NET/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs @@ -18,6 +18,8 @@ namespace Discord.Rest private ImmutableArray _attachments = ImmutableArray.Create(); private ImmutableArray _embeds = ImmutableArray.Create(); private ImmutableArray _tags = ImmutableArray.Create(); + private ImmutableArray _roleMentionIds = ImmutableArray.Create(); + private ImmutableArray _userMentions = ImmutableArray.Create(); /// public override bool IsTTS => _isTTS; @@ -28,15 +30,17 @@ namespace Discord.Rest /// public override DateTimeOffset? EditedTimestamp => DateTimeUtils.FromTicks(_editedTimestampTicks); /// + public override bool MentionedEveryone => _isMentioningEveryone; + /// public override IReadOnlyCollection Attachments => _attachments; /// public override IReadOnlyCollection Embeds => _embeds; /// public override IReadOnlyCollection MentionedChannelIds => MessageHelper.FilterTagsByKey(TagType.ChannelMention, _tags); /// - public override IReadOnlyCollection MentionedRoleIds => MessageHelper.FilterTagsByKey(TagType.RoleMention, _tags); + public override IReadOnlyCollection MentionedRoleIds => _roleMentionIds; /// - public override IReadOnlyCollection MentionedUsers => MessageHelper.FilterTagsByValue(TagType.UserMention, _tags); + public override IReadOnlyCollection MentionedUsers => _userMentions; /// public override IReadOnlyCollection Tags => _tags; @@ -67,6 +71,8 @@ namespace Discord.Rest { _isSuppressed = model.Flags.Value.HasFlag(API.MessageFlags.Suppressed); } + if (model.RoleMentions.IsSpecified) + _roleMentionIds = model.RoleMentions.Value.ToImmutableArray(); if (model.Attachments.IsSpecified) { @@ -96,20 +102,19 @@ namespace Discord.Rest _embeds = ImmutableArray.Create(); } - ImmutableArray mentions = ImmutableArray.Create(); if (model.UserMentions.IsSpecified) { var value = model.UserMentions.Value; if (value.Length > 0) { - var newMentions = ImmutableArray.CreateBuilder(value.Length); + var newMentions = ImmutableArray.CreateBuilder(value.Length); for (int i = 0; i < value.Length; i++) { var val = value[i]; if (val.Object != null) newMentions.Add(RestUser.Create(Discord, val.Object)); } - mentions = newMentions.ToImmutable(); + _userMentions = newMentions.ToImmutable(); } } @@ -118,7 +123,7 @@ namespace Discord.Rest var text = model.Content.Value; var guildId = (Channel as IGuildChannel)?.GuildId; var guild = guildId != null ? (Discord as IDiscordClient).GetGuildAsync(guildId.Value, CacheMode.CacheOnly).Result : null; - _tags = MessageHelper.ParseTags(text, null, guild, mentions); + _tags = MessageHelper.ParseTags(text, null, guild, _userMentions); model.Content = text; } } @@ -148,6 +153,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/Discord.NET/Discord.Net.Rest/Entities/Users/RestUser.cs b/Discord.NET/Discord.Net.Rest/Entities/Users/RestUser.cs index d5fffca..f5becd3 100644 --- a/Discord.NET/Discord.Net.Rest/Entities/Users/RestUser.cs +++ b/Discord.NET/Discord.Net.Rest/Entities/Users/RestUser.cs @@ -35,6 +35,8 @@ namespace Discord.Rest /// public virtual IImmutableSet ActiveClients => ImmutableHashSet.Empty; /// + public virtual IImmutableList Activities => ImmutableList.Empty; + /// public virtual bool IsWebhook => false; internal RestUser(BaseDiscordClient discord, ulong id) diff --git a/Discord.NET/Discord.Net.Rest/Extensions/EntityExtensions.cs b/Discord.NET/Discord.Net.Rest/Extensions/EntityExtensions.cs index e265f99..abdfc9d 100644 --- a/Discord.NET/Discord.Net.Rest/Extensions/EntityExtensions.cs +++ b/Discord.NET/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/Discord.NET/Discord.Net.Rest/Net/Queue/ClientBucket.cs b/Discord.NET/Discord.Net.Rest/Net/Queue/ClientBucket.cs index cd9d8aa..e726a08 100644 --- a/Discord.NET/Discord.Net.Rest/Net/Queue/ClientBucket.cs +++ b/Discord.NET/Discord.Net.Rest/Net/Queue/ClientBucket.cs @@ -10,14 +10,14 @@ namespace Discord.Net.Queue internal struct ClientBucket { private static readonly ImmutableDictionary DefsByType; - private static readonly ImmutableDictionary DefsById; + private static readonly ImmutableDictionary DefsById; static ClientBucket() { var buckets = new[] { - new ClientBucket(ClientBucketType.Unbucketed, "", 10, 10), - new ClientBucket(ClientBucketType.SendEdit, "", 10, 10) + new ClientBucket(ClientBucketType.Unbucketed, BucketId.Create(null, "", null), 10, 10), + new ClientBucket(ClientBucketType.SendEdit, BucketId.Create(null, "", null), 10, 10) }; var builder = ImmutableDictionary.CreateBuilder(); @@ -25,21 +25,21 @@ namespace Discord.Net.Queue builder.Add(bucket.Type, bucket); DefsByType = builder.ToImmutable(); - var builder2 = ImmutableDictionary.CreateBuilder(); + var builder2 = ImmutableDictionary.CreateBuilder(); foreach (var bucket in buckets) builder2.Add(bucket.Id, bucket); DefsById = builder2.ToImmutable(); } public static ClientBucket Get(ClientBucketType type) => DefsByType[type]; - public static ClientBucket Get(string id) => DefsById[id]; + public static ClientBucket Get(BucketId id) => DefsById[id]; public ClientBucketType Type { get; } - public string Id { get; } + public BucketId Id { get; } public int WindowCount { get; } public int WindowSeconds { get; } - public ClientBucket(ClientBucketType type, string id, int count, int seconds) + public ClientBucket(ClientBucketType type, BucketId id, int count, int seconds) { Type = type; Id = id; diff --git a/Discord.NET/Discord.Net.Rest/Net/Queue/RequestQueue.cs b/Discord.NET/Discord.Net.Rest/Net/Queue/RequestQueue.cs index 4baf764..691ac77 100644 --- a/Discord.NET/Discord.Net.Rest/Net/Queue/RequestQueue.cs +++ b/Discord.NET/Discord.Net.Rest/Net/Queue/RequestQueue.cs @@ -12,9 +12,9 @@ namespace Discord.Net.Queue { internal class RequestQueue : IDisposable { - public event Func RateLimitTriggered; + public event Func RateLimitTriggered; - private readonly ConcurrentDictionary _buckets; + private readonly ConcurrentDictionary _buckets; private readonly SemaphoreSlim _tokenLock; private readonly CancellationTokenSource _cancelTokenSource; //Dispose token private CancellationTokenSource _clearToken; @@ -34,7 +34,7 @@ namespace Discord.Net.Queue _requestCancelToken = CancellationToken.None; _parentToken = CancellationToken.None; - _buckets = new ConcurrentDictionary(); + _buckets = new ConcurrentDictionary(); _cleanupTask = RunCleanup(); } @@ -82,7 +82,7 @@ namespace Discord.Net.Queue else request.Options.CancelToken = _requestCancelToken; - var bucket = GetOrCreateBucket(request.Options.BucketId, request); + var bucket = GetOrCreateBucket(request.Options, request); var result = await bucket.SendAsync(request).ConfigureAwait(false); createdTokenSource?.Dispose(); return result; @@ -110,14 +110,32 @@ namespace Discord.Net.Queue _waitUntil = DateTimeOffset.UtcNow.AddMilliseconds(info.RetryAfter.Value + (info.Lag?.TotalMilliseconds ?? 0.0)); } - private RequestBucket GetOrCreateBucket(string id, RestRequest request) + private RequestBucket GetOrCreateBucket(RequestOptions options, RestRequest request) { - return _buckets.GetOrAdd(id, x => new RequestBucket(this, request, x)); + var bucketId = options.BucketId; + object obj = _buckets.GetOrAdd(bucketId, x => new RequestBucket(this, request, x)); + if (obj is BucketId hashBucket) + { + options.BucketId = hashBucket; + return (RequestBucket)_buckets.GetOrAdd(hashBucket, x => new RequestBucket(this, request, x)); + } + return (RequestBucket)obj; } - internal async Task RaiseRateLimitTriggered(string bucketId, RateLimitInfo? info) + internal async Task RaiseRateLimitTriggered(BucketId bucketId, RateLimitInfo? info) { await RateLimitTriggered(bucketId, info).ConfigureAwait(false); } + internal (RequestBucket, BucketId) UpdateBucketHash(BucketId id, string discordHash) + { + if (!id.IsHashBucket) + { + var bucket = BucketId.Create(discordHash, id); + var hashReqQueue = (RequestBucket)_buckets.GetOrAdd(bucket, _buckets[id]); + _buckets.AddOrUpdate(id, bucket, (oldBucket, oldObj) => bucket); + return (hashReqQueue, bucket); + } + return (null, null); + } private async Task RunCleanup() { @@ -126,10 +144,15 @@ namespace Discord.Net.Queue while (!_cancelTokenSource.IsCancellationRequested) { var now = DateTimeOffset.UtcNow; - foreach (var bucket in _buckets.Select(x => x.Value)) + foreach (var bucket in _buckets.Where(x => x.Value is RequestBucket).Select(x => (RequestBucket)x.Value)) { if ((now - bucket.LastAttemptAt).TotalMinutes > 1.0) + { + if (bucket.Id.IsHashBucket) + foreach (var redirectBucket in _buckets.Where(x => x.Value == bucket.Id).Select(x => (BucketId)x.Value)) + _buckets.TryRemove(redirectBucket, out _); //remove redirections if hash bucket _buckets.TryRemove(bucket.Id, out _); + } } await Task.Delay(60000, _cancelTokenSource.Token).ConfigureAwait(false); //Runs each minute } diff --git a/Discord.NET/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs b/Discord.NET/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs index 72dd164..f1471d5 100644 --- a/Discord.NET/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs +++ b/Discord.NET/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs @@ -13,16 +13,19 @@ namespace Discord.Net.Queue { internal class RequestBucket { + private const int MinimumSleepTimeMs = 750; + private readonly object _lock; private readonly RequestQueue _queue; private int _semaphore; private DateTimeOffset? _resetTick; + private RequestBucket _redirectBucket; - public string Id { get; private set; } + public BucketId Id { get; private set; } public int WindowCount { get; private set; } public DateTimeOffset LastAttemptAt { get; private set; } - public RequestBucket(RequestQueue queue, RestRequest request, string id) + public RequestBucket(RequestQueue queue, RestRequest request, BucketId id) { _queue = queue; Id = id; @@ -30,14 +33,14 @@ namespace Discord.Net.Queue _lock = new object(); if (request.Options.IsClientBucket) - WindowCount = ClientBucket.Get(request.Options.BucketId).WindowCount; + WindowCount = ClientBucket.Get(Id).WindowCount; else WindowCount = 1; //Only allow one request until we get a header back _semaphore = WindowCount; _resetTick = null; LastAttemptAt = DateTimeOffset.UtcNow; } - + static int nextId = 0; public async Task SendAsync(RestRequest request) { @@ -50,6 +53,8 @@ namespace Discord.Net.Queue { await _queue.EnterGlobalAsync(id, request).ConfigureAwait(false); await EnterAsync(id, request).ConfigureAwait(false); + if (_redirectBucket != null) + return await _redirectBucket.SendAsync(request); #if DEBUG_LIMITS Debug.WriteLine($"[{id}] Sending..."); @@ -158,6 +163,9 @@ namespace Discord.Net.Queue while (true) { + if (_redirectBucket != null) + break; + if (DateTimeOffset.UtcNow > request.TimeoutAt || request.Options.CancelToken.IsCancellationRequested) { if (!isRateLimited) @@ -173,7 +181,8 @@ namespace Discord.Net.Queue } DateTimeOffset? timeoutAt = request.TimeoutAt; - if (windowCount > 0 && Interlocked.Decrement(ref _semaphore) < 0) + int semaphore = Interlocked.Decrement(ref _semaphore); + if (windowCount > 0 && semaphore < 0) { if (!isRateLimited) { @@ -183,10 +192,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,31 +206,63 @@ 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; } #if DEBUG_LIMITS else - Debug.WriteLine($"[{id}] Entered Semaphore ({_semaphore}/{WindowCount} remaining)"); + Debug.WriteLine($"[{id}] Entered Semaphore ({semaphore}/{WindowCount} remaining)"); #endif break; } } - private void UpdateRateLimit(int id, RestRequest request, RateLimitInfo info, bool is429) + private void UpdateRateLimit(int id, RestRequest request, RateLimitInfo info, bool is429, bool redirected = false) { if (WindowCount == 0) return; lock (_lock) { + if (redirected) + { + Interlocked.Decrement(ref _semaphore); //we might still hit a real ratelimit if all tickets were already taken, can't do much about it since we didn't know they were the same +#if DEBUG_LIMITS + Debug.WriteLine($"[{id}] Decrease Semaphore"); +#endif + } bool hasQueuedReset = _resetTick != null; + + if (info.Bucket != null && !redirected) + { + (RequestBucket, BucketId) hashBucket = _queue.UpdateBucketHash(Id, info.Bucket); + if (!(hashBucket.Item1 is null) && !(hashBucket.Item2 is null)) + { + if (hashBucket.Item1 == this) //this bucket got promoted to a hash queue + { + Id = hashBucket.Item2; +#if DEBUG_LIMITS + Debug.WriteLine($"[{id}] Promoted to Hash Bucket ({hashBucket.Item2})"); +#endif + } + else + { + _redirectBucket = hashBucket.Item1; //this request should be part of another bucket, this bucket will be disabled, redirect everything + _redirectBucket.UpdateRateLimit(id, request, info, is429, redirected: true); //update the hash bucket ratelimit +#if DEBUG_LIMITS + Debug.WriteLine($"[{id}] Redirected to {_redirectBucket.Id}"); +#endif + return; + } + } + } + if (info.Limit.HasValue && WindowCount != info.Limit.Value) { WindowCount = info.Limit.Value; @@ -230,7 +272,6 @@ namespace Discord.Net.Queue #endif } - var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); DateTimeOffset? resetTick = null; //Using X-RateLimit-Remaining causes a race condition @@ -247,16 +288,18 @@ namespace Discord.Net.Queue Debug.WriteLine($"[{id}] Retry-After: {info.RetryAfter.Value} ({info.RetryAfter.Value} ms)"); #endif } - else if (info.ResetAfter.HasValue && (request.Options.UseSystemClock.HasValue ? !request.Options.UseSystemClock.Value : false)) - { - resetTick = DateTimeOffset.Now.Add(info.ResetAfter.Value); - } + else if (info.ResetAfter.HasValue && (request.Options.UseSystemClock.HasValue ? !request.Options.UseSystemClock.Value : false)) + { + resetTick = DateTimeOffset.UtcNow.Add(info.ResetAfter.Value); +#if DEBUG_LIMITS + Debug.WriteLine($"[{id}] Reset-After: {info.ResetAfter.Value} ({info.ResetAfter?.TotalMilliseconds} ms)"); +#endif + } else if (info.Reset.HasValue) { resetTick = info.Reset.Value.AddSeconds(info.Lag?.TotalSeconds ?? 1.0); - /* millisecond precision makes this unnecessary, retaining in case of regression - + /* millisecond precision makes this unnecessary, retaining in case of regression if (request.Options.IsReactionBucket) resetTick = DateTimeOffset.Now.AddMilliseconds(250); */ @@ -266,17 +309,17 @@ namespace Discord.Net.Queue Debug.WriteLine($"[{id}] X-RateLimit-Reset: {info.Reset.Value.ToUnixTimeSeconds()} ({diff} ms, {info.Lag?.TotalMilliseconds} ms lag)"); #endif } - else if (request.Options.IsClientBucket && request.Options.BucketId != null) + else if (request.Options.IsClientBucket && Id != null) { - resetTick = DateTimeOffset.UtcNow.AddSeconds(ClientBucket.Get(request.Options.BucketId).WindowSeconds); + resetTick = DateTimeOffset.UtcNow.AddSeconds(ClientBucket.Get(Id).WindowSeconds); #if DEBUG_LIMITS - Debug.WriteLine($"[{id}] Client Bucket ({ClientBucket.Get(request.Options.BucketId).WindowSeconds * 1000} ms)"); + Debug.WriteLine($"[{id}] Client Bucket ({ClientBucket.Get(Id).WindowSeconds * 1000} ms)"); #endif } if (resetTick == null) { - WindowCount = 0; //No rate limit info, disable limits on this bucket (should only ever happen with a user token) + WindowCount = 0; //No rate limit info, disable limits on this bucket #if DEBUG_LIMITS Debug.WriteLine($"[{id}] Disabled Semaphore"); #endif diff --git a/Discord.NET/Discord.Net.Rest/Net/RateLimitInfo.cs b/Discord.NET/Discord.Net.Rest/Net/RateLimitInfo.cs index 13e9e39..6a7df7b 100644 --- a/Discord.NET/Discord.Net.Rest/Net/RateLimitInfo.cs +++ b/Discord.NET/Discord.Net.Rest/Net/RateLimitInfo.cs @@ -11,7 +11,8 @@ namespace Discord.Net public int? Remaining { get; } public int? RetryAfter { get; } public DateTimeOffset? Reset { get; } - public TimeSpan? ResetAfter { get; } + public TimeSpan? ResetAfter { get; } + public string Bucket { get; } public TimeSpan? Lag { get; } internal RateLimitInfo(Dictionary headers) @@ -26,8 +27,9 @@ namespace Discord.Net double.TryParse(temp, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var reset) ? DateTimeOffset.FromUnixTimeMilliseconds((long)(reset * 1000)) : (DateTimeOffset?)null; RetryAfter = headers.TryGetValue("Retry-After", out temp) && int.TryParse(temp, NumberStyles.None, CultureInfo.InvariantCulture, out var retryAfter) ? retryAfter : (int?)null; - ResetAfter = headers.TryGetValue("X-RateLimit-Reset-After", out temp) && - float.TryParse(temp, out var resetAfter) ? TimeSpan.FromMilliseconds((long)(resetAfter * 1000)) : (TimeSpan?)null; + ResetAfter = headers.TryGetValue("X-RateLimit-Reset-After", out temp) && + double.TryParse(temp, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var resetAfter) ? TimeSpan.FromMilliseconds((long)(resetAfter * 1000)) : (TimeSpan?)null; + Bucket = headers.TryGetValue("X-RateLimit-Bucket", out temp) ? temp : null; Lag = headers.TryGetValue("Date", out temp) && DateTimeOffset.TryParse(temp, CultureInfo.InvariantCulture, DateTimeStyles.None, out var date) ? DateTimeOffset.UtcNow - date : (TimeSpan?)null; } diff --git a/Discord.NET/Discord.Net.WebSocket/API/Gateway/IdentifyParams.cs b/Discord.NET/Discord.Net.WebSocket/API/Gateway/IdentifyParams.cs index 1e0bf71..e3e2449 100644 --- a/Discord.NET/Discord.Net.WebSocket/API/Gateway/IdentifyParams.cs +++ b/Discord.NET/Discord.Net.WebSocket/API/Gateway/IdentifyParams.cs @@ -1,4 +1,4 @@ -ο»Ώ#pragma warning disable CS1591 +#pragma warning disable CS1591 using Newtonsoft.Json; using System.Collections.Generic; @@ -17,5 +17,7 @@ namespace Discord.API.Gateway public Optional ShardingParams { get; set; } [JsonProperty("guild_subscriptions")] public Optional GuildSubscriptions { get; set; } + [JsonProperty("intents")] + public Optional Intents { get; set; } } } diff --git a/Discord.NET/Discord.Net.WebSocket/API/Gateway/RemoveAllReactionsForEmoteEvent.cs b/Discord.NET/Discord.Net.WebSocket/API/Gateway/RemoveAllReactionsForEmoteEvent.cs new file mode 100644 index 0000000..7f804d3 --- /dev/null +++ b/Discord.NET/Discord.Net.WebSocket/API/Gateway/RemoveAllReactionsForEmoteEvent.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace Discord.API.Gateway +{ + internal class RemoveAllReactionsForEmoteEvent + { + [JsonProperty("channel_id")] + public ulong ChannelId { get; set; } + [JsonProperty("guild_id")] + public Optional GuildId { get; set; } + [JsonProperty("message_id")] + public ulong MessageId { get; set; } + [JsonProperty("emoji")] + public Emoji Emoji { get; set; } + } +} diff --git a/Discord.NET/Discord.Net.WebSocket/AssemblyInfo.cs b/Discord.NET/Discord.Net.WebSocket/AssemblyInfo.cs deleted file mode 100644 index 442ec7d..0000000 --- a/Discord.NET/Discord.Net.WebSocket/AssemblyInfo.cs +++ /dev/null @@ -1,5 +0,0 @@ -using System.Runtime.CompilerServices; - -[assembly: InternalsVisibleTo("Discord.Net.Relay")] -[assembly: InternalsVisibleTo("Discord.Net.Tests")] -[assembly: InternalsVisibleTo("Discord.Net.Tests.Unit")] diff --git a/Discord.NET/Discord.Net.WebSocket/BaseSocketClient.Events.cs b/Discord.NET/Discord.Net.WebSocket/BaseSocketClient.Events.cs index 908314f..2cd62b3 100644 --- a/Discord.NET/Discord.Net.WebSocket/BaseSocketClient.Events.cs +++ b/Discord.NET/Discord.Net.WebSocket/BaseSocketClient.Events.cs @@ -234,6 +234,28 @@ namespace Discord.WebSocket remove { _reactionsClearedEvent.Remove(value); } } internal readonly AsyncEvent, ISocketMessageChannel, Task>> _reactionsClearedEvent = new AsyncEvent, ISocketMessageChannel, Task>>(); + /// + /// Fired when all reactions to a message with a specific emote are removed. + /// + /// + /// + /// This event is fired when all reactions to a message with a specific emote are removed. + /// The event handler must return a and accept a and + /// a as its parameters. + /// + /// + /// The channel where this message was sent will be passed into the parameter. + /// + /// + /// The emoji that all reactions had and were removed will be passed into the parameter. + /// + /// + public event Func, ISocketMessageChannel, IEmote, Task> ReactionsRemovedForEmote + { + add { _reactionsRemovedForEmoteEvent.Add(value); } + remove { _reactionsRemovedForEmoteEvent.Remove(value); } + } + internal readonly AsyncEvent, ISocketMessageChannel, IEmote, Task>> _reactionsRemovedForEmoteEvent = new AsyncEvent, ISocketMessageChannel, IEmote, Task>>(); //Roles /// Fired when a role is created. diff --git a/Discord.NET/Discord.Net.WebSocket/ClientState.cs b/Discord.NET/Discord.Net.WebSocket/ClientState.cs index dad185d..f2e370d 100644 --- a/Discord.NET/Discord.Net.WebSocket/ClientState.cs +++ b/Discord.NET/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/Discord.NET/Discord.Net.WebSocket/ConnectionManager.cs b/Discord.NET/Discord.Net.WebSocket/ConnectionManager.cs index 8c9c743..2237e2d 100644 --- a/Discord.NET/Discord.Net.WebSocket/ConnectionManager.cs +++ b/Discord.NET/Discord.Net.WebSocket/ConnectionManager.cs @@ -44,6 +44,8 @@ namespace Discord var ex2 = ex as WebSocketClosedException; if (ex2?.CloseCode == 4006) CriticalError(new Exception("WebSocket session expired", ex)); + else if (ex2?.CloseCode == 4014) + CriticalError(new Exception("WebSocket connection was closed", ex)); else Error(new Exception("WebSocket connection was closed", ex)); } @@ -141,7 +143,16 @@ namespace Discord catch (OperationCanceledException) { } }); - await _onConnecting().ConfigureAwait(false); + try + { + await _onConnecting().ConfigureAwait(false); + } + catch (TaskCanceledException ex) + { + Exception innerEx = ex.InnerException ?? new OperationCanceledException("Failed to connect."); + Error(innerEx); + throw innerEx; + } await _logger.InfoAsync("Connected").ConfigureAwait(false); State = ConnectionState.Connected; diff --git a/Discord.NET/Discord.Net.WebSocket/DiscordShardedClient.cs b/Discord.NET/Discord.Net.WebSocket/DiscordShardedClient.cs index 0877abf..930ea15 100644 --- a/Discord.NET/Discord.Net.WebSocket/DiscordShardedClient.cs +++ b/Discord.NET/Discord.Net.WebSocket/DiscordShardedClient.cs @@ -313,6 +313,7 @@ namespace Discord.WebSocket client.ReactionAdded += (cache, channel, reaction) => _reactionAddedEvent.InvokeAsync(cache, channel, reaction); client.ReactionRemoved += (cache, channel, reaction) => _reactionRemovedEvent.InvokeAsync(cache, channel, reaction); client.ReactionsCleared += (cache, channel) => _reactionsClearedEvent.InvokeAsync(cache, channel); + client.ReactionsRemovedForEmote += (cache, channel, emote) => _reactionsRemovedForEmoteEvent.InvokeAsync(cache, channel, emote); client.RoleCreated += (role) => _roleCreatedEvent.InvokeAsync(role); client.RoleDeleted += (role) => _roleDeletedEvent.InvokeAsync(role); @@ -389,8 +390,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(); } diff --git a/Discord.NET/Discord.Net.WebSocket/DiscordSocketApiClient.cs b/Discord.NET/Discord.Net.WebSocket/DiscordSocketApiClient.cs index 9313f07..1b21bd6 100644 --- a/Discord.NET/Discord.Net.WebSocket/DiscordSocketApiClient.cs +++ b/Discord.NET/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; @@ -215,7 +209,7 @@ namespace Discord.API await _sentGatewayMessageEvent.InvokeAsync(opCode).ConfigureAwait(false); } - public async Task SendIdentifyAsync(int largeThreshold = 100, int shardID = 0, int totalShards = 1, bool guildSubscriptions = true, RequestOptions options = null) + public async Task SendIdentifyAsync(int largeThreshold = 100, int shardID = 0, int totalShards = 1, bool guildSubscriptions = true, GatewayIntents? gatewayIntents = null, RequestOptions options = null) { options = RequestOptions.CreateOrClone(options); var props = new Dictionary @@ -226,12 +220,16 @@ namespace Discord.API { Token = AuthToken, Properties = props, - LargeThreshold = largeThreshold, - GuildSubscriptions = guildSubscriptions + LargeThreshold = largeThreshold }; if (totalShards > 1) msg.ShardingParams = new int[] { shardID, totalShards }; + if (gatewayIntents.HasValue) + msg.Intents = (int)gatewayIntents.Value; + else + msg.GuildSubscriptions = guildSubscriptions; + await SendGatewayAsync(GatewayOpCode.Identify, msg, options: options).ConfigureAwait(false); } public async Task SendResumeAsync(string sessionId, int lastSeq, RequestOptions options = null) diff --git a/Discord.NET/Discord.Net.WebSocket/DiscordSocketClient.Events.cs b/Discord.NET/Discord.Net.WebSocket/DiscordSocketClient.Events.cs index 51dea5f..0418727 100644 --- a/Discord.NET/Discord.Net.WebSocket/DiscordSocketClient.Events.cs +++ b/Discord.NET/Discord.Net.WebSocket/DiscordSocketClient.Events.cs @@ -21,7 +21,13 @@ namespace Discord.WebSocket remove { _disconnectedEvent.Remove(value); } } private readonly AsyncEvent> _disconnectedEvent = new AsyncEvent>(); - /// Fired when guild data has finished downloading. + /// + /// Fired when guild data has finished downloading. + /// + /// + /// It is possible that some guilds might be unsynced if + /// was not long enough to receive all GUILD_AVAILABLEs before READY. + /// public event Func Ready { add { _readyEvent.Add(value); } diff --git a/Discord.NET/Discord.Net.WebSocket/DiscordSocketClient.cs b/Discord.NET/Discord.Net.WebSocket/DiscordSocketClient.cs index e70457f..0263f96 100644 --- a/Discord.NET/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/Discord.NET/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -44,6 +44,7 @@ namespace Discord.WebSocket private RestApplication _applicationInfo; private bool _isDisposed; private bool _guildSubscriptions; + private GatewayIntents? _gatewayIntents; /// /// Provides access to a REST-only client with a shared state from this client. @@ -137,6 +138,7 @@ namespace Discord.WebSocket Rest = new DiscordSocketRestClient(config, ApiClient); _heartbeatTimes = new ConcurrentQueue(); _guildSubscriptions = config.GuildSubscriptions; + _gatewayIntents = config.GatewayIntents; _stateLock = new SemaphoreSlim(1, 1); _gatewayLogger = LogManager.CreateLogger(ShardId == 0 && TotalShards == 1 ? "Gateway" : $"Shard #{ShardId}"); @@ -167,7 +169,7 @@ namespace Discord.WebSocket GuildAvailable += g => { - if (ConnectionState == ConnectionState.Connected && AlwaysDownloadUsers && !g.HasAllMembers) + if (_guildDownloadTask?.IsCompleted == true && ConnectionState == ConnectionState.Connected && AlwaysDownloadUsers && !g.HasAllMembers) { var _ = g.DownloadUsersAsync(); } @@ -242,7 +244,7 @@ namespace Discord.WebSocket else { await _gatewayLogger.DebugAsync("Identifying").ConfigureAwait(false); - await ApiClient.SendIdentifyAsync(shardID: ShardId, totalShards: TotalShards, guildSubscriptions: _guildSubscriptions).ConfigureAwait(false); + await ApiClient.SendIdentifyAsync(shardID: ShardId, totalShards: TotalShards, guildSubscriptions: _guildSubscriptions, gatewayIntents: _gatewayIntents).ConfigureAwait(false); } //Wait for READY @@ -264,7 +266,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); @@ -306,6 +308,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 +323,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 => @@ -328,7 +342,7 @@ namespace Discord.WebSocket { var user = SocketGlobalUser.Create(this, state, model); user.GlobalUser.AddRef(); - user.Presence = new SocketPresence(UserStatus.Online, null, null); + user.Presence = new SocketPresence(UserStatus.Online, null, null, null); return user; }); } @@ -356,7 +370,7 @@ namespace Discord.WebSocket { var cachedGuilds = guilds.ToImmutableArray(); - const short batchSize = 50; + int batchSize = _gatewayIntents.HasValue ? 1 : 100; ulong[] batchIds = new ulong[Math.Min(batchSize, cachedGuilds.Length)]; Task[] batchTasks = new Task[batchIds.Length]; int batchCount = (cachedGuilds.Length + (batchSize - 1)) / batchSize; @@ -364,7 +378,7 @@ namespace Discord.WebSocket for (int i = 0, k = 0; i < batchCount; i++) { bool isLast = i == batchCount - 1; - int count = isLast ? (batchIds.Length - (batchCount - 1) * batchSize) : batchSize; + int count = isLast ? (cachedGuilds.Length - (batchCount - 1) * batchSize) : batchSize; for (int j = 0; j < count; j++, k++) { @@ -436,7 +450,7 @@ namespace Discord.WebSocket return; var status = Status; var statusSince = _statusSince; - CurrentUser.Presence = new SocketPresence(status, Activity, null); + CurrentUser.Presence = new SocketPresence(status, Activity, null, null); var gameModel = new GameModel(); // Discord only accepts rich presence over RPC, don't even bother building a payload @@ -505,13 +519,13 @@ namespace Discord.WebSocket _sessionId = null; _lastSeq = 0; - await ApiClient.SendIdentifyAsync(shardID: ShardId, totalShards: TotalShards).ConfigureAwait(false); + await ApiClient.SendIdentifyAsync(shardID: ShardId, totalShards: TotalShards, guildSubscriptions: _guildSubscriptions, gatewayIntents: _gatewayIntents).ConfigureAwait(false); } break; 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: @@ -534,7 +548,7 @@ namespace Discord.WebSocket { var model = data.Guilds[i]; var guild = AddGuild(model, state); - if (!guild.IsAvailable || ApiClient.AuthTokenType == TokenType.User) + if (!guild.IsAvailable) unavailableGuilds++; else await GuildAvailableAsync(guild).ConfigureAwait(false); @@ -553,9 +567,6 @@ namespace Discord.WebSocket return; } - if (ApiClient.AuthTokenType == TokenType.User) - await SyncGuildsAsync().ConfigureAwait(false); - _lastGuildAvailableTime = Environment.TickCount; _guildDownloadTask = WaitForGuildsAsync(_connection.CancelToken, _gatewayLogger) .ContinueWith(async x => @@ -567,6 +578,9 @@ namespace Discord.WebSocket } else if (_connection.CancelToken.IsCancellationRequested) return; + + if (BaseConfig.AlwaysDownloadUsers) + _ = DownloadUsersAsync(Guilds.Where(x => x.IsAvailable && !x.HasAllMembers)); await TimedInvokeAsync(_readyEvent, nameof(Ready)).ConfigureAwait(false); await _gatewayLogger.InfoAsync("Ready").ConfigureAwait(false); @@ -630,9 +644,8 @@ namespace Discord.WebSocket var guild = AddGuild(data, State); if (guild != null) { - if (ApiClient.AuthTokenType == TokenType.User) - await SyncGuildsAsync().ConfigureAwait(false); await TimedInvokeAsync(_joinedGuildEvent, nameof(JoinedGuild), guild).ConfigureAwait(false); + await GuildAvailableAsync(guild).ConfigureAwait(false); } else { @@ -890,6 +903,13 @@ namespace Discord.WebSocket if (user != null) { + var globalBefore = user.GlobalUser.Clone(); + if (user.GlobalUser.Update(State, data.User)) + { + //Global data was updated, trigger UserUpdated + await TimedInvokeAsync(_userUpdatedEvent, nameof(UserUpdated), globalBefore, user).ConfigureAwait(false); + } + var before = user.Clone(); user.Update(State, data); await TimedInvokeAsync(_guildMemberUpdatedEvent, nameof(GuildMemberUpdated), before, user).ConfigureAwait(false); @@ -1383,6 +1403,34 @@ namespace Discord.WebSocket } } break; + case "MESSAGE_REACTION_REMOVE_EMOJI": + { + await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_REACTION_REMOVE_EMOJI)").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + if (State.GetChannel(data.ChannelId) is ISocketMessageChannel channel) + { + var cachedMsg = channel.GetCachedMessage(data.MessageId) as SocketUserMessage; + bool isCached = cachedMsg != null; + + var optionalMsg = !isCached + ? Optional.Create() + : Optional.Create(cachedMsg); + + var cacheable = new Cacheable(cachedMsg, data.MessageId, isCached, async () => await channel.GetMessageAsync(data.MessageId).ConfigureAwait(false) as IUserMessage); + var emote = data.Emoji.ToIEmote(); + + cachedMsg?.RemoveAllReactionsForEmoteAsync(emote); + + await TimedInvokeAsync(_reactionsRemovedForEmoteEvent, nameof(ReactionsRemovedForEmote), cacheable, channel, emote).ConfigureAwait(false); + } + else + { + await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); + return; + } + } + break; case "MESSAGE_DELETE_BULK": { await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_DELETE_BULK)").ConfigureAwait(false); @@ -1694,7 +1742,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; } } @@ -1734,7 +1782,7 @@ namespace Discord.WebSocket try { await logger.DebugAsync("GuildDownloader Started").ConfigureAwait(false); - while ((_unavailableGuildCount != 0) && (Environment.TickCount - _lastGuildAvailableTime < 2000)) + while ((_unavailableGuildCount != 0) && (Environment.TickCount - _lastGuildAvailableTime < BaseConfig.MaxWaitBetweenGuildAvailablesBeforeReady)) await Task.Delay(500, cancelToken).ConfigureAwait(false); await logger.DebugAsync("GuildDownloader Stopped").ConfigureAwait(false); } @@ -1763,17 +1811,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/Discord.NET/Discord.Net.WebSocket/DiscordSocketConfig.cs b/Discord.NET/Discord.Net.WebSocket/DiscordSocketConfig.cs index 98ab0ef..11e44d8 100644 --- a/Discord.NET/Discord.Net.WebSocket/DiscordSocketConfig.cs +++ b/Discord.NET/Discord.Net.WebSocket/DiscordSocketConfig.cs @@ -87,7 +87,7 @@ namespace Discord.WebSocket /// /// /// For more information, please see - /// Request Guild Members + /// Request Guild Members /// on the official Discord API documentation. /// /// @@ -121,9 +121,45 @@ namespace Discord.WebSocket /// /// Gets or sets enabling dispatching of guild subscription events e.g. presence and typing events. + /// This is not used if are provided. /// public bool GuildSubscriptions { get; set; } = true; + /// + /// Gets or sets the maximum wait time in milliseconds between GUILD_AVAILABLE events before firing READY. + /// + /// If zero, READY will fire as soon as it is received and all guilds will be unavailable. + /// + /// + /// This property is measured in milliseconds, negative values will throw an exception. + /// If a guild is not received before READY, it will be unavailable. + /// + /// + /// The maximum wait time in milliseconds between GUILD_AVAILABLE events before firing READY. + /// + /// Value must be at least 0. + public int MaxWaitBetweenGuildAvailablesBeforeReady { + get + { + return _maxWaitForGuildAvailable; + } + set + { + Preconditions.AtLeast(value, 0, nameof(MaxWaitBetweenGuildAvailablesBeforeReady)); + _maxWaitForGuildAvailable = value; + } + } + private int _maxWaitForGuildAvailable = 10000; + + /// Gets or sets gateway intents to limit what events are sent from Discord. Allows for more granular control than the property. + /// + /// + /// For more information, please see + /// GatewayIntents + /// on the official Discord API documentation. + /// + public GatewayIntents? GatewayIntents { get; set; } + /// /// Initializes a default configuration. /// diff --git a/Discord.NET/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs b/Discord.NET/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs index b88d010..e8511f1 100644 --- a/Discord.NET/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs +++ b/Discord.NET/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs @@ -29,16 +29,20 @@ 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. /// /// - /// This method follows the same behavior as described in . + /// This method follows the same behavior as described in . /// Please visit its documentation for more details on this method. /// /// The file path of the file. @@ -47,16 +51,20 @@ namespace Discord.WebSocket /// The to be sent. /// The options to be used when sending the request. /// Whether the message attachment should be hidden as a spoiler. + /// + /// Specifies if notifications are sent for mentioned users and roles in the message . + /// If null, all mentioned roles and users will be notified. + /// /// /// A task that represents an asynchronous send operation for delivering the message. The task result /// contains the sent message. /// - new Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false); + new Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null); /// /// Sends a file to this message channel with an optional caption. /// /// - /// This method follows the same behavior as described in . + /// This method follows the same behavior as described in . /// Please visit its documentation for more details on this method. /// /// The of the file to be sent. @@ -66,11 +74,15 @@ namespace Discord.WebSocket /// The to be sent. /// The options to be used when sending the request. /// Whether the message attachment should be hidden as a spoiler. + /// + /// Specifies if notifications are sent for mentioned users and roles in the message . + /// If null, all mentioned roles and users will be notified. + /// /// /// A task that represents an asynchronous send operation for delivering the message. The task result /// contains the sent message. /// - new Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false); + new Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null); /// /// Gets a cached message from this channel. diff --git a/Discord.NET/Discord.Net.WebSocket/Entities/Channels/SocketChannelHelper.cs b/Discord.NET/Discord.Net.WebSocket/Entities/Channels/SocketChannelHelper.cs index e6339b6..5cfbcc1 100644 --- a/Discord.NET/Discord.Net.WebSocket/Entities/Channels/SocketChannelHelper.cs +++ b/Discord.NET/Discord.Net.WebSocket/Entities/Channels/SocketChannelHelper.cs @@ -11,23 +11,11 @@ namespace Discord.WebSocket public static IAsyncEnumerable> GetMessagesAsync(ISocketMessageChannel channel, DiscordSocketClient discord, MessageCache messages, ulong? fromMessageId, Direction dir, int limit, CacheMode mode, RequestOptions options) { - if (dir == Direction.Around) - throw new NotImplementedException(); //TODO: Impl - - IReadOnlyCollection cachedMessages = null; - IAsyncEnumerable> result = null; - if (dir == Direction.After && fromMessageId == null) return AsyncEnumerable.Empty>(); - if (dir == Direction.Before || mode == CacheMode.CacheOnly) - { - if (messages != null) //Cache enabled - cachedMessages = messages.GetMany(fromMessageId, dir, limit); - else - cachedMessages = ImmutableArray.Create(); - result = ImmutableArray.Create(cachedMessages).ToAsyncEnumerable>(); - } + var cachedMessages = GetCachedMessages(channel, discord, messages, fromMessageId, dir, limit); + var result = ImmutableArray.Create(cachedMessages).ToAsyncEnumerable>(); if (dir == Direction.Before) { @@ -38,18 +26,35 @@ namespace Discord.WebSocket //Download remaining messages ulong? minId = cachedMessages.Count > 0 ? cachedMessages.Min(x => x.Id) : fromMessageId; var downloadedMessages = ChannelHelper.GetMessagesAsync(channel, discord, minId, dir, limit, options); - return result.Concat(downloadedMessages); + if (cachedMessages.Count != 0) + return result.Concat(downloadedMessages); + else + return downloadedMessages; } - else + else if (dir == Direction.After) + { + limit -= cachedMessages.Count; + if (mode == CacheMode.CacheOnly || limit <= 0) + return result; + + //Download remaining messages + ulong maxId = cachedMessages.Count > 0 ? cachedMessages.Max(x => x.Id) : fromMessageId.Value; + var downloadedMessages = ChannelHelper.GetMessagesAsync(channel, discord, maxId, dir, limit, options); + if (cachedMessages.Count != 0) + return result.Concat(downloadedMessages); + else + return downloadedMessages; + } + else //Direction.Around { - if (mode == CacheMode.CacheOnly) + if (mode == CacheMode.CacheOnly || limit <= cachedMessages.Count) return result; - //Dont use cache in this case + //Cache isn't useful here since Discord will send them anyways return ChannelHelper.GetMessagesAsync(channel, discord, fromMessageId, dir, limit, options); } } - public static IReadOnlyCollection GetCachedMessages(SocketChannel channel, DiscordSocketClient discord, MessageCache messages, + public static IReadOnlyCollection GetCachedMessages(ISocketMessageChannel channel, DiscordSocketClient discord, MessageCache messages, ulong? fromMessageId, Direction dir, int limit) { if (messages != null) //Cache enabled diff --git a/Discord.NET/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs b/Discord.NET/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs index 838fb8e..5276855 100644 --- a/Discord.NET/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs +++ b/Discord.NET/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs @@ -135,16 +135,16 @@ 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) - => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options, isSpoiler); + public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null) + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, options, isSpoiler); /// /// Message content is too long, length must be less or equal to . - public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false) - => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options, isSpoiler); + public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null) + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, options, isSpoiler); /// public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) => ChannelHelper.DeleteMessageAsync(this, messageId, Discord, options); @@ -229,14 +229,14 @@ namespace Discord.WebSocket async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) => await GetPinnedMessagesAsync(options).ConfigureAwait(false); /// - async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler) - => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions) + => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions).ConfigureAwait(false); /// - 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.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions) + => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions).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/Discord.NET/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs b/Discord.NET/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs index 26fcbe8..b95bbff 100644 --- a/Discord.NET/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs +++ b/Discord.NET/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs @@ -163,15 +163,15 @@ 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) - => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options, isSpoiler); + public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null) + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, options, isSpoiler); /// - public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false) - => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options, isSpoiler); + public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null) + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, options, isSpoiler); /// public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) @@ -293,14 +293,14 @@ namespace Discord.WebSocket => await GetPinnedMessagesAsync(options).ConfigureAwait(false); /// - async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler) - => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions) + => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions).ConfigureAwait(false); /// - 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.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions) + => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions).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/Discord.NET/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs b/Discord.NET/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs index c65f3be..3cc8496 100644 --- a/Discord.NET/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs +++ b/Discord.NET/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs @@ -125,7 +125,6 @@ namespace Discord.WebSocket public virtual async Task AddPermissionOverwriteAsync(IUser user, OverwritePermissions permissions, RequestOptions options = null) { await ChannelHelper.AddPermissionOverwriteAsync(this, Discord, user, permissions, options).ConfigureAwait(false); - _overwrites = _overwrites.Add(new Overwrite(user.Id, PermissionTarget.User, new OverwritePermissions(permissions.AllowValue, permissions.DenyValue))); } /// @@ -140,7 +139,6 @@ namespace Discord.WebSocket public virtual async Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options = null) { await ChannelHelper.AddPermissionOverwriteAsync(this, Discord, role, permissions, options).ConfigureAwait(false); - _overwrites = _overwrites.Add(new Overwrite(role.Id, PermissionTarget.Role, new OverwritePermissions(permissions.AllowValue, permissions.DenyValue))); } /// /// Removes the permission overwrite for the given user, if one exists. @@ -153,15 +151,6 @@ namespace Discord.WebSocket public virtual async Task RemovePermissionOverwriteAsync(IUser user, RequestOptions options = null) { await ChannelHelper.RemovePermissionOverwriteAsync(this, Discord, user, options).ConfigureAwait(false); - - for (int i = 0; i < _overwrites.Length; i++) - { - if (_overwrites[i].TargetId == user.Id) - { - _overwrites = _overwrites.RemoveAt(i); - return; - } - } } /// /// Removes the permission overwrite for the given role, if one exists. @@ -174,15 +163,6 @@ namespace Discord.WebSocket public virtual async Task RemovePermissionOverwriteAsync(IRole role, RequestOptions options = null) { await ChannelHelper.RemovePermissionOverwriteAsync(this, Discord, role, options).ConfigureAwait(false); - - for (int i = 0; i < _overwrites.Length; i++) - { - if (_overwrites[i].TargetId == role.Id) - { - _overwrites = _overwrites.RemoveAt(i); - return; - } - } } public new virtual SocketGuildUser GetUser(ulong id) => null; diff --git a/Discord.NET/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs b/Discord.NET/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs index ca7ca11..e49e3ed 100644 --- a/Discord.NET/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs +++ b/Discord.NET/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs @@ -161,17 +161,17 @@ 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) - => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options, isSpoiler); + public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null) + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, allowedMentions, options, isSpoiler); /// /// Message content is too long, length must be less or equal to . - public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false) - => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options, isSpoiler); + public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null) + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, allowedMentions, options, isSpoiler); /// public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) @@ -302,14 +302,14 @@ namespace Discord.WebSocket => await GetPinnedMessagesAsync(options).ConfigureAwait(false); /// - async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler) - => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions) + => await SendFileAsync(filePath, text, isTTS, embed, options, isSpoiler, allowedMentions).ConfigureAwait(false); /// - 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.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler, AllowedMentions allowedMentions) + => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler, allowedMentions).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/Discord.NET/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/Discord.NET/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index 1643d22..160b915 100644 --- a/Discord.NET/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/Discord.NET/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -304,13 +304,6 @@ namespace Discord.WebSocket _features = ImmutableArray.Create();*/ _syncPromise = new TaskCompletionSource(); _downloaderPromise = new TaskCompletionSource(); - - if (Discord.ApiClient.AuthTokenType != TokenType.User) - { - _syncPromise.TrySetResultAsync(true); - /*if (!model.Large) - _ = _downloaderPromise.TrySetResultAsync(true);*/ - } return; } @@ -388,7 +381,7 @@ namespace Discord.WebSocket Description = model.Description; PremiumSubscriptionCount = model.PremiumSubscriptionCount.GetValueOrDefault(); PreferredLocale = model.PreferredLocale; - PreferredCulture = new CultureInfo(PreferredLocale); + PreferredCulture = PreferredLocale == null ? null : new CultureInfo(PreferredLocale); if (model.Emojis != null) { @@ -630,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 /// @@ -804,6 +804,41 @@ namespace Discord.WebSocket } return null; } + internal void PurgeGuildUserCache() + { + var members = Users; + var self = CurrentUser; + _members.Clear(); + if (self != null) + _members.TryAdd(self.Id, self); + + DownloadedMemberCount = _members.Count; + + foreach (var member in members) + { + if (member.Id != self?.Id) + member.GlobalUser.RemoveRef(Discord); + } + } + + /// + /// 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() @@ -815,6 +850,23 @@ namespace Discord.WebSocket _downloaderPromise.TrySetResultAsync(true); } + /// + /// Gets a collection of users in this guild that the name or nickname starts with the + /// provided at . + /// + /// + /// The can not be higher than . + /// + /// The partial name or nickname to search. + /// The maximum number of users to be gotten. + /// 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 that the name or nickname starts with the provided at . + /// + public Task> SearchUsersAsync(string query, int limit = DiscordConfig.MaxUsersPerBatch, RequestOptions options = null) + => GuildHelper.SearchUsersAsync(this, Discord, query, limit, options); + //Audit logs /// /// Gets the specified number of audit log entries for this guild. @@ -1169,8 +1221,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) @@ -1184,6 +1241,14 @@ namespace Discord.WebSocket /// Task IGuild.GetOwnerAsync(CacheMode mode, RequestOptions options) => Task.FromResult(Owner); + /// + async Task> IGuild.SearchUsersAsync(string query, int limit, CacheMode mode, RequestOptions options) + { + if (mode == CacheMode.AllowDownload) + return await SearchUsersAsync(query, limit, options).ConfigureAwait(false); + else + return ImmutableArray.Create(); + } /// async Task> IGuild.GetAuditLogsAsync(int limit, CacheMode cacheMode, RequestOptions options, diff --git a/Discord.NET/Discord.Net.WebSocket/Entities/Messages/MessageCache.cs b/Discord.NET/Discord.Net.WebSocket/Entities/Messages/MessageCache.cs index 24e46df..6baf568 100644 --- a/Discord.NET/Discord.Net.WebSocket/Entities/Messages/MessageCache.cs +++ b/Discord.NET/Discord.Net.WebSocket/Entities/Messages/MessageCache.cs @@ -56,11 +56,23 @@ namespace Discord.WebSocket cachedMessageIds = _orderedMessages; else if (dir == Direction.Before) cachedMessageIds = _orderedMessages.Where(x => x < fromMessageId.Value); - else + else if (dir == Direction.After) cachedMessageIds = _orderedMessages.Where(x => x > fromMessageId.Value); + else //Direction.Around + { + if (!_messages.TryGetValue(fromMessageId.Value, out SocketMessage msg)) + return ImmutableArray.Empty; + int around = limit / 2; + var before = GetMany(fromMessageId, Direction.Before, around); + var after = GetMany(fromMessageId, Direction.After, around).Reverse(); + + return after.Concat(new SocketMessage[] { msg }).Concat(before).ToImmutableArray(); + } if (dir == Direction.Before) cachedMessageIds = cachedMessageIds.Reverse(); + if (dir == Direction.Around) //Only happens if fromMessageId is null, should only get "around" and itself (+1) + limit = limit / 2 + 1; return cachedMessageIds .Select(x => diff --git a/Discord.NET/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs b/Discord.NET/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs index 7900b7e..614bd04 100644 --- a/Discord.NET/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs +++ b/Discord.NET/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs @@ -46,6 +46,8 @@ namespace Discord.WebSocket public virtual bool IsSuppressed => false; /// public virtual DateTimeOffset? EditedTimestamp => null; + /// + public virtual bool MentionedEveryone => false; /// public MessageActivity Activity { get; private set; } @@ -140,7 +142,7 @@ namespace Discord.WebSocket Activity = new MessageActivity() { Type = model.Activity.Value.Type.Value, - PartyId = model.Activity.Value.PartyId.Value + PartyId = model.Activity.Value.PartyId.GetValueOrDefault() }; } @@ -200,6 +202,10 @@ namespace Discord.WebSocket { _reactions.Clear(); } + internal void RemoveReactionsForEmote(IEmote emote) + { + _reactions.RemoveAll(x => x.Emote.Equals(emote)); + } /// public Task AddReactionAsync(IEmote emote, RequestOptions options = null) @@ -214,6 +220,9 @@ namespace Discord.WebSocket public Task RemoveAllReactionsAsync(RequestOptions options = null) => MessageHelper.RemoveAllReactionsAsync(this, Discord, options); /// + public Task RemoveAllReactionsForEmoteAsync(IEmote emote, RequestOptions options = null) + => MessageHelper.RemoveAllReactionsForEmoteAsync(this, emote, Discord, options); + /// public IAsyncEnumerable> GetReactionUsersAsync(IEmote emote, int limit, RequestOptions options = null) => MessageHelper.GetReactionUsersAsync(this, emote, limit, Discord, options); } diff --git a/Discord.NET/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs b/Discord.NET/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs index b26dfe5..51b0c20 100644 --- a/Discord.NET/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs +++ b/Discord.NET/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs @@ -20,7 +20,9 @@ namespace Discord.WebSocket private ImmutableArray _attachments = ImmutableArray.Create(); private ImmutableArray _embeds = ImmutableArray.Create(); private ImmutableArray _tags = ImmutableArray.Create(); - + private ImmutableArray _roleMentions = ImmutableArray.Create(); + private ImmutableArray _userMentions = ImmutableArray.Create(); + /// public override bool IsTTS => _isTTS; /// @@ -30,6 +32,8 @@ namespace Discord.WebSocket /// public override DateTimeOffset? EditedTimestamp => DateTimeUtils.FromTicks(_editedTimestampTicks); /// + public override bool MentionedEveryone => _isMentioningEveryone; + /// public override IReadOnlyCollection Attachments => _attachments; /// public override IReadOnlyCollection Embeds => _embeds; @@ -38,9 +42,9 @@ namespace Discord.WebSocket /// public override IReadOnlyCollection MentionedChannels => MessageHelper.FilterTagsByValue(TagType.ChannelMention, _tags); /// - public override IReadOnlyCollection MentionedRoles => MessageHelper.FilterTagsByValue(TagType.RoleMention, _tags); + public override IReadOnlyCollection MentionedRoles => _roleMentions; /// - public override IReadOnlyCollection MentionedUsers => MessageHelper.FilterTagsByValue(TagType.UserMention, _tags); + public override IReadOnlyCollection MentionedUsers => _userMentions; internal SocketUserMessage(DiscordSocketClient discord, ulong id, ISocketMessageChannel channel, SocketUser author, MessageSource source) : base(discord, id, channel, author, source) @@ -57,6 +61,8 @@ namespace Discord.WebSocket { base.Update(state, model); + SocketGuild guild = (Channel as SocketGuildChannel)?.Guild; + if (model.IsTextToSpeech.IsSpecified) _isTTS = model.IsTextToSpeech.Value; if (model.Pinned.IsSpecified) @@ -69,6 +75,8 @@ namespace Discord.WebSocket { _isSuppressed = model.Flags.Value.HasFlag(API.MessageFlags.Suppressed); } + if (model.RoleMentions.IsSpecified) + _roleMentions = model.RoleMentions.Value.Select(x => guild.GetRole(x)).ToImmutableArray(); if (model.Attachments.IsSpecified) { @@ -98,32 +106,33 @@ namespace Discord.WebSocket _embeds = ImmutableArray.Create(); } - IReadOnlyCollection mentions = ImmutableArray.Create(); //Is passed to ParseTags to get real mention collection if (model.UserMentions.IsSpecified) { var value = model.UserMentions.Value; if (value.Length > 0) { - var newMentions = ImmutableArray.CreateBuilder(value.Length); + var newMentions = ImmutableArray.CreateBuilder(value.Length); for (int i = 0; i < value.Length; i++) { var val = value[i]; - if (val.Object != null) + var guildUser = guild.GetUser(val.Id); + if (guildUser != null) + newMentions.Add(guildUser); + else if (val.Object != null) newMentions.Add(SocketUnknownUser.Create(Discord, state, val.Object)); } - mentions = newMentions.ToImmutable(); + _userMentions = newMentions.ToImmutable(); } } if (model.Content.IsSpecified) { var text = model.Content.Value; - var guild = (Channel as SocketGuildChannel)?.Guild; - _tags = MessageHelper.ParseTags(text, Channel, guild, mentions); + _tags = MessageHelper.ParseTags(text, Channel, guild, _userMentions); 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 +156,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; } diff --git a/Discord.NET/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs b/Discord.NET/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs index e5dbfa0..a506a5d 100644 --- a/Discord.NET/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs +++ b/Discord.NET/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs @@ -154,6 +154,8 @@ namespace Discord.WebSocket Nickname = model.Nick.Value; if (model.Roles.IsSpecified) UpdateRoles(model.Roles.Value); + if (model.PremiumSince.IsSpecified) + _premiumSinceTicks = model.PremiumSince.Value?.UtcTicks; } private void UpdateRoles(ulong[] roleIds) { diff --git a/Discord.NET/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs b/Discord.NET/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs index 52f1113..407e144 100644 --- a/Discord.NET/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs +++ b/Discord.NET/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs @@ -18,16 +18,20 @@ namespace Discord.WebSocket public IActivity Activity { get; } /// public IImmutableSet ActiveClients { get; } - internal SocketPresence(UserStatus status, IActivity activity, IImmutableSet activeClients) + /// + public IImmutableList Activities { get; } + internal SocketPresence(UserStatus status, IActivity activity, IImmutableSet activeClients, IImmutableList activities) { Status = status; - Activity= activity; - ActiveClients = activeClients; + Activity = activity; + ActiveClients = activeClients ?? ImmutableHashSet.Empty; + Activities = activities ?? ImmutableList.Empty; } internal static SocketPresence Create(Model model) { var clients = ConvertClientTypesDict(model.ClientStatus.GetValueOrDefault()); - return new SocketPresence(model.Status, model.Game?.ToEntity(), clients); + var activities = ConvertActivitiesList(model.Activities); + return new SocketPresence(model.Status, model.Game?.ToEntity(), clients, activities); } /// /// Creates a new containing all of the client types @@ -53,6 +57,25 @@ namespace Discord.WebSocket } return set.ToImmutableHashSet(); } + /// + /// Creates a new containing all the activities + /// that a user has from the data supplied in the Presence update frame. + /// + /// + /// A list of . + /// + /// + /// A list of all that this user currently has available. + /// + private static IImmutableList ConvertActivitiesList(IList activities) + { + if (activities == null || activities.Count == 0) + return ImmutableList.Empty; + var list = new List(); + foreach (var activity in activities) + list.Add(activity.ToEntity()); + return list.ToImmutableList(); + } /// /// Gets the status of the user. diff --git a/Discord.NET/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs b/Discord.NET/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs index 840a1c3..dd2e747 100644 --- a/Discord.NET/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs +++ b/Discord.NET/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs @@ -25,7 +25,7 @@ namespace Discord.WebSocket /// public override bool IsWebhook => false; /// - internal override SocketPresence Presence { get { return new SocketPresence(UserStatus.Offline, null, null); } set { } } + internal override SocketPresence Presence { get { return new SocketPresence(UserStatus.Offline, null, null, null); } set { } } /// /// This field is not supported for an unknown user. internal override SocketGlobalUser GlobalUser => diff --git a/Discord.NET/Discord.Net.WebSocket/Entities/Users/SocketUser.cs b/Discord.NET/Discord.Net.WebSocket/Entities/Users/SocketUser.cs index 09c4165..7d3c2d2 100644 --- a/Discord.NET/Discord.Net.WebSocket/Entities/Users/SocketUser.cs +++ b/Discord.NET/Discord.Net.WebSocket/Entities/Users/SocketUser.cs @@ -41,11 +41,16 @@ namespace Discord.WebSocket public UserStatus Status => Presence.Status; /// public IImmutableSet ActiveClients => Presence.ActiveClients ?? ImmutableHashSet.Empty; + /// + public IImmutableList Activities => Presence.Activities ?? ImmutableList.Empty; /// /// 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) diff --git a/Discord.NET/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs b/Discord.NET/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs index 8819fe1..d400e1a 100644 --- a/Discord.NET/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs +++ b/Discord.NET/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs @@ -30,7 +30,7 @@ namespace Discord.WebSocket /// public override bool IsWebhook => true; /// - internal override SocketPresence Presence { get { return new SocketPresence(UserStatus.Offline, null, null); } set { } } + internal override SocketPresence Presence { get { return new SocketPresence(UserStatus.Offline, null, null, null); } set { } } internal override SocketGlobalUser GlobalUser => throw new NotSupportedException(); diff --git a/Discord.NET/Discord.Net.WebSocket/GatewayReconnectException.cs b/Discord.NET/Discord.Net.WebSocket/GatewayReconnectException.cs new file mode 100644 index 0000000..1a80245 --- /dev/null +++ b/Discord.NET/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/Discord.NET/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs b/Discord.NET/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs index 36a6fea..4723ae5 100644 --- a/Discord.NET/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs +++ b/Discord.NET/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 { diff --git a/Discord.NET/Discord.Net.Webhook/DiscordWebhookClient.cs b/Discord.NET/Discord.Net.Webhook/DiscordWebhookClient.cs index 542ec79..9c9bfe9 100644 --- a/Discord.NET/Discord.Net.Webhook/DiscordWebhookClient.cs +++ b/Discord.NET/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 | RegexOptions.CultureInvariant); /// Creates a new Webhook Discord client. public DiscordWebhookClient(ulong webhookId, string webhookToken, DiscordRestConfig config) @@ -77,30 +77,32 @@ namespace Discord.Webhook ApiClient.RequestQueue.RateLimitTriggered += async (id, info) => { if (info == null) - await _restLogger.VerboseAsync($"Preemptive Rate limit triggered: {id ?? "null"}").ConfigureAwait(false); + await _restLogger.VerboseAsync($"Preemptive Rate limit triggered: {id?.ToString() ?? "null"}").ConfigureAwait(false); else - await _restLogger.WarningAsync($"Rate limit triggered: {id ?? "null"}").ConfigureAwait(false); + await _restLogger.WarningAsync($"Rate limit triggered: {id?.ToString() ?? "null"}").ConfigureAwait(false); }; ApiClient.SentRequest += async (method, endpoint, millis) => await _restLogger.VerboseAsync($"{method} {endpoint}: {millis} ms").ConfigureAwait(false); } private static API.DiscordRestApiClient CreateApiClient(DiscordRestConfig config) => new API.DiscordRestApiClient(config.RestClientProvider, DiscordRestConfig.UserAgent); - /// 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) - => WebhookClientHelper.SendMessageAsync(this, text, isTTS, embeds, username, avatarUrl, options); + string username = null, string avatarUrl = null, RequestOptions options = null, AllowedMentions allowedMentions = null) + => WebhookClientHelper.SendMessageAsync(this, text, isTTS, embeds, username, avatarUrl, allowedMentions, options); /// Sends a message to the channel for this webhook with an attachment. /// Returns the ID of the created message. public Task SendFileAsync(string filePath, string text, bool isTTS = false, - IEnumerable embeds = null, string username = null, string avatarUrl = null, RequestOptions options = null, bool isSpoiler = false) - => WebhookClientHelper.SendFileAsync(this, filePath, text, isTTS, embeds, username, avatarUrl, options, isSpoiler); + IEnumerable embeds = null, string username = null, string avatarUrl = null, + RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null) + => WebhookClientHelper.SendFileAsync(this, filePath, text, isTTS, embeds, username, avatarUrl, allowedMentions, options, isSpoiler); /// Sends a message to the channel for this webhook with an attachment. /// Returns the ID of the created message. public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, - IEnumerable embeds = null, string username = null, string avatarUrl = null, RequestOptions options = null, bool isSpoiler = false) - => WebhookClientHelper.SendFileAsync(this, stream, filename, text, isTTS, embeds, username, avatarUrl, options, isSpoiler); + IEnumerable embeds = null, string username = null, string avatarUrl = null, + RequestOptions options = null, bool isSpoiler = false, AllowedMentions allowedMentions = null) + => WebhookClientHelper.SendFileAsync(this, stream, filename, text, isTTS, embeds, username, avatarUrl, allowedMentions, options, isSpoiler); /// Modifies the properties of this webhook. public Task ModifyWebhookAsync(Action func, RequestOptions options = null) @@ -132,13 +134,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(); diff --git a/Discord.NET/Discord.Net.Webhook/WebhookClientHelper.cs b/Discord.NET/Discord.Net.Webhook/WebhookClientHelper.cs index 311d58b..4bc2eac 100644 --- a/Discord.NET/Discord.Net.Webhook/WebhookClientHelper.cs +++ b/Discord.NET/Discord.Net.Webhook/WebhookClientHelper.cs @@ -21,7 +21,7 @@ namespace Discord.Webhook return RestInternalWebhook.Create(client, model); } public static async Task SendMessageAsync(DiscordWebhookClient client, - string text, bool isTTS, IEnumerable embeds, string username, string avatarUrl, RequestOptions options) + string text, bool isTTS, IEnumerable embeds, string username, string avatarUrl, AllowedMentions allowedMentions, RequestOptions options) { var args = new CreateWebhookMessageParams(text) { IsTTS = isTTS }; if (embeds != null) @@ -30,19 +30,21 @@ namespace Discord.Webhook args.Username = username; if (avatarUrl != null) args.AvatarUrl = avatarUrl; + if (allowedMentions != null) + args.AllowedMentions = allowedMentions.ToModel(); var model = await client.ApiClient.CreateWebhookMessageAsync(client.Webhook.Id, args, options: options).ConfigureAwait(false); return model.Id; } public static async Task SendFileAsync(DiscordWebhookClient client, string filePath, string text, bool isTTS, - IEnumerable embeds, string username, string avatarUrl, RequestOptions options, bool isSpoiler) + IEnumerable embeds, string username, string avatarUrl, AllowedMentions allowedMentions, RequestOptions options, bool isSpoiler) { string filename = Path.GetFileName(filePath); using (var file = File.OpenRead(filePath)) - return await SendFileAsync(client, file, filename, text, isTTS, embeds, username, avatarUrl, options, isSpoiler).ConfigureAwait(false); + return await SendFileAsync(client, file, filename, text, isTTS, embeds, username, avatarUrl, allowedMentions, options, isSpoiler).ConfigureAwait(false); } public static async Task SendFileAsync(DiscordWebhookClient client, Stream stream, string filename, string text, bool isTTS, - IEnumerable embeds, string username, string avatarUrl, RequestOptions options, bool isSpoiler) + IEnumerable embeds, string username, string avatarUrl, AllowedMentions allowedMentions, RequestOptions options, bool isSpoiler) { var args = new UploadWebhookFileParams(stream) { Filename = filename, Content = text, IsTTS = isTTS, IsSpoiler = isSpoiler }; if (username != null) @@ -51,6 +53,8 @@ namespace Discord.Webhook args.AvatarUrl = avatarUrl; if (embeds != null) args.Embeds = embeds.Select(x => x.ToModel()).ToArray(); + if(allowedMentions != null) + args.AllowedMentions = allowedMentions.ToModel(); var msg = await client.ApiClient.UploadWebhookFileAsync(client.Webhook.Id, args, options).ConfigureAwait(false); return msg.Id; }