diff --git a/CHANGELOG.md b/CHANGELOG.md index 5988f90c3..21e37b295 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # Changelog +## [2.4.0] - 2021-05-22 +### Added +- #1726 Add stickers (91a9063) +- #1753 Webhook message edit & delete functionality (f67cd8e) +- #1757 Add ability to add/remove roles by id (4c9910c) +- #1781 Add GetEmotesAsync to IGuild (df23d57) +- #1801 Add missing property to MESSAGE_REACTION_ADD event (0715d7d) +- #1828 Add methods to interact with reactions without a message object (5b244f2) +- #1830 Add ModifyMessageAsync to IMessageChannel (365a848) +- #1844 Add Discord Certified Moderator user flag (4b8d444) + +### Fixed +- #1486 Add type reader when entity type reader exists (c46daaa) +- #1835 Cached message emoji cleanup at MESSAGE_REACTION_REMOVE_EMOJI (8afef82) + +### Misc +- #1778 Remove URI check from EmbedBuilder (25b04c4) +- #1800 Fix spelling in SnowflakeUtils.FromSnowflake (6aff419) + ## [2.3.1] - 2021-03-10 ### Fixed - #1761 Deadlock in DiscordShardedClient when Ready is never received (73e5cc2) diff --git a/Discord.Net.targets b/Discord.Net.targets index 82e0d21fa..febd921d1 100644 --- a/Discord.Net.targets +++ b/Discord.Net.targets @@ -1,7 +1,7 @@ - 2.3.1 - + 3.0.0 + dev latest Discord.Net Contributors discord;discordapp diff --git a/README.md b/README.md index 34a633f72..32e1515af 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,6 @@ An unofficial .NET API Wrapper for the Discord client (https://discord.com). ## Documentation -- [Stable](https://discord.foxbot.me/) - - Hosted by @foxbot - [Nightly](https://docs.stillu.cc/) - [Latest CI repo](https://github.com/discord-net/docs-static) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 2fe5abfe8..9aa0e5788 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -39,4 +39,3 @@ jobs: steps: - template: azure/build.yml - template: azure/deploy.yml - - template: azure/docs.yml diff --git a/docs/_overwrites/Common/EmbedBuilder.Overwrites.md b/docs/_overwrites/Common/EmbedBuilder.Overwrites.md index 409a78e94..85c292dd2 100644 --- a/docs/_overwrites/Common/EmbedBuilder.Overwrites.md +++ b/docs/_overwrites/Common/EmbedBuilder.Overwrites.md @@ -28,7 +28,7 @@ public async Task SendRichEmbedAsync() var embed = new EmbedBuilder { // Embed property can be set within object initializer - Title = "Hello world!" + Title = "Hello world!", Description = "I am a description set by initializer." }; // Or with methods diff --git a/docs/guides/commands/intro.md b/docs/guides/commands/intro.md index abe7065c1..14341a32b 100644 --- a/docs/guides/commands/intro.md +++ b/docs/guides/commands/intro.md @@ -134,7 +134,7 @@ If, for whatever reason, you have two commands which are ambiguous to each other, you may use the @Discord.Commands.PriorityAttribute to specify which should be tested before the other. -The `Priority` attributes are sorted in ascending order; the higher +The `Priority` attributes are sorted in descending order; the higher priority will be called first. ### Command Context diff --git a/docs/guides/getting_started/first-bot.md b/docs/guides/getting_started/first-bot.md index 150466be1..e1af20d30 100644 --- a/docs/guides/getting_started/first-bot.md +++ b/docs/guides/getting_started/first-bot.md @@ -80,15 +80,11 @@ recommended for these operations to be awaited in a properly established async context whenever possible. To establish an async context, we will be creating an async main method -in your console application, and rewriting the static main method to -invoke the new async main. +in your console application. [!code-csharp[Async Context](samples/first-bot/async-context.cs)] -As a result of this, your program will now start and immediately -jump into an async context. This allows us to create a connection -to Discord later on without having to worry about setting up the -correct async implementation. +As a result of this, your program will now start into an async context. > [!WARNING] > If your application throws any exceptions within an async context, diff --git a/docs/guides/getting_started/samples/first-bot/async-context.cs b/docs/guides/getting_started/samples/first-bot/async-context.cs index 3c98c9e46..98a3cea15 100644 --- a/docs/guides/getting_started/samples/first-bot/async-context.cs +++ b/docs/guides/getting_started/samples/first-bot/async-context.cs @@ -1,7 +1,6 @@ public class Program { - public static void Main(string[] args) - => new Program().MainAsync().GetAwaiter().GetResult(); + public static Task Main(string[] args) => new Program().MainAsync(); public async Task MainAsync() { diff --git a/docs/guides/getting_started/samples/first-bot/complete.cs b/docs/guides/getting_started/samples/first-bot/complete.cs index 871641e23..542056435 100644 --- a/docs/guides/getting_started/samples/first-bot/complete.cs +++ b/docs/guides/getting_started/samples/first-bot/complete.cs @@ -2,8 +2,7 @@ public class Program { private DiscordSocketClient _client; - public static void Main(string[] args) - => new Program().MainAsync().GetAwaiter().GetResult(); + public static Task Main(string[] args) => new Program().MainAsync(); public async Task MainAsync() { diff --git a/docs/guides/getting_started/samples/first-bot/structure.cs b/docs/guides/getting_started/samples/first-bot/structure.cs index 5165e2fdb..4e64b1732 100644 --- a/docs/guides/getting_started/samples/first-bot/structure.cs +++ b/docs/guides/getting_started/samples/first-bot/structure.cs @@ -10,11 +10,11 @@ using Discord.WebSocket; class Program { // Program entry point - static void Main(string[] args) + static Task Main(string[] args) { // Call the Program constructor, followed by the // MainAsync method and wait until it finishes (which should be never). - new Program().MainAsync().GetAwaiter().GetResult(); + return new Program().MainAsync(); } private readonly DiscordSocketClient _client; diff --git a/samples/03_sharded_client/Modules/PublicModule.cs b/samples/03_sharded_client/Modules/PublicModule.cs index 60e57563a..fad2ba98c 100644 --- a/samples/03_sharded_client/Modules/PublicModule.cs +++ b/samples/03_sharded_client/Modules/PublicModule.cs @@ -9,7 +9,7 @@ namespace _03_sharded_client.Modules [Command("info")] public async Task InfoAsync() { - var msg = $@"Hi {Context.User}! There are currently {Context.Client.Shards} shards! + var msg = $@"Hi {Context.User}! There are currently {Context.Client.Shards.Count} shards! This guild is being served by shard number {Context.Client.GetShardFor(Context.Guild).ShardId}"; await ReplyAsync(msg); } diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index 1d4b0e15a..8659b0130 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -408,7 +408,7 @@ namespace Discord.Commands var typeInfo = type.GetTypeInfo(); if (typeInfo.IsEnum) return true; - return _entityTypeReaders.Any(x => type == x.EntityType || typeInfo.ImplementedInterfaces.Contains(x.TypeReaderType)); + return _entityTypeReaders.Any(x => type == x.EntityType || typeInfo.ImplementedInterfaces.Contains(x.EntityType)); } internal void AddNullableTypeReader(Type valueType, TypeReader valueTypeReader) { @@ -511,7 +511,7 @@ namespace Discord.Commands await _commandExecutedEvent.InvokeAsync(Optional.Create(), context, searchResult).ConfigureAwait(false); return searchResult; } - + var commands = searchResult.Commands; var preconditionResults = new Dictionary(); diff --git a/src/Discord.Net.Core/DiscordConfig.cs b/src/Discord.Net.Core/DiscordConfig.cs index 429ad7b0c..da8525644 100644 --- a/src/Discord.Net.Core/DiscordConfig.cs +++ b/src/Discord.Net.Core/DiscordConfig.cs @@ -16,7 +16,7 @@ namespace Discord /// Discord API documentation /// . /// - public const int APIVersion = 6; + public const int APIVersion = 9; /// /// Returns the Voice API version Discord.Net uses. /// @@ -43,7 +43,7 @@ namespace Discord /// /// The user agent used in each Discord.Net request. /// - public static string UserAgent { get; } = $"DiscordBot (https://github.com/RogueException/Discord.Net, v{Version})"; + public static string UserAgent { get; } = $"DiscordBot (https://github.com/discord-net/Discord.Net, v{Version})"; /// /// Returns the base Discord API URL. /// @@ -141,18 +141,6 @@ namespace Discord /// internal bool DisplayInitialLog { get; set; } = true; - /// - /// Gets or sets the level of precision of the rate limit reset response. - /// - /// - /// If set to , this value will be rounded up to the - /// nearest second. - /// - /// - /// The currently set . - /// - public RateLimitPrecision RateLimitPrecision { get; set; } = RateLimitPrecision.Millisecond; - /// /// Gets or sets whether or not rate-limits should use the system clock. /// diff --git a/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs b/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs index abd20e4f0..e60eb9c13 100644 --- a/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs @@ -257,6 +257,21 @@ namespace Discord /// Task DeleteMessageAsync(IMessage message, RequestOptions options = null); + /// + /// Modifies a message. + /// + /// + /// This method modifies this message with the specified properties. To see an example of this + /// method and what properties are available, please refer to . + /// + /// The snowflake identifier of the message that would be changed. + /// A delegate containing the properties to modify the message with. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous modification operation. + /// + Task ModifyMessageAsync(ulong messageId, Action func, RequestOptions options = null); + /// /// Broadcasts the "user is typing" message to all users in this channel, lasting 10 seconds. /// diff --git a/src/Discord.Net.Core/Entities/Guilds/GuildEmbedProperties.cs b/src/Discord.Net.Core/Entities/Guilds/GuildEmbedProperties.cs deleted file mode 100644 index 34473e93c..000000000 --- a/src/Discord.Net.Core/Entities/Guilds/GuildEmbedProperties.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace Discord -{ - /// - /// Provides properties that are used to modify the widget of an with the specified changes. - /// - public class GuildEmbedProperties - { - /// - /// Sets whether the widget should be enabled. - /// - public Optional Enabled { get; set; } - /// - /// Sets the channel that the invite should place its users in, if not null. - /// - public Optional Channel { get; set; } - /// - /// Sets the channel the invite should place its users in, if not null. - /// - public Optional ChannelId { get; set; } - } -} diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs index 6283508e5..b8fd858df 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs @@ -28,13 +28,6 @@ namespace Discord /// int AFKTimeout { get; } /// - /// Gets a value that indicates whether this guild is embeddable (i.e. can use widget). - /// - /// - /// if this guild has a widget enabled; otherwise . - /// - bool IsEmbeddable { get; } - /// /// Gets a value that indicates whether this guild has the widget enabled. /// /// @@ -132,29 +125,6 @@ namespace Discord /// ulong? AFKChannelId { get; } /// - /// Gets the ID of the default channel for this guild. - /// - /// - /// This property retrieves the snowflake identifier of the first viewable text channel for this guild. - /// - /// This channel does not guarantee the user can send message to it, as it only looks for the first viewable - /// text channel. - /// - /// - /// - /// A representing the snowflake identifier of the default text channel; 0 if - /// none can be found. - /// - ulong DefaultChannelId { get; } - /// - /// Gets the ID of the widget embed channel of this guild. - /// - /// - /// A representing the snowflake identifier of the embedded channel found within the - /// widget settings of this guild; if none is set. - /// - ulong? EmbedChannelId { get; } - /// /// Gets the ID of the channel assigned to the widget of this guild. /// /// @@ -364,16 +334,6 @@ namespace Discord /// Task ModifyAsync(Action func, RequestOptions options = null); /// - /// Modifies this guild's embed channel. - /// - /// The delegate containing the properties to modify the guild widget with. - /// The options to be used when sending the request. - /// - /// A task that represents the asynchronous modification operation. - /// - [Obsolete("This endpoint is deprecated, use ModifyWidgetAsync instead.")] - Task ModifyEmbedAsync(Action func, RequestOptions options = null); - /// /// Modifies this guild's widget. /// /// The delegate containing the properties to modify the guild widget with. @@ -592,17 +552,6 @@ namespace Discord /// Task GetDefaultChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); /// - /// Gets the embed channel (i.e. the channel set in the guild's widget settings) in this guild. - /// - /// 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 the embed channel set - /// within the server's widget settings; if none is set. - /// - [Obsolete("This endpoint is deprecated, use GetWidgetChannelAsync instead.")] - Task GetEmbedChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); - /// /// Gets the widget channel (i.e. the channel set in the guild's widget settings) in this guild. /// /// The that determines whether the object should be fetched from cache. @@ -892,6 +841,15 @@ namespace Discord /// Task> GetWebhooksAsync(RequestOptions options = null); + /// + /// Gets a collection of emotes from this guild. + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection + /// of emotes found within the guild. + /// + Task> GetEmotesAsync(RequestOptions options = null); /// /// Gets a specific emote from this guild. /// diff --git a/src/Discord.Net.Core/Entities/Guilds/PermissionTarget.cs b/src/Discord.Net.Core/Entities/Guilds/PermissionTarget.cs index 3da2fb147..fb759e4c5 100644 --- a/src/Discord.Net.Core/Entities/Guilds/PermissionTarget.cs +++ b/src/Discord.Net.Core/Entities/Guilds/PermissionTarget.cs @@ -8,10 +8,10 @@ namespace Discord /// /// The target of the permission is a role. /// - Role, + Role = 0, /// /// The target of the permission is a user. /// - User + User = 1, } } diff --git a/src/Discord.Net.Core/Entities/Invites/IInviteMetadata.cs b/src/Discord.Net.Core/Entities/Invites/IInviteMetadata.cs index c64b3205b..c2580c853 100644 --- a/src/Discord.Net.Core/Entities/Invites/IInviteMetadata.cs +++ b/src/Discord.Net.Core/Entities/Invites/IInviteMetadata.cs @@ -16,14 +16,6 @@ namespace Discord /// bool IsTemporary { get; } /// - /// Gets a value that indicates whether the invite has been revoked. - /// - /// - /// true if this invite was revoked; otherwise false. - /// - [Obsolete("This property doesn't exist anymore and shouldn't be used.")] - bool IsRevoked { get; } - /// /// Gets the time (in seconds) until the invite expires. /// /// diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs b/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs index 555fd95df..89aaf5fde 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs @@ -12,7 +12,6 @@ namespace Discord { private string _title; private string _description; - private string _url; private EmbedImage? _image; private EmbedThumbnail? _thumbnail; private List _fields; @@ -28,7 +27,7 @@ namespace Discord /// /// Returns the maximum length of description allowed by Discord. /// - public const int MaxDescriptionLength = 2048; + public const int MaxDescriptionLength = 4096; /// /// Returns the maximum length of total characters allowed by Discord. /// @@ -70,26 +69,14 @@ namespace Discord /// Gets or sets the URL of an . /// Url is not a well-formed . /// The URL of the embed. - public string Url - { - get => _url; - set - { - if (!value.IsNullOrUri()) throw new ArgumentException(message: "Url must be a well-formed URI.", paramName: nameof(Url)); - _url = value; - } - } + public string Url { get; set; } /// Gets or sets the thumbnail URL of an . /// Url is not a well-formed . /// The thumbnail URL of the embed. public string ThumbnailUrl { get => _thumbnail?.Url; - set - { - if (!value.IsNullOrUri()) throw new ArgumentException(message: "Url must be a well-formed URI.", paramName: nameof(ThumbnailUrl)); - _thumbnail = new EmbedThumbnail(value, null, null, null); - } + set => _thumbnail = new EmbedThumbnail(value, null, null, null); } /// Gets or sets the image URL of an . /// Url is not a well-formed . @@ -97,11 +84,7 @@ namespace Discord public string ImageUrl { get => _image?.Url; - set - { - if (!value.IsNullOrUri()) throw new ArgumentException(message: "Url must be a well-formed URI.", paramName: nameof(ImageUrl)); - _image = new EmbedImage(value, null, null, null); - } + set => _image = new EmbedImage(value, null, null, null); } /// Gets or sets the list of of an . @@ -553,8 +536,6 @@ namespace Discord public class EmbedAuthorBuilder { private string _name; - private string _url; - private string _iconUrl; /// /// Gets the maximum author name length allowed by Discord. /// @@ -585,15 +566,7 @@ namespace Discord /// /// The URL of the author field. /// - public string Url - { - get => _url; - set - { - if (!value.IsNullOrUri()) throw new ArgumentException(message: "Url must be a well-formed URI.", paramName: nameof(Url)); - _url = value; - } - } + public string Url { get; set; } /// /// Gets or sets the icon URL of the author field. /// @@ -601,15 +574,7 @@ namespace Discord /// /// The icon URL of the author field. /// - public string IconUrl - { - get => _iconUrl; - set - { - if (!value.IsNullOrUri()) throw new ArgumentException(message: "Url must be a well-formed URI.", paramName: nameof(IconUrl)); - _iconUrl = value; - } - } + public string IconUrl { get; set; } /// /// Sets the name of the author field. @@ -671,7 +636,6 @@ namespace Discord public class EmbedFooterBuilder { private string _text; - private string _iconUrl; /// /// Gets the maximum footer length allowed by Discord. @@ -703,15 +667,7 @@ namespace Discord /// /// The icon URL of the footer field. /// - public string IconUrl - { - get => _iconUrl; - set - { - if (!value.IsNullOrUri()) throw new ArgumentException(message: "Url must be a well-formed URI.", paramName: nameof(IconUrl)); - _iconUrl = value; - } - } + public string IconUrl { get; set; } /// /// Sets the name of the footer field. diff --git a/src/Discord.Net.Core/Entities/Messages/IMessage.cs b/src/Discord.Net.Core/Entities/Messages/IMessage.cs index 80b1ffa68..b5023eb59 100644 --- a/src/Discord.Net.Core/Entities/Messages/IMessage.cs +++ b/src/Discord.Net.Core/Entities/Messages/IMessage.cs @@ -10,7 +10,7 @@ namespace Discord public interface IMessage : ISnowflakeEntity, IDeletable { /// - /// Gets the type of this system message. + /// Gets the type of this message. /// MessageType Type { get; } /// @@ -164,6 +164,14 @@ namespace Discord /// IReadOnlyDictionary Reactions { get; } + /// + /// Gets all stickers included in this message. + /// + /// + /// A read-only collection of sticker objects. + /// + IReadOnlyCollection Stickers { get; } + /// /// Gets the flags related to this message. /// diff --git a/src/Discord.Net.Core/Entities/Messages/ISticker.cs b/src/Discord.Net.Core/Entities/Messages/ISticker.cs new file mode 100644 index 000000000..e7e4405b6 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Messages/ISticker.cs @@ -0,0 +1,67 @@ +using System.Collections.Generic; + +namespace Discord +{ + /// + /// Represents a discord sticker. + /// + public interface ISticker + { + /// + /// Gets the ID of this sticker. + /// + /// + /// A snowflake ID associated with this sticker. + /// + ulong Id { get; } + /// + /// Gets the ID of the pack of this sticker. + /// + /// + /// A snowflake ID associated with the pack of this sticker. + /// + ulong PackId { get; } + /// + /// Gets the name of this sticker. + /// + /// + /// A with the name of this sticker. + /// + string Name { get; } + /// + /// Gets the description of this sticker. + /// + /// + /// A with the description of this sticker. + /// + string Description { get; } + /// + /// Gets the list of tags of this sticker. + /// + /// + /// A read-only list with the tags of this sticker. + /// + IReadOnlyCollection Tags { get; } + /// + /// Gets the asset hash of this sticker. + /// + /// + /// A with the asset hash of this sticker. + /// + string Asset { get; } + /// + /// Gets the preview asset hash of this sticker. + /// + /// + /// A with the preview asset hash of this sticker. + /// + string PreviewAsset { get; } + /// + /// Gets the format type of this sticker. + /// + /// + /// A with the format type of this sticker. + /// + StickerFormatType FormatType { get; } + } +} diff --git a/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs b/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs index 1589e2ae5..c2d0e13bc 100644 --- a/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs +++ b/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs @@ -36,18 +36,6 @@ namespace Discord /// Task ModifyAsync(Action func, RequestOptions options = null); /// - /// Modifies the suppression of this message. - /// - /// - /// This method modifies whether or not embeds in this message are suppressed (hidden). - /// - /// Whether or not embeds in this message should be suppressed. - /// The options to be used when sending the request. - /// - /// A task that represents the asynchronous modification operation. - /// - Task ModifySuppressionAsync(bool suppressEmbeds, RequestOptions options = null); - /// /// Adds this message to its channel's pinned messages. /// /// The options to be used when sending the request. diff --git a/src/Discord.Net.Core/Entities/Messages/MessageType.cs b/src/Discord.Net.Core/Entities/Messages/MessageType.cs index ad1f3a3cd..bfe763cad 100644 --- a/src/Discord.Net.Core/Entities/Messages/MessageType.cs +++ b/src/Discord.Net.Core/Entities/Messages/MessageType.cs @@ -60,9 +60,6 @@ namespace Discord /// /// The message is an inline reply. /// - /// - /// Only available in API v8. - /// Reply = 19, } } diff --git a/src/Discord.Net.Core/Entities/Messages/SticketFormatType.cs b/src/Discord.Net.Core/Entities/Messages/SticketFormatType.cs new file mode 100644 index 000000000..d24a38534 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Messages/SticketFormatType.cs @@ -0,0 +1,15 @@ +namespace Discord +{ + /// Defines the types of formats for stickers. + public enum StickerFormatType + { + /// Default value for a sticker format type. + None = 0, + /// The sticker format type is png. + Png = 1, + /// The sticker format type is apng. + Apng = 2, + /// The sticker format type is lottie. + Lottie = 3, + } +} diff --git a/src/Discord.Net.Core/Entities/Permissions/ChannelPermission.cs b/src/Discord.Net.Core/Entities/Permissions/ChannelPermission.cs index e1f78373e..bf08887bd 100644 --- a/src/Discord.Net.Core/Entities/Permissions/ChannelPermission.cs +++ b/src/Discord.Net.Core/Entities/Permissions/ChannelPermission.cs @@ -22,11 +22,6 @@ namespace Discord /// AddReactions = 0x00_00_00_40, /// - /// Allows for reading of messages. This flag is obsolete, use instead. - /// - [Obsolete("Use ViewChannel instead.")] - ReadMessages = ViewChannel, - /// /// Allows guild members to view a channel, which includes reading messages in text channels. /// ViewChannel = 0x00_00_04_00, diff --git a/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs b/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs index ed675b5f3..d774cc51d 100644 --- a/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs +++ b/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs @@ -45,9 +45,6 @@ namespace Discord /// If true, a user may add reactions. public bool AddReactions => Permissions.GetValue(RawValue, ChannelPermission.AddReactions); - /// If true, a user may join channels. - [Obsolete("Use ViewChannel instead.")] - public bool ReadMessages => ViewChannel; /// If true, a user may view channels. public bool ViewChannel => Permissions.GetValue(RawValue, ChannelPermission.ViewChannel); diff --git a/src/Discord.Net.Core/Entities/Permissions/GuildPermission.cs b/src/Discord.Net.Core/Entities/Permissions/GuildPermission.cs index 645b67489..31bd6164a 100644 --- a/src/Discord.Net.Core/Entities/Permissions/GuildPermission.cs +++ b/src/Discord.Net.Core/Entities/Permissions/GuildPermission.cs @@ -65,8 +65,6 @@ namespace Discord /// Allows for viewing of audit logs. /// ViewAuditLog = 0x00_00_00_80, - [Obsolete("Use ViewChannel instead.")] - ReadMessages = ViewChannel, ViewChannel = 0x00_00_04_00, SendMessages = 0x00_00_08_00, /// diff --git a/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs b/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs index ba6757fc6..b03c0e1a8 100644 --- a/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs +++ b/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs @@ -37,9 +37,6 @@ namespace Discord /// If true, a user may view the guild insights. public bool ViewGuildInsights => Permissions.GetValue(RawValue, GuildPermission.ViewGuildInsights); - /// If True, a user may join channels. - [Obsolete("Use ViewChannel instead.")] - public bool ReadMessages => ViewChannel; /// If True, a user may view channels. public bool ViewChannel => Permissions.GetValue(RawValue, GuildPermission.ViewChannel); /// If True, a user may send messages. @@ -90,6 +87,9 @@ namespace Discord /// Creates a new with the provided packed value. public GuildPermissions(ulong rawValue) { RawValue = rawValue; } + /// Creates a new with the provided packed value after converting to ulong. + public GuildPermissions(string rawValue) { RawValue = ulong.Parse(rawValue); } + private GuildPermissions(ulong initialValue, bool? createInstantInvite = null, bool? kickMembers = null, diff --git a/src/Discord.Net.Core/Entities/Permissions/OverwritePermissions.cs b/src/Discord.Net.Core/Entities/Permissions/OverwritePermissions.cs index 7876d49ff..4f144c74b 100644 --- a/src/Discord.Net.Core/Entities/Permissions/OverwritePermissions.cs +++ b/src/Discord.Net.Core/Entities/Permissions/OverwritePermissions.cs @@ -43,9 +43,6 @@ namespace Discord /// If Allowed, a user may add reactions. public PermValue AddReactions => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.AddReactions); /// If Allowed, a user may join channels. - [Obsolete("Use ViewChannel instead.")] - public PermValue ReadMessages => ViewChannel; - /// If Allowed, a user may join channels. public PermValue ViewChannel => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.ViewChannel); /// If Allowed, a user may send messages. public PermValue SendMessages => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.SendMessages); @@ -93,6 +90,13 @@ namespace Discord DenyValue = denyValue; } + /// Creates a new OverwritePermissions with the provided allow and deny packed values after converting to ulong. + public OverwritePermissions(string allowValue, string denyValue) + { + AllowValue = ulong.Parse(allowValue); + DenyValue = ulong.Parse(denyValue); + } + private OverwritePermissions(ulong allowValue, ulong denyValue, PermValue? createInstantInvite = null, PermValue? manageChannel = null, diff --git a/src/Discord.Net.Core/Entities/Teams/ITeam.cs b/src/Discord.Net.Core/Entities/Teams/ITeam.cs index 5ef3e4253..b6e3d987b 100644 --- a/src/Discord.Net.Core/Entities/Teams/ITeam.cs +++ b/src/Discord.Net.Core/Entities/Teams/ITeam.cs @@ -20,6 +20,10 @@ namespace Discord /// IReadOnlyList TeamMembers { get; } /// + /// Gets the name of this team. + /// + string Name { get; } + /// /// Gets the user identifier that owns this team. /// ulong OwnerUserId { get; } diff --git a/src/Discord.Net.Core/Entities/Users/IGuildUser.cs b/src/Discord.Net.Core/Entities/Users/IGuildUser.cs index 582e9e7a7..492cb9566 100644 --- a/src/Discord.Net.Core/Entities/Users/IGuildUser.cs +++ b/src/Discord.Net.Core/Entities/Users/IGuildUser.cs @@ -113,7 +113,15 @@ namespace Discord /// A task that represents the asynchronous modification operation. /// Task ModifyAsync(Action func, RequestOptions options = null); - + /// + /// Adds the specified role to this user in the guild. + /// + /// The role to be added to the user. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous role addition operation. + /// + Task AddRoleAsync(ulong roleId, RequestOptions options = null); /// /// Adds the specified role to this user in the guild. /// @@ -124,6 +132,15 @@ namespace Discord /// Task AddRoleAsync(IRole role, RequestOptions options = null); /// + /// Adds the specified to this user in the guild. + /// + /// The roles to be added to the user. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous role addition operation. + /// + Task AddRolesAsync(IEnumerable roleIds, RequestOptions options = null); + /// /// Adds the specified to this user in the guild. /// /// The roles to be added to the user. @@ -133,6 +150,15 @@ namespace Discord /// Task AddRolesAsync(IEnumerable roles, RequestOptions options = null); /// + /// Removes the specified from this user in the guild. + /// + /// The role to be removed from the user. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous role removal operation. + /// + Task RemoveRoleAsync(ulong roleId, RequestOptions options = null); + /// /// Removes the specified from this user in the guild. /// /// The role to be removed from the user. @@ -142,6 +168,15 @@ namespace Discord /// Task RemoveRoleAsync(IRole role, RequestOptions options = null); /// + /// Removes the specified from this user in the guild. + /// + /// The roles to be removed from the user. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous role removal operation. + /// + Task RemoveRolesAsync(IEnumerable roleIds, RequestOptions options = null); + /// /// Removes the specified from this user in the guild. /// /// The roles to be removed from the user. diff --git a/src/Discord.Net.Core/Entities/Users/IPresence.cs b/src/Discord.Net.Core/Entities/Users/IPresence.cs index a17ac0df2..6972037f0 100644 --- a/src/Discord.Net.Core/Entities/Users/IPresence.cs +++ b/src/Discord.Net.Core/Entities/Users/IPresence.cs @@ -7,10 +7,6 @@ namespace Discord /// public interface IPresence { - /// - /// Gets the activity this user is currently doing. - /// - IActivity Activity { get; } /// /// Gets the current status of this user. /// diff --git a/src/Discord.Net.Core/Entities/Users/IUser.cs b/src/Discord.Net.Core/Entities/Users/IUser.cs index e6008aab6..9596a8338 100644 --- a/src/Discord.Net.Core/Entities/Users/IUser.cs +++ b/src/Discord.Net.Core/Entities/Users/IUser.cs @@ -87,7 +87,7 @@ namespace Discord UserProperties? PublicFlags { get; } /// - /// Gets the direct message channel of this user, or create one if it does not already exist. + /// Creates the direct message channel of this user. /// /// /// This method is used to obtain or create a channel used to send a direct message. @@ -102,7 +102,7 @@ namespace Discord /// /// The following example attempts to send a direct message to the target user and logs the incident should /// it fail. - /// /// /// The options to be used when sending the request. @@ -110,6 +110,6 @@ namespace Discord /// A task that represents the asynchronous operation for getting or creating a DM channel. The task result /// contains the DM channel associated with this user. /// - Task GetOrCreateDMChannelAsync(RequestOptions options = null); + Task CreateDMChannelAsync(RequestOptions options = null); } } diff --git a/src/Discord.Net.Core/Entities/Users/UserProperties.cs b/src/Discord.Net.Core/Entities/Users/UserProperties.cs index b6deb744b..68232b254 100644 --- a/src/Discord.Net.Core/Entities/Users/UserProperties.cs +++ b/src/Discord.Net.Core/Entities/Users/UserProperties.cs @@ -22,12 +22,6 @@ namespace Discord /// HypeSquadEvents = 1 << 2, /// - /// Flag given to users who have participated in the bug report program. - /// This flag is obsolete, use instead. - /// - [Obsolete("Use BugHunterLevel1 instead.")] - BugHunter = 1 << 3, - /// /// Flag given to users who have participated in the bug report program and are level 1. /// BugHunterLevel1 = 1 << 3, @@ -67,5 +61,9 @@ namespace Discord /// Flag given to users that developed bots and early verified their accounts. /// EarlyVerifiedBotDeveloper = 1 << 17, + /// + /// Flag given to users that are discord certified moderators who has give discord's exam. + /// + DiscordCertifiedModerator = 1 << 18, } } diff --git a/src/Discord.Net.Core/Extensions/EmbedBuilderExtensions.cs b/src/Discord.Net.Core/Extensions/EmbedBuilderExtensions.cs index a3b8ddd5b..c05df7cb7 100644 --- a/src/Discord.Net.Core/Extensions/EmbedBuilderExtensions.cs +++ b/src/Discord.Net.Core/Extensions/EmbedBuilderExtensions.cs @@ -27,7 +27,7 @@ namespace Discord /// Fills the embed author field with the provided user's full username and avatar URL. public static EmbedBuilder WithAuthor(this EmbedBuilder builder, IUser user) => - builder.WithAuthor($"{user.Username}#{user.Discriminator}", user.GetAvatarUrl()); + builder.WithAuthor($"{user.Username}#{user.Discriminator}", user.GetAvatarUrl() ?? user.GetDefaultAvatarUrl()); /// Converts a object to a . /// The embed type is not . diff --git a/src/Discord.Net.Core/Extensions/StringExtensions.cs b/src/Discord.Net.Core/Extensions/StringExtensions.cs deleted file mode 100644 index c0ebb2626..000000000 --- a/src/Discord.Net.Core/Extensions/StringExtensions.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; - -namespace Discord -{ - internal static class StringExtensions - { - public static bool IsNullOrUri(this string url) => - string.IsNullOrEmpty(url) || Uri.IsWellFormedUriString(url, UriKind.Absolute); - } -} diff --git a/src/Discord.Net.Core/Extensions/UserExtensions.cs b/src/Discord.Net.Core/Extensions/UserExtensions.cs index 90f26828a..3e46308e6 100644 --- a/src/Discord.Net.Core/Extensions/UserExtensions.cs +++ b/src/Discord.Net.Core/Extensions/UserExtensions.cs @@ -42,7 +42,7 @@ namespace Discord RequestOptions options = null, AllowedMentions allowedMentions = null) { - return await (await user.GetOrCreateDMChannelAsync().ConfigureAwait(false)).SendMessageAsync(text, isTTS, embed, options, allowedMentions).ConfigureAwait(false); + return await (await user.CreateDMChannelAsync().ConfigureAwait(false)).SendMessageAsync(text, isTTS, embed, options, allowedMentions).ConfigureAwait(false); } /// @@ -94,7 +94,7 @@ namespace Discord RequestOptions options = null ) { - return await (await user.GetOrCreateDMChannelAsync().ConfigureAwait(false)).SendFileAsync(stream, filename, text, isTTS, embed, options).ConfigureAwait(false); + return await (await user.CreateDMChannelAsync().ConfigureAwait(false)).SendFileAsync(stream, filename, text, isTTS, embed, options).ConfigureAwait(false); } /// @@ -149,7 +149,7 @@ namespace Discord Embed embed = null, RequestOptions options = null) { - return await (await user.GetOrCreateDMChannelAsync().ConfigureAwait(false)).SendFileAsync(filePath, text, isTTS, embed, options).ConfigureAwait(false); + return await (await user.CreateDMChannelAsync().ConfigureAwait(false)).SendFileAsync(filePath, text, isTTS, embed, options).ConfigureAwait(false); } /// diff --git a/src/Discord.Net.Core/GatewayIntents.cs b/src/Discord.Net.Core/GatewayIntents.cs index f3dc5ceb9..6976806b2 100644 --- a/src/Discord.Net.Core/GatewayIntents.cs +++ b/src/Discord.Net.Core/GatewayIntents.cs @@ -39,5 +39,16 @@ namespace Discord DirectMessageReactions = 1 << 13, /// This intent includes TYPING_START DirectMessageTyping = 1 << 14, + /// + /// This intent includes all but and + /// that are privileged must be enabled for the application. + /// + AllUnprivileged = Guilds | GuildBans | GuildEmojis | GuildIntegrations | GuildWebhooks | GuildInvites | + GuildVoiceStates | GuildMessages | GuildMessageReactions | GuildMessageTyping | DirectMessages | + DirectMessageReactions | DirectMessageTyping, + /// + /// This intent includes all of them, including privileged ones. + /// + All = AllUnprivileged | GuildMembers | GuildPresences } } diff --git a/src/Discord.Net.Core/RateLimitPrecision.cs b/src/Discord.Net.Core/RateLimitPrecision.cs deleted file mode 100644 index fe3c1b90e..000000000 --- a/src/Discord.Net.Core/RateLimitPrecision.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Discord -{ - /// - /// Specifies the level of precision to request in the rate limit - /// response header. - /// - public enum RateLimitPrecision - { - /// - /// Specifies precision rounded up to the nearest whole second - /// - Second, - /// - /// Specifies precision rounded to the nearest millisecond. - /// - Millisecond - } -} diff --git a/src/Discord.Net.Core/TokenType.cs b/src/Discord.Net.Core/TokenType.cs index 0b958fe5c..1d68dc66b 100644 --- a/src/Discord.Net.Core/TokenType.cs +++ b/src/Discord.Net.Core/TokenType.cs @@ -5,8 +5,6 @@ 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/src/Discord.Net.Core/Utils/SnowflakeUtils.cs b/src/Discord.Net.Core/Utils/SnowflakeUtils.cs index dd8f8ca66..e52c99376 100644 --- a/src/Discord.Net.Core/Utils/SnowflakeUtils.cs +++ b/src/Discord.Net.Core/Utils/SnowflakeUtils.cs @@ -12,7 +12,7 @@ namespace Discord /// /// The snowflake identifier to resolve. /// - /// A representing the time for when the object is geenrated. + /// A representing the time for when the object is generated. /// public static DateTimeOffset FromSnowflake(ulong value) => DateTimeOffset.FromUnixTimeMilliseconds((long)((value >> 22) + 1420070400000UL)); diff --git a/src/Discord.Net.Examples/Core/Entities/Users/IUser.Examples.cs b/src/Discord.Net.Examples/Core/Entities/Users/IUser.Examples.cs index 79a90b46d..83daedaa0 100644 --- a/src/Discord.Net.Examples/Core/Entities/Users/IUser.Examples.cs +++ b/src/Discord.Net.Examples/Core/Entities/Users/IUser.Examples.cs @@ -18,11 +18,11 @@ namespace Discord.Net.Examples.Core.Entities.Users #endregion - #region GetOrCreateDMChannelAsync + #region CreateDMChannelAsync public async Task MessageUserAsync(IUser user) { - var channel = await user.GetOrCreateDMChannelAsync(); + var channel = await user.CreateDMChannelAsync(); try { await channel.SendMessageAsync("Awesome stuff!"); diff --git a/src/Discord.Net.Examples/WebSocket/BaseSocketClient.Events.Examples.cs b/src/Discord.Net.Examples/WebSocket/BaseSocketClient.Events.Examples.cs index 387584877..27d393c07 100644 --- a/src/Discord.Net.Examples/WebSocket/BaseSocketClient.Events.Examples.cs +++ b/src/Discord.Net.Examples/WebSocket/BaseSocketClient.Events.Examples.cs @@ -15,7 +15,7 @@ namespace Discord.Net.Examples.WebSocket => client.ReactionAdded += HandleReactionAddedAsync; public async Task HandleReactionAddedAsync(Cacheable cachedMessage, - ISocketMessageChannel originChannel, SocketReaction reaction) + Cacheable originChannel, SocketReaction reaction) { var message = await cachedMessage.GetOrDownloadAsync(); if (message != null && reaction.User.IsSpecified) @@ -100,16 +100,17 @@ namespace Discord.Net.Examples.WebSocket public void HookMessageDeleted(BaseSocketClient client) => client.MessageDeleted += HandleMessageDelete; - public Task HandleMessageDelete(Cacheable cachedMessage, ISocketMessageChannel channel) + public async Task HandleMessageDelete(Cacheable cachedMessage, Cacheable cachedChannel) { // check if the message exists in cache; if not, we cannot report what was removed - if (!cachedMessage.HasValue) return Task.CompletedTask; + if (!cachedMessage.HasValue) return; + // gets or downloads the channel if it's not in the cache + IMessageChannel channel = await cachedChannel.GetOrDownloadAsync(); var message = cachedMessage.Value; Console.WriteLine( $"A message ({message.Id}) from {message.Author} was removed from the channel {channel.Name} ({channel.Id}):" + Environment.NewLine + message.Content); - return Task.CompletedTask; } #endregion diff --git a/src/Discord.Net.Rest/API/Common/Guild.cs b/src/Discord.Net.Rest/API/Common/Guild.cs index 46075ce4d..bd25c7e1a 100644 --- a/src/Discord.Net.Rest/API/Common/Guild.cs +++ b/src/Discord.Net.Rest/API/Common/Guild.cs @@ -23,10 +23,6 @@ namespace Discord.API public ulong? AFKChannelId { get; set; } [JsonProperty("afk_timeout")] public int AFKTimeout { get; set; } - [JsonProperty("embed_enabled")] - public Optional EmbedEnabled { get; set; } - [JsonProperty("embed_channel_id")] - public Optional EmbedChannelId { get; set; } [JsonProperty("verification_level")] public VerificationLevel VerificationLevel { get; set; } [JsonProperty("default_message_notifications")] diff --git a/src/Discord.Net.Rest/API/Common/GuildEmbed.cs b/src/Discord.Net.Rest/API/Common/GuildEmbed.cs deleted file mode 100644 index d81632181..000000000 --- a/src/Discord.Net.Rest/API/Common/GuildEmbed.cs +++ /dev/null @@ -1,13 +0,0 @@ -#pragma warning disable CS1591 -using Newtonsoft.Json; - -namespace Discord.API -{ - internal class GuildEmbed - { - [JsonProperty("enabled")] - public bool Enabled { get; set; } - [JsonProperty("channel_id")] - public ulong? ChannelId { get; set; } - } -} diff --git a/src/Discord.Net.Rest/API/Common/InviteVanity.cs b/src/Discord.Net.Rest/API/Common/InviteVanity.cs index d39792674..a36ddee46 100644 --- a/src/Discord.Net.Rest/API/Common/InviteVanity.cs +++ b/src/Discord.Net.Rest/API/Common/InviteVanity.cs @@ -6,5 +6,7 @@ namespace Discord.API { [JsonProperty("code")] public string Code { get; set; } + [JsonProperty("uses")] + public int Uses { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Common/Message.cs b/src/Discord.Net.Rest/API/Common/Message.cs index b781de346..6ea2c29ff 100644 --- a/src/Discord.Net.Rest/API/Common/Message.cs +++ b/src/Discord.Net.Rest/API/Common/Message.cs @@ -58,5 +58,7 @@ namespace Discord.API public Optional AllowedMentions { get; set; } [JsonProperty("referenced_message")] public Optional ReferencedMessage { get; set; } + [JsonProperty("stickers")] + public Optional Stickers { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Common/Overwrite.cs b/src/Discord.Net.Rest/API/Common/Overwrite.cs index 1f3548a1c..3d94b0640 100644 --- a/src/Discord.Net.Rest/API/Common/Overwrite.cs +++ b/src/Discord.Net.Rest/API/Common/Overwrite.cs @@ -10,8 +10,8 @@ namespace Discord.API [JsonProperty("type")] public PermissionTarget TargetType { get; set; } [JsonProperty("deny"), Int53] - public ulong Deny { get; set; } + public string Deny { get; set; } [JsonProperty("allow"), Int53] - public ulong Allow { get; set; } + public string Allow { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Common/Presence.cs b/src/Discord.Net.Rest/API/Common/Presence.cs index b37ad4229..b44e9185d 100644 --- a/src/Discord.Net.Rest/API/Common/Presence.cs +++ b/src/Discord.Net.Rest/API/Common/Presence.cs @@ -13,8 +13,6 @@ namespace Discord.API public Optional GuildId { get; set; } [JsonProperty("status")] public UserStatus Status { get; set; } - [JsonProperty("game")] - public Game Game { get; set; } [JsonProperty("roles")] public Optional Roles { get; set; } diff --git a/src/Discord.Net.Rest/API/Common/Role.cs b/src/Discord.Net.Rest/API/Common/Role.cs index 190ae25c0..c655175da 100644 --- a/src/Discord.Net.Rest/API/Common/Role.cs +++ b/src/Discord.Net.Rest/API/Common/Role.cs @@ -18,7 +18,7 @@ namespace Discord.API [JsonProperty("position")] public int Position { get; set; } [JsonProperty("permissions"), Int53] - public ulong Permissions { get; set; } + public string Permissions { get; set; } [JsonProperty("managed")] public bool Managed { get; set; } [JsonProperty("tags")] diff --git a/src/Discord.Net.Rest/API/Common/Sticker.cs b/src/Discord.Net.Rest/API/Common/Sticker.cs new file mode 100644 index 000000000..0d1cac974 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/Sticker.cs @@ -0,0 +1,25 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class Sticker + { + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("pack_id")] + public ulong PackId { get; set; } + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("description")] + public string Desription { get; set; } + [JsonProperty("tags")] + public Optional Tags { get; set; } + [JsonProperty("asset")] + public string Asset { get; set; } + [JsonProperty("preview_asset")] + public string PreviewAsset { get; set; } + [JsonProperty("format_type")] + public StickerFormatType FormatType { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Common/Team.cs b/src/Discord.Net.Rest/API/Common/Team.cs index 4910f43f7..852368522 100644 --- a/src/Discord.Net.Rest/API/Common/Team.cs +++ b/src/Discord.Net.Rest/API/Common/Team.cs @@ -11,6 +11,8 @@ namespace Discord.API public ulong Id { get; set; } [JsonProperty("members")] public TeamMember[] TeamMembers { get; set; } + [JsonProperty("name")] + public string Name { get; set; } [JsonProperty("owner_user_id")] public ulong OwnerUserId { get; set; } } diff --git a/src/Discord.Net.Rest/API/Common/UserGuild.cs b/src/Discord.Net.Rest/API/Common/UserGuild.cs index f4f763885..825e9a09a 100644 --- a/src/Discord.Net.Rest/API/Common/UserGuild.cs +++ b/src/Discord.Net.Rest/API/Common/UserGuild.cs @@ -1,4 +1,4 @@ -#pragma warning disable CS1591 +#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API @@ -14,6 +14,6 @@ namespace Discord.API [JsonProperty("owner")] public bool Owner { get; set; } [JsonProperty("permissions"), Int53] - public ulong Permissions { get; set; } + public string Permissions { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Rest/ModifyChannelPermissionsParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyChannelPermissionsParams.cs index 0fe5f7e5a..269111a61 100644 --- a/src/Discord.Net.Rest/API/Rest/ModifyChannelPermissionsParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyChannelPermissionsParams.cs @@ -1,4 +1,4 @@ -#pragma warning disable CS1591 +#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API.Rest @@ -7,13 +7,13 @@ namespace Discord.API.Rest internal class ModifyChannelPermissionsParams { [JsonProperty("type")] - public string Type { get; } + public int Type { get; } [JsonProperty("allow")] - public ulong Allow { get; } + public string Allow { get; } [JsonProperty("deny")] - public ulong Deny { get; } + public string Deny { get; } - public ModifyChannelPermissionsParams(string type, ulong allow, ulong deny) + public ModifyChannelPermissionsParams(int type, string allow, string deny) { Type = type; Allow = allow; diff --git a/src/Discord.Net.Rest/API/Rest/ModifyGuildRoleParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyGuildRoleParams.cs index 287e1cafe..8605411c5 100644 --- a/src/Discord.Net.Rest/API/Rest/ModifyGuildRoleParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyGuildRoleParams.cs @@ -1,4 +1,4 @@ -#pragma warning disable CS1591 +#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API.Rest @@ -9,7 +9,7 @@ namespace Discord.API.Rest [JsonProperty("name")] public Optional Name { get; set; } [JsonProperty("permissions")] - public Optional Permissions { get; set; } + public Optional Permissions { get; set; } [JsonProperty("color")] public Optional Color { get; set; } [JsonProperty("hoist")] diff --git a/src/Discord.Net.Rest/API/Rest/ModifyWebhookMessageParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyWebhookMessageParams.cs new file mode 100644 index 000000000..ba8fcbb4e --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/ModifyWebhookMessageParams.cs @@ -0,0 +1,16 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + internal class ModifyWebhookMessageParams + { + [JsonProperty("content")] + public Optional Content { get; set; } + [JsonProperty("embeds")] + public Optional Embeds { get; set; } + [JsonProperty("allowed_mentions")] + public Optional AllowedMentions { get; set; } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/SuppressEmbedParams.cs b/src/Discord.Net.Rest/API/Rest/SuppressEmbedParams.cs deleted file mode 100644 index 9139627b8..000000000 --- a/src/Discord.Net.Rest/API/Rest/SuppressEmbedParams.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Newtonsoft.Json; - -namespace Discord.API.Rest -{ - [JsonObject(MemberSerialization = MemberSerialization.OptIn)] - internal class SuppressEmbedParams - { - [JsonProperty("suppress")] - public bool Suppressed { get; set; } - } -} diff --git a/src/Discord.Net.Rest/ClientHelper.cs b/src/Discord.Net.Rest/ClientHelper.cs index 3add70d78..380321125 100644 --- a/src/Discord.Net.Rest/ClientHelper.cs +++ b/src/Discord.Net.Rest/ClientHelper.cs @@ -22,7 +22,7 @@ namespace Discord.Rest return RestToken.Create(client, model); } - public static async Task GetChannelAsync(BaseDiscordClient client, + public static async Task GetChannelAsync(BaseDiscordClient client, ulong id, RequestOptions options) { var model = await client.ApiClient.GetChannelAsync(id, options).ConfigureAwait(false); @@ -50,13 +50,13 @@ namespace Discord.Rest .Where(x => x.Type == ChannelType.Group) .Select(x => RestGroupChannel.Create(client, x)).ToImmutableArray(); } - + public static async Task> GetConnectionsAsync(BaseDiscordClient client, RequestOptions options) { var models = await client.ApiClient.GetMyConnectionsAsync(options).ConfigureAwait(false); return models.Select(RestConnection.Create).ToImmutableArray(); } - + public static async Task GetInviteAsync(BaseDiscordClient client, string inviteId, RequestOptions options) { @@ -65,7 +65,7 @@ namespace Discord.Rest return RestInviteMetadata.Create(client, null, null, model); return null; } - + public static async Task GetGuildAsync(BaseDiscordClient client, ulong id, bool withCounts, RequestOptions options) { @@ -74,14 +74,6 @@ namespace Discord.Rest return RestGuild.Create(client, model); return null; } - public static async Task GetGuildEmbedAsync(BaseDiscordClient client, - ulong id, RequestOptions options) - { - var model = await client.ApiClient.GetGuildEmbedAsync(id, options).ConfigureAwait(false); - if (model != null) - return RestGuildEmbed.Create(model); - return null; - } public static async Task GetGuildWidgetAsync(BaseDiscordClient client, ulong id, RequestOptions options) { @@ -90,7 +82,7 @@ namespace Discord.Rest return RestGuildWidget.Create(model); return null; } - public static IAsyncEnumerable> GetGuildSummariesAsync(BaseDiscordClient client, + public static IAsyncEnumerable> GetGuildSummariesAsync(BaseDiscordClient client, ulong? fromGuildId, int? limit, RequestOptions options) { return new PagedAsyncEnumerable( @@ -141,7 +133,7 @@ namespace Discord.Rest var model = await client.ApiClient.CreateGuildAsync(args, options).ConfigureAwait(false); return RestGuild.Create(client, model); } - + public static async Task GetUserAsync(BaseDiscordClient client, ulong id, RequestOptions options) { @@ -206,5 +198,9 @@ namespace Discord.Rest } }; } + public static Task AddRoleAsync(BaseDiscordClient client, ulong guildId, ulong userId, ulong roleId, RequestOptions options = null) + => client.ApiClient.AddRoleAsync(guildId, userId, roleId, options); + public static Task RemoveRoleAsync(BaseDiscordClient client, ulong guildId, ulong userId, ulong roleId, RequestOptions options = null) + => client.ApiClient.RemoveRoleAsync(guildId, userId, roleId, options); } } diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index ea83d59eb..3a68c684a 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -48,20 +48,18 @@ namespace Discord.API internal IRestClient RestClient { get; private set; } internal ulong? CurrentUserId { get; set; } - public RateLimitPrecision RateLimitPrecision { get; private set; } internal bool UseSystemClock { get; set; } internal JsonSerializer Serializer => _serializer; /// Unknown OAuth token type. public DiscordRestApiClient(RestClientProvider restClientProvider, string userAgent, RetryMode defaultRetryMode = RetryMode.AlwaysRetry, - JsonSerializer serializer = null, RateLimitPrecision rateLimitPrecision = RateLimitPrecision.Second, bool useSystemClock = true, string clientId = null, string clientSecret = null) + JsonSerializer serializer = null, bool useSystemClock = true, string clientId = null, string clientSecret = null) { _restClientProvider = restClientProvider; UserAgent = userAgent; DefaultRetryMode = defaultRetryMode; _serializer = serializer ?? new JsonSerializer { ContractResolver = new DiscordContractResolver() }; - RateLimitPrecision = rateLimitPrecision; UseSystemClock = useSystemClock; RequestQueue = new RequestQueue(); @@ -82,14 +80,12 @@ namespace Discord.API RestClient.SetHeader("accept", "*/*"); RestClient.SetHeader("user-agent", UserAgent); RestClient.SetHeader("authorization", GetPrefixedToken(AuthTokenType, AuthToken)); - RestClient.SetHeader("X-RateLimit-Precision", RateLimitPrecision.ToString().ToLower()); } /// Unknown OAuth token type. internal static string GetPrefixedToken(TokenType tokenType, string token) { return tokenType switch { - default(TokenType) => token, TokenType.Bot => $"Bot {token}", TokenType.Bearer => $"Bearer {token}", _ => throw new ArgumentException(message: "Unknown OAuth token type.", paramName: nameof(tokenType)), @@ -600,6 +596,43 @@ namespace Discord.API 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 . + /// This operation may only be called with a token. + public async Task ModifyWebhookMessageAsync(ulong webhookId, ulong messageId, ModifyWebhookMessageParams args, RequestOptions options = null) + { + if (AuthTokenType != TokenType.Webhook) + throw new InvalidOperationException($"This operation may only be called with a {nameof(TokenType.Webhook)} token."); + + Preconditions.NotNull(args, nameof(args)); + Preconditions.NotEqual(webhookId, 0, nameof(webhookId)); + Preconditions.NotEqual(messageId, 0, nameof(messageId)); + + if (args.Embeds.IsSpecified) + Preconditions.AtMost(args.Embeds.Value.Length, 10, nameof(args.Embeds), "A max of 10 Embeds are allowed."); + if (args.Content.IsSpecified && args.Content.Value.Length > DiscordConfig.MaxMessageSize) + throw new ArgumentException(message: $"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", paramName: nameof(args.Content)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(webhookId: webhookId); + await SendJsonAsync("PATCH", () => $"webhooks/{webhookId}/{AuthToken}/messages/{messageId}", args, ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); + } + + /// This operation may only be called with a token. + public async Task DeleteWebhookMessageAsync(ulong webhookId, ulong messageId, RequestOptions options = null) + { + if (AuthTokenType != TokenType.Webhook) + throw new InvalidOperationException($"This operation may only be called with a {nameof(TokenType.Webhook)} token."); + + Preconditions.NotEqual(webhookId, 0, nameof(webhookId)); + Preconditions.NotEqual(messageId, 0, nameof(messageId)); + + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(webhookId: webhookId); + await SendAsync("DELETE", () => $"webhooks/{webhookId}/{AuthToken}/messages/{messageId}", ids, 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) { @@ -685,16 +718,6 @@ namespace Discord.API return await SendJsonAsync("PATCH", () => $"channels/{channelId}/messages/{messageId}", args, ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); } - public async Task SuppressEmbedAsync(ulong channelId, ulong messageId, Rest.SuppressEmbedParams args, 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 SendJsonAsync("POST", () => $"channels/{channelId}/messages/{messageId}/suppress-embeds", args, ids, options: options).ConfigureAwait(false); - } - public async Task AddReactionAsync(ulong channelId, ulong messageId, string emoji, RequestOptions options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); @@ -976,7 +999,7 @@ namespace Discord.API var ids = new BucketIds(guildId: guildId); string reason = string.IsNullOrWhiteSpace(args.Reason) ? "" : $"&reason={Uri.EscapeDataString(args.Reason)}"; - await SendAsync("PUT", () => $"guilds/{guildId}/bans/{userId}?delete-message-days={args.DeleteMessageDays}{reason}", ids, options: options).ConfigureAwait(false); + await SendAsync("PUT", () => $"guilds/{guildId}/bans/{userId}?delete_message_days={args.DeleteMessageDays}{reason}", ids, options: options).ConfigureAwait(false); } /// and must not be equal to zero. public async Task RemoveGuildBanAsync(ulong guildId, ulong userId, RequestOptions options = null) @@ -989,32 +1012,6 @@ namespace Discord.API await SendAsync("DELETE", () => $"guilds/{guildId}/bans/{userId}", ids, options: options).ConfigureAwait(false); } - //Guild Embeds - /// must not be equal to zero. - public async Task GetGuildEmbedAsync(ulong guildId, RequestOptions options = null) - { - Preconditions.NotEqual(guildId, 0, nameof(guildId)); - options = RequestOptions.CreateOrClone(options); - - try - { - var ids = new BucketIds(guildId: guildId); - return await SendAsync("GET", () => $"guilds/{guildId}/embed", ids, options: options).ConfigureAwait(false); - } - catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.NotFound) { return null; } - } - /// must not be equal to zero. - /// must not be . - public async Task ModifyGuildEmbedAsync(ulong guildId, Rest.ModifyGuildEmbedParams args, RequestOptions options = null) - { - Preconditions.NotNull(args, nameof(args)); - Preconditions.NotEqual(guildId, 0, nameof(guildId)); - options = RequestOptions.CreateOrClone(options); - - var ids = new BucketIds(guildId: guildId); - return await SendJsonAsync("PATCH", () => $"guilds/{guildId}/embed", args, ids, options: options).ConfigureAwait(false); - } - //Guild Widget /// must not be equal to zero. public async Task GetGuildWidgetAsync(ulong guildId, RequestOptions options = null) @@ -1320,6 +1317,15 @@ namespace Discord.API } //Guild emoji + public async Task> GetGuildEmotesAsync(ulong guildId, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + return await SendAsync>("GET", () => $"guilds/{guildId}/emojis", ids, options: options).ConfigureAwait(false); + } + public async Task GetGuildEmoteAsync(ulong guildId, ulong emoteId, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); diff --git a/src/Discord.Net.Rest/DiscordRestClient.cs b/src/Discord.Net.Rest/DiscordRestClient.cs index a4c4b18b2..0478da02e 100644 --- a/src/Discord.Net.Rest/DiscordRestClient.cs +++ b/src/Discord.Net.Rest/DiscordRestClient.cs @@ -16,7 +16,7 @@ namespace Discord.Rest /// /// Gets the logged-in user. /// - public new RestSelfUser CurrentUser => base.CurrentUser as RestSelfUser; + public new RestSelfUser CurrentUser { get => base.CurrentUser as RestSelfUser; internal set => base.CurrentUser = value; } /// public DiscordRestClient() : this(new DiscordRestConfig()) { } @@ -86,9 +86,6 @@ namespace Discord.Rest => ClientHelper.GetGuildAsync(this, id, false, options); public Task GetGuildAsync(ulong id, bool withCounts, RequestOptions options = null) => ClientHelper.GetGuildAsync(this, id, withCounts, options); - [Obsolete("This endpoint is deprecated, use GetGuildWidgetAsync instead.")] - public Task GetGuildEmbedAsync(ulong id, RequestOptions options = null) - => ClientHelper.GetGuildEmbedAsync(this, id, options); public Task GetGuildWidgetAsync(ulong id, RequestOptions options = null) => ClientHelper.GetGuildWidgetAsync(this, id, options); public IAsyncEnumerable> GetGuildSummariesAsync(RequestOptions options = null) @@ -113,7 +110,19 @@ namespace Discord.Rest => ClientHelper.GetVoiceRegionAsync(this, id, options); public Task GetWebhookAsync(ulong id, RequestOptions options = null) => ClientHelper.GetWebhookAsync(this, id, options); - + public Task AddRoleAsync(ulong guildId, ulong userId, ulong roleId) + => ClientHelper.AddRoleAsync(this, guildId, userId, roleId); + public Task RemoveRoleAsync(ulong guildId, ulong userId, ulong roleId) + => ClientHelper.RemoveRoleAsync(this, guildId, userId, roleId); + + public Task AddReactionAsync(ulong channelId, ulong messageId, IEmote emote, RequestOptions options = null) + => MessageHelper.AddReactionAsync(channelId, messageId, emote, this, options); + public Task RemoveReactionAsync(ulong channelId, ulong messageId, ulong userId, IEmote emote, RequestOptions options = null) + => MessageHelper.RemoveReactionAsync(channelId, messageId, userId, emote, this, options); + public Task RemoveAllReactionsAsync(ulong channelId, ulong messageId, RequestOptions options = null) + => MessageHelper.RemoveAllReactionsAsync(channelId, messageId, this, options); + public Task RemoveAllReactionsForEmoteAsync(ulong channelId, ulong messageId, IEmote emote, RequestOptions options = null) + => MessageHelper.RemoveAllReactionsForEmoteAsync(channelId, messageId, emote, this, options); //IDiscordClient /// async Task IDiscordClient.GetApplicationInfoAsync(RequestOptions options) diff --git a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs index c1d0ac294..22395ab3a 100644 --- a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs +++ b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs @@ -33,8 +33,8 @@ namespace Discord.Rest { TargetId = overwrite.TargetId, TargetType = overwrite.TargetType, - Allow = overwrite.Permissions.AllowValue, - Deny = overwrite.Permissions.DenyValue + Allow = overwrite.Permissions.AllowValue.ToString(), + Deny = overwrite.Permissions.DenyValue.ToString() }).ToArray() : Optional.Create(), }; @@ -59,8 +59,8 @@ namespace Discord.Rest { TargetId = overwrite.TargetId, TargetType = overwrite.TargetType, - Allow = overwrite.Permissions.AllowValue, - Deny = overwrite.Permissions.DenyValue + Allow = overwrite.Permissions.AllowValue.ToString(), + Deny = overwrite.Permissions.DenyValue.ToString() }).ToArray() : Optional.Create(), }; @@ -84,8 +84,8 @@ namespace Discord.Rest { TargetId = overwrite.TargetId, TargetType = overwrite.TargetType, - Allow = overwrite.Permissions.AllowValue, - Deny = overwrite.Permissions.DenyValue + Allow = overwrite.Permissions.AllowValue.ToString(), + Deny = overwrite.Permissions.DenyValue.ToString() }).ToArray() : Optional.Create(), }; @@ -286,6 +286,13 @@ namespace Discord.Rest return RestUserMessage.Create(client, channel, client.CurrentUser, model); } + public static async Task ModifyMessageAsync(IMessageChannel channel, ulong messageId, Action func, + BaseDiscordClient client, RequestOptions options) + { + var msgModel = await MessageHelper.ModifyAsync(channel.Id, messageId, client, func, options).ConfigureAwait(false); + return RestUserMessage.Create(client, channel, msgModel.Author.IsSpecified ? RestUser.Create(client, msgModel.Author.Value) : client.CurrentUser, msgModel); + } + public static Task DeleteMessageAsync(IMessageChannel channel, ulong messageId, BaseDiscordClient client, RequestOptions options) => MessageHelper.DeleteAsync(channel.Id, messageId, client, options); @@ -321,13 +328,13 @@ namespace Discord.Rest public static async Task AddPermissionOverwriteAsync(IGuildChannel channel, BaseDiscordClient client, IUser user, OverwritePermissions perms, RequestOptions options) { - var args = new ModifyChannelPermissionsParams("member", perms.AllowValue, perms.DenyValue); + var args = new ModifyChannelPermissionsParams((int)PermissionTarget.User, perms.AllowValue.ToString(), perms.DenyValue.ToString()); await client.ApiClient.ModifyChannelPermissionsAsync(channel.Id, user.Id, args, options).ConfigureAwait(false); } public static async Task AddPermissionOverwriteAsync(IGuildChannel channel, BaseDiscordClient client, IRole role, OverwritePermissions perms, RequestOptions options) { - var args = new ModifyChannelPermissionsParams("role", perms.AllowValue, perms.DenyValue); + var args = new ModifyChannelPermissionsParams((int)PermissionTarget.Role, perms.AllowValue.ToString(), perms.DenyValue.ToString()); await client.ApiClient.ModifyChannelPermissionsAsync(channel.Id, role.Id, args, options).ConfigureAwait(false); } public static async Task RemovePermissionOverwriteAsync(IGuildChannel channel, BaseDiscordClient client, @@ -443,8 +450,8 @@ namespace Discord.Rest { TargetId = overwrite.TargetId, TargetType = overwrite.TargetType, - Allow = overwrite.Permissions.AllowValue, - Deny = overwrite.Permissions.DenyValue + Allow = overwrite.Permissions.AllowValue.ToString(), + Deny = overwrite.Permissions.DenyValue.ToString() }).ToArray() }; await client.ApiClient.ModifyGuildChannelAsync(channel.Id, apiArgs, options).ConfigureAwait(false); diff --git a/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs index d59a10fb5..6ccfd204c 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs @@ -135,6 +135,10 @@ namespace Discord.Rest public Task DeleteMessageAsync(IMessage message, RequestOptions options = null) => ChannelHelper.DeleteMessageAsync(this, message.Id, Discord, options); + /// + public async Task ModifyMessageAsync(ulong messageId, Action func, RequestOptions options = null) + => await ChannelHelper.ModifyMessageAsync(this, messageId, func, Discord, options).ConfigureAwait(false); + /// public Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); diff --git a/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs index 666d6a4f4..2b0ab8b42 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs @@ -93,6 +93,10 @@ namespace Discord.Rest public Task DeleteMessageAsync(IMessage message, RequestOptions options = null) => ChannelHelper.DeleteMessageAsync(this, message.Id, Discord, options); + /// + public async Task ModifyMessageAsync(ulong messageId, Action func, RequestOptions options = null) + => await ChannelHelper.ModifyMessageAsync(this, messageId, func, Discord, options).ConfigureAwait(false); + /// /// Message content is too long, length must be less or equal to . public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null, MessageReference messageReference = null) diff --git a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs index fdc4b7988..c6d0b0509 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs @@ -152,6 +152,10 @@ namespace Discord.Rest public Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) => ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options); + /// + public async Task ModifyMessageAsync(ulong messageId, Action func, RequestOptions options = null) + => await ChannelHelper.ModifyMessageAsync(this, messageId, func, Discord, options).ConfigureAwait(false); + /// public Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); diff --git a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs index 04ec27930..58a4ea2c8 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Threading.Tasks; -using EmbedModel = Discord.API.GuildEmbed; using WidgetModel = Discord.API.GuildWidget; using Model = Discord.API.Guild; using RoleModel = Discord.API.Role; @@ -81,26 +80,6 @@ namespace Discord.Rest return await client.ApiClient.ModifyGuildAsync(guild.Id, apiArgs, options).ConfigureAwait(false); } /// is null. - public static async Task ModifyEmbedAsync(IGuild guild, BaseDiscordClient client, - Action func, RequestOptions options) - { - if (func == null) throw new ArgumentNullException(nameof(func)); - - var args = new GuildEmbedProperties(); - func(args); - var apiArgs = new API.Rest.ModifyGuildEmbedParams - { - Enabled = args.Enabled - }; - - if (args.Channel.IsSpecified) - apiArgs.ChannelId = args.Channel.Value?.Id; - else if (args.ChannelId.IsSpecified) - apiArgs.ChannelId = args.ChannelId.Value; - - return await client.ApiClient.ModifyGuildEmbedAsync(guild.Id, apiArgs, options).ConfigureAwait(false); - } - /// is null. public static async Task ModifyWidgetAsync(IGuild guild, BaseDiscordClient client, Action func, RequestOptions options) { @@ -205,8 +184,8 @@ namespace Discord.Rest { TargetId = overwrite.TargetId, TargetType = overwrite.TargetType, - Allow = overwrite.Permissions.AllowValue, - Deny = overwrite.Permissions.DenyValue + Allow = overwrite.Permissions.AllowValue.ToString(), + Deny = overwrite.Permissions.DenyValue.ToString() }).ToArray() : Optional.Create(), }; @@ -233,8 +212,8 @@ namespace Discord.Rest { TargetId = overwrite.TargetId, TargetType = overwrite.TargetType, - Allow = overwrite.Permissions.AllowValue, - Deny = overwrite.Permissions.DenyValue + Allow = overwrite.Permissions.AllowValue.ToString(), + Deny = overwrite.Permissions.DenyValue.ToString() }).ToArray() : Optional.Create(), }; @@ -258,8 +237,8 @@ namespace Discord.Rest { TargetId = overwrite.TargetId, TargetType = overwrite.TargetType, - Allow = overwrite.Permissions.AllowValue, - Deny = overwrite.Permissions.DenyValue + Allow = overwrite.Permissions.AllowValue.ToString(), + Deny = overwrite.Permissions.DenyValue.ToString() }).ToArray() : Optional.Create(), }; @@ -304,6 +283,7 @@ namespace Discord.Rest var vanityModel = await client.ApiClient.GetVanityInviteAsync(guild.Id, options).ConfigureAwait(false); if (vanityModel == null) throw new InvalidOperationException("This guild does not have a vanity URL."); var inviteModel = await client.ApiClient.GetInviteAsync(vanityModel.Code, options).ConfigureAwait(false); + inviteModel.Uses = vanityModel.Uses; return RestInviteMetadata.Create(client, guild, null, inviteModel); } @@ -320,7 +300,7 @@ namespace Discord.Rest Hoist = isHoisted, Mentionable = isMentionable, Name = name, - Permissions = permissions?.RawValue ?? Optional.Create() + Permissions = permissions?.RawValue.ToString() ?? Optional.Create() }; var model = await client.ApiClient.CreateGuildRoleAsync(guild.Id, createGuildRoleParams, options).ConfigureAwait(false); @@ -496,6 +476,11 @@ namespace Discord.Rest } //Emotes + public static async Task> GetEmotesAsync(IGuild guild, BaseDiscordClient client, RequestOptions options) + { + var models = await client.ApiClient.GetGuildEmotesAsync(guild.Id, options).ConfigureAwait(false); + return models.Select(x => x.ToEntity()).ToImmutableArray(); + } public static async Task GetEmoteAsync(IGuild guild, BaseDiscordClient client, ulong id, RequestOptions options) { var emote = await client.ApiClient.GetGuildEmoteAsync(guild.Id, id, options).ConfigureAwait(false); diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs index c74e128a8..ea703a26a 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -6,7 +6,6 @@ using System.Diagnostics; using System.Globalization; using System.Linq; using System.Threading.Tasks; -using EmbedModel = Discord.API.GuildEmbed; using WidgetModel = Discord.API.GuildWidget; using Model = Discord.API.Guild; @@ -27,8 +26,6 @@ namespace Discord.Rest /// public int AFKTimeout { get; private set; } /// - public bool IsEmbeddable { get; private set; } - /// public bool IsWidgetEnabled { get; private set; } /// public VerificationLevel VerificationLevel { get; private set; } @@ -42,8 +39,6 @@ namespace Discord.Rest /// public ulong? AFKChannelId { get; private set; } /// - public ulong? EmbedChannelId { get; private set; } - /// public ulong? WidgetChannelId { get; private set; } /// public ulong? SystemChannelId { get; private set; } @@ -95,8 +90,6 @@ namespace Discord.Rest /// public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); - [Obsolete("DefaultChannelId is deprecated, use GetDefaultChannelAsync")] - public ulong DefaultChannelId => Id; /// public string IconUrl => CDN.GetGuildIconUrl(Id, IconId); /// @@ -133,16 +126,12 @@ namespace Discord.Rest internal void Update(Model model) { AFKChannelId = model.AFKChannelId; - if (model.EmbedChannelId.IsSpecified) - EmbedChannelId = model.EmbedChannelId.Value; if (model.WidgetChannelId.IsSpecified) WidgetChannelId = model.WidgetChannelId.Value; SystemChannelId = model.SystemChannelId; RulesChannelId = model.RulesChannelId; PublicUpdatesChannelId = model.PublicUpdatesChannelId; AFKTimeout = model.AFKTimeout; - if (model.EmbedEnabled.IsSpecified) - IsEmbeddable = model.EmbedEnabled.Value; if (model.WidgetEnabled.IsSpecified) IsWidgetEnabled = model.WidgetEnabled.Value; IconId = model.Icon; @@ -200,11 +189,6 @@ namespace Discord.Rest Available = true; } - internal void Update(EmbedModel model) - { - EmbedChannelId = model.ChannelId; - IsEmbeddable = model.Enabled; - } internal void Update(WidgetModel model) { WidgetChannelId = model.ChannelId; @@ -241,15 +225,6 @@ namespace Discord.Rest Update(model); } - /// - /// is . - [Obsolete("This endpoint is deprecated, use ModifyWidgetAsync instead.")] - public async Task ModifyEmbedAsync(Action func, RequestOptions options = null) - { - var model = await GuildHelper.ModifyEmbedAsync(this, Discord, func, options).ConfigureAwait(false); - Update(model); - } - /// /// is . public async Task ModifyWidgetAsync(Action func, RequestOptions options = null) @@ -463,23 +438,6 @@ namespace Discord.Rest .FirstOrDefault(); } - /// - /// Gets the embed channel (i.e. the channel set in the guild's widget settings) in this guild. - /// - /// The options to be used when sending the request. - /// - /// A task that represents the asynchronous get operation. The task result contains the embed channel set - /// within the server's widget settings; if none is set. - /// - [Obsolete("This endpoint is deprecated, use GetWidgetChannelAsync instead.")] - public async Task GetEmbedChannelAsync(RequestOptions options = null) - { - var embedId = EmbedChannelId; - if (embedId.HasValue) - return await GuildHelper.GetChannelAsync(this, Discord, embedId.Value, options).ConfigureAwait(false); - return null; - } - /// /// Gets the widget channel (i.e. the channel set in the guild's widget settings) in this guild. /// @@ -828,6 +786,9 @@ namespace Discord.Rest //Emotes /// + public Task> GetEmotesAsync(RequestOptions options = null) + => GuildHelper.GetEmotesAsync(this, Discord, options); + /// public Task GetEmoteAsync(ulong id, RequestOptions options = null) => GuildHelper.GetEmoteAsync(this, Discord, id, options); /// @@ -934,15 +895,6 @@ namespace Discord.Rest return null; } /// - [Obsolete("This endpoint is deprecated, use GetWidgetChannelAsync instead.")] - async Task IGuild.GetEmbedChannelAsync(CacheMode mode, RequestOptions options) - { - if (mode == CacheMode.AllowDownload) - return await GetEmbedChannelAsync(options).ConfigureAwait(false); - else - return null; - } - /// async Task IGuild.GetWidgetChannelAsync(CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuildEmbed.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuildEmbed.cs deleted file mode 100644 index 41c76eb06..000000000 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuildEmbed.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Diagnostics; -using Model = Discord.API.GuildEmbed; - -namespace Discord.Rest -{ - [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public struct RestGuildEmbed - { - public bool IsEnabled { get; private set; } - public ulong? ChannelId { get; private set; } - - internal RestGuildEmbed(bool isEnabled, ulong? channelId) - { - ChannelId = channelId; - IsEnabled = isEnabled; - } - internal static RestGuildEmbed Create(Model model) - { - return new RestGuildEmbed(model.Enabled, model.ChannelId); - } - - public override string ToString() => ChannelId?.ToString() ?? "Unknown"; - private string DebuggerDisplay => $"{ChannelId} ({(IsEnabled ? "Enabled" : "Disabled")})"; - } -} diff --git a/src/Discord.Net.Rest/Entities/Invites/RestInviteMetadata.cs b/src/Discord.Net.Rest/Entities/Invites/RestInviteMetadata.cs index 07ee5851c..a0ed9ec81 100644 --- a/src/Discord.Net.Rest/Entities/Invites/RestInviteMetadata.cs +++ b/src/Discord.Net.Rest/Entities/Invites/RestInviteMetadata.cs @@ -8,9 +8,6 @@ namespace Discord.Rest { private long _createdAtTicks; - /// - [Obsolete("This property doesn't exist anymore and shouldn't be used.")] - public bool IsRevoked { get; private set; } /// public bool IsTemporary { get; private set; } /// diff --git a/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs b/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs index e22ae71cd..31252466b 100644 --- a/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs +++ b/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs @@ -71,6 +71,48 @@ namespace Discord.Rest return await client.ApiClient.ModifyMessageAsync(msg.Channel.Id, msg.Id, apiArgs, options).ConfigureAwait(false); } + public static async Task ModifyAsync(ulong channelId, ulong msgId, BaseDiscordClient client, Action func, + RequestOptions options) + { + var args = new MessageProperties(); + func(args); + + if ((args.Content.IsSpecified && string.IsNullOrEmpty(args.Content.Value)) && (args.Embed.IsSpecified && args.Embed.Value == null)) + Preconditions.NotNullOrEmpty(args.Content.IsSpecified ? args.Content.Value : string.Empty, nameof(args.Content)); + + if (args.AllowedMentions.IsSpecified) + { + AllowedMentions allowedMentions = args.AllowedMentions.Value; + Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed."); + Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed."); + + // 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 apiArgs = new API.Rest.ModifyMessageParams + { + Content = args.Content, + Embed = args.Embed.IsSpecified ? args.Embed.Value.ToModel() : Optional.Create(), + Flags = args.Flags.IsSpecified ? args.Flags.Value : Optional.Create(), + AllowedMentions = args.AllowedMentions.IsSpecified ? args.AllowedMentions.Value.ToModel() : Optional.Create(), + }; + return await client.ApiClient.ModifyMessageAsync(channelId, msgId, apiArgs, options).ConfigureAwait(false); + } + public static Task DeleteAsync(IMessage msg, BaseDiscordClient client, RequestOptions options) => DeleteAsync(msg.Channel.Id, msg.Id, client, options); @@ -80,13 +122,9 @@ namespace Discord.Rest await client.ApiClient.DeleteMessageAsync(channelId, msgId, options).ConfigureAwait(false); } - public static async Task SuppressEmbedsAsync(IMessage msg, BaseDiscordClient client, bool suppress, RequestOptions options) + public static async Task AddReactionAsync(ulong channelId, ulong messageId, IEmote emote, BaseDiscordClient client, RequestOptions options) { - var apiArgs = new API.Rest.SuppressEmbedParams - { - Suppressed = suppress - }; - await client.ApiClient.SuppressEmbedAsync(msg.Channel.Id, msg.Id, apiArgs, options).ConfigureAwait(false); + await client.ApiClient.AddReactionAsync(channelId, messageId, emote is Emote e ? $"{e.Name}:{e.Id}" : UrlEncode(emote.Name), options).ConfigureAwait(false); } public static async Task AddReactionAsync(IMessage msg, IEmote emote, BaseDiscordClient client, RequestOptions options) @@ -94,16 +132,31 @@ namespace Discord.Rest await client.ApiClient.AddReactionAsync(msg.Channel.Id, msg.Id, emote is Emote e ? $"{e.Name}:{e.Id}" : UrlEncode(emote.Name), options).ConfigureAwait(false); } + public static async Task RemoveReactionAsync(ulong channelId, ulong messageId, ulong userId, IEmote emote, BaseDiscordClient client, RequestOptions options) + { + await client.ApiClient.RemoveReactionAsync(channelId, messageId, userId, emote is Emote e ? $"{e.Name}:{e.Id}" : UrlEncode(emote.Name), options).ConfigureAwait(false); + } + public static async Task RemoveReactionAsync(IMessage msg, ulong userId, IEmote emote, BaseDiscordClient client, RequestOptions options) { await client.ApiClient.RemoveReactionAsync(msg.Channel.Id, msg.Id, userId, emote is Emote e ? $"{e.Name}:{e.Id}" : UrlEncode(emote.Name), options).ConfigureAwait(false); } + public static async Task RemoveAllReactionsAsync(ulong channelId, ulong messageId, BaseDiscordClient client, RequestOptions options) + { + await client.ApiClient.RemoveAllReactionsAsync(channelId, messageId, options).ConfigureAwait(false); + } + public static async Task RemoveAllReactionsAsync(IMessage msg, BaseDiscordClient client, RequestOptions options) { await client.ApiClient.RemoveAllReactionsAsync(msg.Channel.Id, msg.Id, options).ConfigureAwait(false); } + public static async Task RemoveAllReactionsForEmoteAsync(ulong channelId, ulong messageId, IEmote emote, BaseDiscordClient client, RequestOptions options) + { + await client.ApiClient.RemoveAllReactionsForEmoteAsync(channelId, messageId, emote is Emote e ? $"{e.Name}:{e.Id}" : UrlEncode(emote.Name), 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}" : UrlEncode(emote.Name), options).ConfigureAwait(false); diff --git a/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs b/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs index b2a745980..0c54743a6 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs @@ -58,6 +58,8 @@ namespace Discord.Rest public virtual IReadOnlyCollection MentionedUsers => ImmutableArray.Create(); /// public virtual IReadOnlyCollection Tags => ImmutableArray.Create(); + /// + public virtual IReadOnlyCollection Stickers => ImmutableArray.Create(); /// public DateTimeOffset Timestamp => DateTimeUtils.FromTicks(_timestampTicks); @@ -69,6 +71,8 @@ namespace Discord.Rest public MessageReference Reference { get; private set; } /// public MessageFlags? Flags { get; private set; } + /// + public MessageType Type { get; private set; } internal RestMessage(BaseDiscordClient discord, ulong id, IMessageChannel channel, IUser author, MessageSource source) : base(discord, id) @@ -86,6 +90,8 @@ namespace Discord.Rest } internal virtual void Update(Model model) { + Type = model.Type; + if (model.Timestamp.IsSpecified) _timestampTicks = model.Timestamp.Value.UtcTicks; @@ -164,8 +170,6 @@ namespace Discord.Rest /// public override string ToString() => Content; - /// - MessageType IMessage.Type => MessageType.Default; IUser IMessage.Author => Author; /// IReadOnlyCollection IMessage.Attachments => Attachments; @@ -173,6 +177,8 @@ namespace Discord.Rest IReadOnlyCollection IMessage.Embeds => Embeds; /// IReadOnlyCollection IMessage.MentionedUserIds => MentionedUsers.Select(x => x.Id).ToImmutableArray(); + /// + IReadOnlyCollection IMessage.Stickers => Stickers; /// public IReadOnlyDictionary Reactions => _reactions.ToDictionary(x => x.Emote, x => new ReactionMetadata { ReactionCount = x.Count, IsMe = x.Me }); diff --git a/src/Discord.Net.Rest/Entities/Messages/RestSystemMessage.cs b/src/Discord.Net.Rest/Entities/Messages/RestSystemMessage.cs index 89a651eb7..1c59d4f45 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestSystemMessage.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestSystemMessage.cs @@ -9,9 +9,6 @@ namespace Discord.Rest [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class RestSystemMessage : RestMessage, ISystemMessage { - /// - public MessageType Type { get; private set; } - internal RestSystemMessage(BaseDiscordClient discord, ulong id, IMessageChannel channel, IUser author) : base(discord, id, channel, author, MessageSource.System) { @@ -25,8 +22,6 @@ namespace Discord.Rest internal override void Update(Model model) { base.Update(model); - - Type = model.Type; } private string DebuggerDisplay => $"{Author}: {Content} ({Id}, {Type})"; diff --git a/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs b/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs index cf025aea1..aa6b44da6 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs @@ -21,6 +21,7 @@ namespace Discord.Rest private ImmutableArray _tags = ImmutableArray.Create(); private ImmutableArray _roleMentionIds = ImmutableArray.Create(); private ImmutableArray _userMentions = ImmutableArray.Create(); + private ImmutableArray _stickers = ImmutableArray.Create(); /// public override bool IsTTS => _isTTS; @@ -45,6 +46,8 @@ namespace Discord.Rest /// public override IReadOnlyCollection Tags => _tags; /// + public override IReadOnlyCollection Stickers => _stickers; + /// public IUserMessage ReferencedMessage => _referencedMessage; internal RestUserMessage(BaseDiscordClient discord, ulong id, IMessageChannel channel, IUser author, MessageSource source) @@ -132,6 +135,20 @@ namespace Discord.Rest IUser refMsgAuthor = MessageHelper.GetAuthor(Discord, guild, refMsg.Author.Value, refMsg.WebhookId.ToNullable()); _referencedMessage = RestUserMessage.Create(Discord, Channel, refMsgAuthor, refMsg); } + + if (model.Stickers.IsSpecified) + { + var value = model.Stickers.Value; + if (value.Length > 0) + { + var stickers = ImmutableArray.CreateBuilder(value.Length); + for (int i = 0; i < value.Length; i++) + stickers.Add(Sticker.Create(value[i])); + _stickers = stickers.ToImmutable(); + } + else + _stickers = ImmutableArray.Create(); + } } /// @@ -147,9 +164,6 @@ namespace Discord.Rest /// public Task UnpinAsync(RequestOptions options = null) => MessageHelper.UnpinAsync(this, Discord, options); - /// - public Task ModifySuppressionAsync(bool suppressEmbeds, RequestOptions options = null) - => MessageHelper.SuppressEmbedsAsync(this, Discord, suppressEmbeds, options); public string Resolve(int startIndex, TagHandling userHandling = TagHandling.Name, TagHandling channelHandling = TagHandling.Name, TagHandling roleHandling = TagHandling.Name, TagHandling everyoneHandling = TagHandling.Ignore, TagHandling emojiHandling = TagHandling.Name) diff --git a/src/Discord.Net.Rest/Entities/Messages/Sticker.cs b/src/Discord.Net.Rest/Entities/Messages/Sticker.cs new file mode 100644 index 000000000..5482bed74 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Messages/Sticker.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using System.Diagnostics; +using Model = Discord.API.Sticker; + +namespace Discord +{ + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class Sticker : ISticker + { + /// + public ulong Id { get; } + /// + public ulong PackId { get; } + /// + public string Name { get; } + /// + public string Description { get; } + /// + public IReadOnlyCollection Tags { get; } + /// + public string Asset { get; } + /// + public string PreviewAsset { get; } + /// + public StickerFormatType FormatType { get; } + + internal Sticker(ulong id, ulong packId, string name, string description, string[] tags, string asset, string previewAsset, StickerFormatType formatType) + { + Id = id; + PackId = packId; + Name = name; + Description = description; + Tags = tags.ToReadOnlyCollection(); + Asset = asset; + PreviewAsset = previewAsset; + FormatType = formatType; + } + internal static Sticker Create(Model model) + { + return new Sticker(model.Id, model.PackId, model.Name, model.Desription, + model.Tags.IsSpecified ? model.Tags.Value.Split(',') : new string[0], + model.Asset, model.PreviewAsset, model.FormatType); + } + + private string DebuggerDisplay => $"{Name} ({Id})"; + } +} diff --git a/src/Discord.Net.Rest/Entities/Roles/RoleHelper.cs b/src/Discord.Net.Rest/Entities/Roles/RoleHelper.cs index d570f078b..73ab7ca31 100644 --- a/src/Discord.Net.Rest/Entities/Roles/RoleHelper.cs +++ b/src/Discord.Net.Rest/Entities/Roles/RoleHelper.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading.Tasks; using Model = Discord.API.Role; using BulkParams = Discord.API.Rest.ModifyGuildRolesParams; @@ -24,7 +24,7 @@ namespace Discord.Rest Hoist = args.Hoist, Mentionable = args.Mentionable, Name = args.Name, - Permissions = args.Permissions.IsSpecified ? args.Permissions.Value.RawValue : Optional.Create() + Permissions = args.Permissions.IsSpecified ? args.Permissions.Value.RawValue.ToString() : Optional.Create() }; var model = await client.ApiClient.ModifyGuildRoleAsync(role.Guild.Id, role.Id, apiArgs, options).ConfigureAwait(false); diff --git a/src/Discord.Net.Rest/Entities/Teams/RestTeam.cs b/src/Discord.Net.Rest/Entities/Teams/RestTeam.cs index 2343f8b5d..43c9417cc 100644 --- a/src/Discord.Net.Rest/Entities/Teams/RestTeam.cs +++ b/src/Discord.Net.Rest/Entities/Teams/RestTeam.cs @@ -12,6 +12,8 @@ namespace Discord.Rest /// public IReadOnlyList TeamMembers { get; private set; } /// + public string Name { get; private set; } + /// public ulong OwnerUserId { get; private set; } private string _iconId; @@ -30,6 +32,7 @@ namespace Discord.Rest { if (model.Icon.IsSpecified) _iconId = model.Icon.Value; + Name = model.Name; OwnerUserId = model.OwnerUserId; TeamMembers = model.TeamMembers.Select(x => new RestTeamMember(Discord, x)).ToImmutableArray(); } diff --git a/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs b/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs index 4a6bc1025..6e6bbe09c 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs @@ -112,17 +112,29 @@ namespace Discord.Rest public Task KickAsync(string reason = null, RequestOptions options = null) => UserHelper.KickAsync(this, Discord, reason, options); /// + public Task AddRoleAsync(ulong roleId, RequestOptions options = null) + => AddRolesAsync(new[] { roleId }, options); + /// public Task AddRoleAsync(IRole role, RequestOptions options = null) - => AddRolesAsync(new[] { role }, options); + => AddRoleAsync(role.Id, options); + /// + public Task AddRolesAsync(IEnumerable roleIds, RequestOptions options = null) + => UserHelper.AddRolesAsync(this, Discord, roleIds, options); /// public Task AddRolesAsync(IEnumerable roles, RequestOptions options = null) - => UserHelper.AddRolesAsync(this, Discord, roles, options); + => AddRolesAsync(roles.Select(x => x.Id), options); + /// + public Task RemoveRoleAsync(ulong roleId, RequestOptions options = null) + => RemoveRolesAsync(new[] { roleId }, options); /// public Task RemoveRoleAsync(IRole role, RequestOptions options = null) - => RemoveRolesAsync(new[] { role }, options); + => RemoveRoleAsync(role.Id, options); + /// + public Task RemoveRolesAsync(IEnumerable roleIds, RequestOptions options = null) + => UserHelper.RemoveRolesAsync(this, Discord, roleIds, options); /// public Task RemoveRolesAsync(IEnumerable roles, RequestOptions options = null) - => UserHelper.RemoveRolesAsync(this, Discord, roles, options); + => RemoveRolesAsync(roles.Select(x => x.Id)); /// /// Resolving permissions requires the parent guild to be downloaded. diff --git a/src/Discord.Net.Rest/Entities/Users/RestUser.cs b/src/Discord.Net.Rest/Entities/Users/RestUser.cs index 131a4ec73..7bc1447fe 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestUser.cs @@ -79,13 +79,13 @@ namespace Discord.Rest } /// - /// Returns a direct message channel to this user, or create one if it does not already exist. + /// Creates a direct message channel to this user. /// /// The options to be used when sending the request. /// /// A task that represents the asynchronous get operation. The task result contains a rest DM channel where the user is the recipient. /// - public Task GetOrCreateDMChannelAsync(RequestOptions options = null) + public Task CreateDMChannelAsync(RequestOptions options = null) => UserHelper.CreateDMChannelAsync(this, Discord, options); /// @@ -107,7 +107,7 @@ namespace Discord.Rest //IUser /// - async Task IUser.GetOrCreateDMChannelAsync(RequestOptions options) - => await GetOrCreateDMChannelAsync(options).ConfigureAwait(false); + async Task IUser.CreateDMChannelAsync(RequestOptions options) + => await CreateDMChannelAsync(options).ConfigureAwait(false); } } diff --git a/src/Discord.Net.Rest/Entities/Users/RestWebhookUser.cs b/src/Discord.Net.Rest/Entities/Users/RestWebhookUser.cs index a06916c9b..2131fec93 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestWebhookUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestWebhookUser.cs @@ -59,27 +59,35 @@ namespace Discord.Rest /// ChannelPermissions IGuildUser.GetPermissions(IGuildChannel channel) => Permissions.ToChannelPerms(channel, GuildPermissions.Webhook.RawValue); /// - Task IGuildUser.KickAsync(string reason, RequestOptions options) => + Task IGuildUser.KickAsync(string reason, RequestOptions options) => throw new NotSupportedException("Webhook users cannot be kicked."); /// - Task IGuildUser.ModifyAsync(Action func, RequestOptions options) => + Task IGuildUser.ModifyAsync(Action func, RequestOptions options) => throw new NotSupportedException("Webhook users cannot be modified."); - /// - Task IGuildUser.AddRoleAsync(IRole role, RequestOptions options) => + Task IGuildUser.AddRoleAsync(ulong role, RequestOptions options) => throw new NotSupportedException("Roles are not supported on webhook users."); - /// - Task IGuildUser.AddRolesAsync(IEnumerable roles, RequestOptions options) => + Task IGuildUser.AddRoleAsync(IRole role, RequestOptions options) => throw new NotSupportedException("Roles are not supported on webhook users."); - /// - Task IGuildUser.RemoveRoleAsync(IRole role, RequestOptions options) => + Task IGuildUser.AddRolesAsync(IEnumerable roles, RequestOptions options) => + throw new NotSupportedException("Roles are not supported on webhook users."); + /// + Task IGuildUser.AddRolesAsync(IEnumerable roles, RequestOptions options) => + throw new NotSupportedException("Roles are not supported on webhook users."); + /// + Task IGuildUser.RemoveRoleAsync(ulong role, RequestOptions options) => + throw new NotSupportedException("Roles are not supported on webhook users."); + /// + Task IGuildUser.RemoveRoleAsync(IRole role, RequestOptions options) => + throw new NotSupportedException("Roles are not supported on webhook users."); + /// + Task IGuildUser.RemoveRolesAsync(IEnumerable roles, RequestOptions options) => throw new NotSupportedException("Roles are not supported on webhook users."); - /// - Task IGuildUser.RemoveRolesAsync(IEnumerable roles, RequestOptions options) => + Task IGuildUser.RemoveRolesAsync(IEnumerable roles, RequestOptions options) => throw new NotSupportedException("Roles are not supported on webhook users."); //IVoiceState diff --git a/src/Discord.Net.Rest/Entities/Users/UserHelper.cs b/src/Discord.Net.Rest/Entities/Users/UserHelper.cs index 58e8cd417..3a19fcfc1 100644 --- a/src/Discord.Net.Rest/Entities/Users/UserHelper.cs +++ b/src/Discord.Net.Rest/Entities/Users/UserHelper.cs @@ -73,16 +73,16 @@ namespace Discord.Rest return RestDMChannel.Create(client, await client.ApiClient.CreateDMChannelAsync(args, options).ConfigureAwait(false)); } - public static async Task AddRolesAsync(IGuildUser user, BaseDiscordClient client, IEnumerable roles, RequestOptions options) + public static async Task AddRolesAsync(IGuildUser user, BaseDiscordClient client, IEnumerable roleIds, RequestOptions options) { - foreach (var role in roles) - await client.ApiClient.AddRoleAsync(user.Guild.Id, user.Id, role.Id, options).ConfigureAwait(false); + foreach (var roleId in roleIds) + await client.ApiClient.AddRoleAsync(user.Guild.Id, user.Id, roleId, options).ConfigureAwait(false); } - public static async Task RemoveRolesAsync(IGuildUser user, BaseDiscordClient client, IEnumerable roles, RequestOptions options) + public static async Task RemoveRolesAsync(IGuildUser user, BaseDiscordClient client, IEnumerable roleIds, RequestOptions options) { - foreach (var role in roles) - await client.ApiClient.RemoveRoleAsync(user.Guild.Id, user.Id, role.Id, options).ConfigureAwait(false); + foreach (var roleId in roleIds) + await client.ApiClient.RemoveRoleAsync(user.Guild.Id, user.Id, roleId, options).ConfigureAwait(false); } } } diff --git a/src/Discord.Net.Rest/Net/Converters/DiscordContractResolver.cs b/src/Discord.Net.Rest/Net/Converters/DiscordContractResolver.cs index a1ed20c6f..931c0c4c9 100644 --- a/src/Discord.Net.Rest/Net/Converters/DiscordContractResolver.cs +++ b/src/Discord.Net.Rest/Net/Converters/DiscordContractResolver.cs @@ -73,8 +73,6 @@ namespace Discord.Net.Converters } //Enums - if (type == typeof(PermissionTarget)) - return PermissionTargetConverter.Instance; if (type == typeof(UserStatus)) return UserStatusConverter.Instance; if (type == typeof(EmbedType)) diff --git a/src/Discord.Net.Rest/Net/Converters/PermissionTargetConverter.cs b/src/Discord.Net.Rest/Net/Converters/PermissionTargetConverter.cs deleted file mode 100644 index de2e379d7..000000000 --- a/src/Discord.Net.Rest/Net/Converters/PermissionTargetConverter.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Newtonsoft.Json; -using System; - -namespace Discord.Net.Converters -{ - internal class PermissionTargetConverter : JsonConverter - { - public static readonly PermissionTargetConverter Instance = new PermissionTargetConverter(); - - public override bool CanConvert(Type objectType) => true; - public override bool CanRead => true; - public override bool CanWrite => true; - - /// Unknown permission target. - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - switch ((string)reader.Value) - { - case "member": - return PermissionTarget.User; - case "role": - return PermissionTarget.Role; - default: - throw new JsonSerializationException("Unknown permission target."); - } - } - - /// Invalid permission target. - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - switch ((PermissionTarget)value) - { - case PermissionTarget.User: - writer.WriteValue("member"); - break; - case PermissionTarget.Role: - writer.WriteValue("role"); - break; - default: - throw new JsonSerializationException("Invalid permission target."); - } - } - } -} diff --git a/src/Discord.Net.WebSocket/API/Gateway/IdentifyParams.cs b/src/Discord.Net.WebSocket/API/Gateway/IdentifyParams.cs index 92a494b71..bb54d4cdd 100644 --- a/src/Discord.Net.WebSocket/API/Gateway/IdentifyParams.cs +++ b/src/Discord.Net.WebSocket/API/Gateway/IdentifyParams.cs @@ -17,8 +17,6 @@ namespace Discord.API.Gateway public Optional ShardingParams { get; set; } [JsonProperty("presence")] public Optional Presence { get; set; } - [JsonProperty("guild_subscriptions")] - public Optional GuildSubscriptions { get; set; } [JsonProperty("intents")] public Optional Intents { get; set; } } diff --git a/src/Discord.Net.WebSocket/API/Gateway/Reaction.cs b/src/Discord.Net.WebSocket/API/Gateway/Reaction.cs index 62de456e2..0d17cbff8 100644 --- a/src/Discord.Net.WebSocket/API/Gateway/Reaction.cs +++ b/src/Discord.Net.WebSocket/API/Gateway/Reaction.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; namespace Discord.API.Gateway { @@ -10,7 +10,11 @@ namespace Discord.API.Gateway public ulong MessageId { get; set; } [JsonProperty("channel_id")] public ulong ChannelId { get; set; } + [JsonProperty("guild_id")] + public Optional GuildId { get; set; } [JsonProperty("emoji")] public Emoji Emoji { get; set; } + [JsonProperty("member")] + public Optional Member { get; set; } } } diff --git a/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs b/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs index 966aec7fa..fcaa793fc 100644 --- a/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs +++ b/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs @@ -124,11 +124,11 @@ namespace Discord.WebSocket /// /// - public event Func, ISocketMessageChannel, Task> MessageDeleted { + public event Func, Cacheable, Task> MessageDeleted { add { _messageDeletedEvent.Add(value); } remove { _messageDeletedEvent.Remove(value); } } - internal readonly AsyncEvent, ISocketMessageChannel, Task>> _messageDeletedEvent = new AsyncEvent, ISocketMessageChannel, Task>>(); + internal readonly AsyncEvent, Cacheable, Task>> _messageDeletedEvent = new AsyncEvent, Cacheable, Task>>(); /// Fired when multiple messages are bulk deleted. /// /// @@ -155,12 +155,12 @@ namespace Discord.WebSocket /// parameter. /// /// - public event Func>, ISocketMessageChannel, Task> MessagesBulkDeleted + public event Func>, Cacheable, Task> MessagesBulkDeleted { add { _messagesBulkDeletedEvent.Add(value); } remove { _messagesBulkDeletedEvent.Remove(value); } } - internal readonly AsyncEvent>, ISocketMessageChannel, Task>> _messagesBulkDeletedEvent = new AsyncEvent>, ISocketMessageChannel, Task>>(); + internal readonly AsyncEvent>, Cacheable, Task>> _messagesBulkDeletedEvent = new AsyncEvent>, Cacheable, Task>>(); /// Fired when a message is updated. /// /// @@ -217,23 +217,23 @@ namespace Discord.WebSocket /// /// - public event Func, ISocketMessageChannel, SocketReaction, Task> ReactionAdded { + public event Func, Cacheable, SocketReaction, Task> ReactionAdded { add { _reactionAddedEvent.Add(value); } remove { _reactionAddedEvent.Remove(value); } } - internal readonly AsyncEvent, ISocketMessageChannel, SocketReaction, Task>> _reactionAddedEvent = new AsyncEvent, ISocketMessageChannel, SocketReaction, Task>>(); + internal readonly AsyncEvent, Cacheable, SocketReaction, Task>> _reactionAddedEvent = new AsyncEvent, Cacheable, SocketReaction, Task>>(); /// Fired when a reaction is removed from a message. - public event Func, ISocketMessageChannel, SocketReaction, Task> ReactionRemoved { + public event Func, Cacheable, SocketReaction, Task> ReactionRemoved { add { _reactionRemovedEvent.Add(value); } remove { _reactionRemovedEvent.Remove(value); } } - internal readonly AsyncEvent, ISocketMessageChannel, SocketReaction, Task>> _reactionRemovedEvent = new AsyncEvent, ISocketMessageChannel, SocketReaction, Task>>(); + internal readonly AsyncEvent, Cacheable, SocketReaction, Task>> _reactionRemovedEvent = new AsyncEvent, Cacheable, SocketReaction, Task>>(); /// Fired when all reactions to a message are cleared. - public event Func, ISocketMessageChannel, Task> ReactionsCleared { + public event Func, Cacheable, Task> ReactionsCleared { add { _reactionsClearedEvent.Add(value); } remove { _reactionsClearedEvent.Remove(value); } } - internal readonly AsyncEvent, ISocketMessageChannel, Task>> _reactionsClearedEvent = new AsyncEvent, ISocketMessageChannel, Task>>(); + internal readonly AsyncEvent, Cacheable, Task>> _reactionsClearedEvent = new AsyncEvent, Cacheable, Task>>(); /// /// Fired when all reactions to a message with a specific emote are removed. /// @@ -250,12 +250,12 @@ namespace Discord.WebSocket /// The emoji that all reactions had and were removed will be passed into the parameter. /// /// - public event Func, ISocketMessageChannel, IEmote, Task> ReactionsRemovedForEmote + public event Func, Cacheable, IEmote, Task> ReactionsRemovedForEmote { add { _reactionsRemovedForEmoteEvent.Add(value); } remove { _reactionsRemovedForEmoteEvent.Remove(value); } } - internal readonly AsyncEvent, ISocketMessageChannel, IEmote, Task>> _reactionsRemovedForEmoteEvent = new AsyncEvent, ISocketMessageChannel, IEmote, Task>>(); + internal readonly AsyncEvent, Cacheable, IEmote, Task>> _reactionsRemovedForEmoteEvent = new AsyncEvent, Cacheable, IEmote, Task>>(); //Roles /// Fired when a role is created. @@ -347,11 +347,11 @@ namespace Discord.WebSocket } internal readonly AsyncEvent> _userUpdatedEvent = new AsyncEvent>(); /// Fired when a guild member is updated, or a member presence is updated. - public event Func GuildMemberUpdated { + public event Func, SocketGuildUser, Task> GuildMemberUpdated { add { _guildMemberUpdatedEvent.Add(value); } remove { _guildMemberUpdatedEvent.Remove(value); } } - internal readonly AsyncEvent> _guildMemberUpdatedEvent = new AsyncEvent>(); + internal readonly AsyncEvent, SocketGuildUser, Task>> _guildMemberUpdatedEvent = new AsyncEvent, SocketGuildUser, Task>>(); /// Fired when a user joins, leaves, or moves voice channels. public event Func UserVoiceStateUpdated { add { _userVoiceStateUpdatedEvent.Add(value); } @@ -372,11 +372,11 @@ namespace Discord.WebSocket } internal readonly AsyncEvent> _selfUpdatedEvent = new AsyncEvent>(); /// Fired when a user starts typing. - public event Func UserIsTyping { + public event Func, Cacheable, Task> UserIsTyping { add { _userIsTypingEvent.Add(value); } remove { _userIsTypingEvent.Remove(value); } } - internal readonly AsyncEvent> _userIsTypingEvent = new AsyncEvent>(); + internal readonly AsyncEvent, Cacheable, Task>> _userIsTypingEvent = new AsyncEvent, Cacheable, Task>>(); /// Fired when a user joins a group channel. public event Func RecipientAdded { add { _recipientAddedEvent.Add(value); } diff --git a/src/Discord.Net.WebSocket/BaseSocketClient.cs b/src/Discord.Net.WebSocket/BaseSocketClient.cs index 36e6c02a9..1cfe6c8bf 100644 --- a/src/Discord.Net.WebSocket/BaseSocketClient.cs +++ b/src/Discord.Net.WebSocket/BaseSocketClient.cs @@ -47,7 +47,7 @@ namespace Discord.WebSocket /// /// Gets the current logged-in user. /// - public new SocketSelfUser CurrentUser { get => base.CurrentUser as SocketSelfUser; protected set => base.CurrentUser = value; } + public virtual new SocketSelfUser CurrentUser { get => base.CurrentUser as SocketSelfUser; protected set => base.CurrentUser = value; } /// /// Gets a collection of guilds that the user is currently in. /// @@ -70,20 +70,11 @@ namespace Discord.WebSocket /// A read-only collection of private channels that the user currently partakes in. /// public abstract IReadOnlyCollection PrivateChannels { get; } - /// - /// Gets a collection of available voice regions. - /// - /// - /// A read-only collection of voice regions that the user has access to. - /// - [Obsolete("This property is obsolete, use the GetVoiceRegionsAsync method instead.")] - public abstract IReadOnlyCollection VoiceRegions { get; } internal BaseSocketClient(DiscordSocketConfig config, DiscordRestApiClient client) : base(config, client) => BaseConfig = config; private static DiscordSocketApiClient CreateApiClient(DiscordSocketConfig config) => new DiscordSocketApiClient(config.RestClientProvider, config.WebSocketProvider, DiscordRestConfig.UserAgent, - rateLimitPrecision: config.RateLimitPrecision, useSystemClock: config.UseSystemClock); /// @@ -164,16 +155,6 @@ namespace Discord.WebSocket /// public abstract SocketGuild GetGuild(ulong id); /// - /// Gets a voice region. - /// - /// The identifier of the voice region (e.g. eu-central ). - /// - /// A REST-based voice region associated with the identifier; null if the voice region is not - /// found. - /// - [Obsolete("This method is obsolete, use GetVoiceRegionAsync instead.")] - public abstract RestVoiceRegion GetVoiceRegion(string id); - /// /// Gets all voice regions. /// /// The options to be used when sending the request. @@ -327,10 +308,14 @@ namespace Discord.WebSocket => Task.FromResult(GetUser(username, discriminator)); /// - Task IDiscordClient.GetVoiceRegionAsync(string id, RequestOptions options) - => Task.FromResult(GetVoiceRegion(id)); + async Task IDiscordClient.GetVoiceRegionAsync(string id, RequestOptions options) + { + return await GetVoiceRegionAsync(id).ConfigureAwait(false); + } /// - Task> IDiscordClient.GetVoiceRegionsAsync(RequestOptions options) - => Task.FromResult>(VoiceRegions); + async Task> IDiscordClient.GetVoiceRegionsAsync(RequestOptions options) + { + return await GetVoiceRegionsAsync().ConfigureAwait(false); + } } } diff --git a/src/Discord.Net.WebSocket/ConnectionManager.cs b/src/Discord.Net.WebSocket/ConnectionManager.cs index 2237e2d1f..e444f359f 100644 --- a/src/Discord.Net.WebSocket/ConnectionManager.cs +++ b/src/Discord.Net.WebSocket/ConnectionManager.cs @@ -75,11 +75,6 @@ namespace Discord nextReconnectDelay = 1000; //Reset delay await _connectionPromise.Task.ConfigureAwait(false); } - catch (OperationCanceledException ex) - { - Cancel(); //In case this exception didn't come from another Error call - await DisconnectAsync(ex, !reconnectCancelToken.IsCancellationRequested).ConfigureAwait(false); - } catch (Exception ex) { Error(ex); //In case this exception didn't come from another Error call @@ -143,16 +138,7 @@ namespace Discord catch (OperationCanceledException) { } }); - try - { - await _onConnecting().ConfigureAwait(false); - } - catch (TaskCanceledException ex) - { - Exception innerEx = ex.InnerException ?? new OperationCanceledException("Failed to connect."); - Error(innerEx); - throw innerEx; - } + await _onConnecting().ConfigureAwait(false); await _logger.InfoAsync("Connected").ConfigureAwait(false); State = ConnectionState.Connected; diff --git a/src/Discord.Net.WebSocket/DiscordShardedClient.cs b/src/Discord.Net.WebSocket/DiscordShardedClient.cs index 4c94b14e8..e6d05b525 100644 --- a/src/Discord.Net.WebSocket/DiscordShardedClient.cs +++ b/src/Discord.Net.WebSocket/DiscordShardedClient.cs @@ -36,14 +36,13 @@ namespace Discord.WebSocket /// public override IReadOnlyCollection PrivateChannels => GetPrivateChannels().ToReadOnlyCollection(GetPrivateChannelCount); public IReadOnlyCollection Shards => _shards; - /// - [Obsolete("This property is obsolete, use the GetVoiceRegionsAsync method instead.")] - public override IReadOnlyCollection VoiceRegions => _shards[0].VoiceRegions; /// /// Provides access to a REST-only client with a shared state from this client. /// - public override DiscordSocketRestClient Rest => _shards[0].Rest; + public override DiscordSocketRestClient Rest => _shards?[0].Rest; + + public override SocketSelfUser CurrentUser { get => _shards?.FirstOrDefault(x => x.CurrentUser != null)?.CurrentUser; protected set => throw new InvalidOperationException(); } /// Creates a new REST/WebSocket Discord client. public DiscordShardedClient() : this(null, new DiscordSocketConfig()) { } @@ -91,8 +90,7 @@ namespace Discord.WebSocket } } private static API.DiscordSocketApiClient CreateApiClient(DiscordSocketConfig config) - => new API.DiscordSocketApiClient(config.RestClientProvider, config.WebSocketProvider, DiscordRestConfig.UserAgent, - rateLimitPrecision: config.RateLimitPrecision); + => new API.DiscordSocketApiClient(config.RestClientProvider, config.WebSocketProvider, DiscordRestConfig.UserAgent); internal async Task AcquireIdentifyLockAsync(int shardId, CancellationToken token) { @@ -264,11 +262,6 @@ namespace Discord.WebSocket return null; } - /// - [Obsolete("This method is obsolete, use GetVoiceRegionAsync instead.")] - public override RestVoiceRegion GetVoiceRegion(string id) - => _shards[0].GetVoiceRegion(id); - /// public override async ValueTask> GetVoiceRegionsAsync(RequestOptions options = null) { @@ -339,14 +332,6 @@ namespace Discord.WebSocket } return Task.Delay(0); }; - if (isPrimary) - { - client.Ready += () => - { - CurrentUser = client.CurrentUser; - return Task.Delay(0); - }; - } client.Connected += () => _shardConnectedEvent.InvokeAsync(client); client.Disconnected += (exception) => _shardDisconnectedEvent.InvokeAsync(exception, client); @@ -432,11 +417,15 @@ namespace Discord.WebSocket => Task.FromResult(GetUser(username, discriminator)); /// - Task> IDiscordClient.GetVoiceRegionsAsync(RequestOptions options) - => Task.FromResult>(VoiceRegions); + async Task> IDiscordClient.GetVoiceRegionsAsync(RequestOptions options) + { + return await GetVoiceRegionsAsync().ConfigureAwait(false); + } /// - Task IDiscordClient.GetVoiceRegionAsync(string id, RequestOptions options) - => Task.FromResult(GetVoiceRegion(id)); + async Task IDiscordClient.GetVoiceRegionAsync(string id, RequestOptions options) + { + return await GetVoiceRegionAsync(id).ConfigureAwait(false); + } internal override void Dispose(bool disposing) { diff --git a/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs b/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs index d14a314d6..65fd23d3f 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs @@ -40,9 +40,8 @@ namespace Discord.API public DiscordSocketApiClient(RestClientProvider restClientProvider, WebSocketProvider webSocketProvider, string userAgent, string url = null, RetryMode defaultRetryMode = RetryMode.AlwaysRetry, JsonSerializer serializer = null, - RateLimitPrecision rateLimitPrecision = RateLimitPrecision.Second, bool useSystemClock = true) - : base(restClientProvider, userAgent, defaultRetryMode, serializer, rateLimitPrecision, useSystemClock) + : base(restClientProvider, userAgent, defaultRetryMode, serializer, useSystemClock) { _gatewayUrl = url; if (url != null) @@ -189,9 +188,9 @@ namespace Discord.API catch { } if (ex is GatewayReconnectException) - await WebSocketClient.DisconnectAsync(4000); + await WebSocketClient.DisconnectAsync(4000).ConfigureAwait(false); else - await WebSocketClient.DisconnectAsync().ConfigureAwait(false); + await WebSocketClient.DisconnectAsync().ConfigureAwait(false); ConnectionState = ConnectionState.Disconnected; } @@ -216,7 +215,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, GatewayIntents? gatewayIntents = null, (UserStatus, bool, long?, GameModel)? presence = null, RequestOptions options = null) + public async Task SendIdentifyAsync(int largeThreshold = 100, int shardID = 0, int totalShards = 1, GatewayIntents gatewayIntents = GatewayIntents.AllUnprivileged, (UserStatus, bool, long?, GameModel)? presence = null, RequestOptions options = null) { options = RequestOptions.CreateOrClone(options); var props = new Dictionary @@ -234,10 +233,7 @@ namespace Discord.API options.BucketId = GatewayBucket.Get(GatewayBucketType.Identify).Id; - if (gatewayIntents.HasValue) - msg.Intents = (int)gatewayIntents.Value; - else - msg.GuildSubscriptions = guildSubscriptions; + msg.Intents = (int)gatewayIntents; if (presence.HasValue) { diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index b43db5d98..2cdff662c 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -43,8 +43,7 @@ namespace Discord.WebSocket private DateTimeOffset? _statusSince; private RestApplication _applicationInfo; private bool _isDisposed; - private bool _guildSubscriptions; - private GatewayIntents? _gatewayIntents; + private GatewayIntents _gatewayIntents; /// /// Provides access to a REST-only client with a shared state from this client. @@ -72,7 +71,6 @@ namespace Discord.WebSocket internal WebSocketProvider WebSocketProvider { get; private set; } internal bool AlwaysDownloadUsers { get; private set; } internal int? HandlerTimeout { get; private set; } - internal bool? ExclusiveBulkDelete { get; private set; } internal new DiscordSocketApiClient ApiClient => base.ApiClient as DiscordSocketApiClient; /// @@ -109,9 +107,6 @@ namespace Discord.WebSocket /// public IReadOnlyCollection GroupChannels => State.PrivateChannels.OfType().ToImmutableArray(); - /// - [Obsolete("This property is obsolete, use the GetVoiceRegionsAsync method instead.")] - public override IReadOnlyCollection VoiceRegions => GetVoiceRegionsAsync().GetAwaiter().GetResult(); /// /// Initializes a new REST/WebSocket-based Discord client. @@ -136,11 +131,9 @@ namespace Discord.WebSocket WebSocketProvider = config.WebSocketProvider; AlwaysDownloadUsers = config.AlwaysDownloadUsers; HandlerTimeout = config.HandlerTimeout; - ExclusiveBulkDelete = config.ExclusiveBulkDelete; State = new ClientState(0, 0); Rest = new DiscordSocketRestClient(config, ApiClient); _heartbeatTimes = new ConcurrentQueue(); - _guildSubscriptions = config.GuildSubscriptions; _gatewayIntents = config.GatewayIntents; _stateLock = new SemaphoreSlim(1, 1); @@ -182,8 +175,7 @@ namespace Discord.WebSocket _largeGuilds = new ConcurrentQueue(); } private static API.DiscordSocketApiClient CreateApiClient(DiscordSocketConfig config) - => new API.DiscordSocketApiClient(config.RestClientProvider, config.WebSocketProvider, DiscordRestConfig.UserAgent, config.GatewayHost, - rateLimitPrecision: config.RateLimitPrecision); + => new API.DiscordSocketApiClient(config.RestClientProvider, config.WebSocketProvider, DiscordRestConfig.UserAgent, config.GatewayHost); /// internal override void Dispose(bool disposing) { @@ -201,11 +193,6 @@ namespace Discord.WebSocket base.Dispose(disposing); } - /// - internal override async Task OnLoginAsync(TokenType tokenType, string token) - { - await Rest.OnLoginAsync(tokenType, token); - } /// internal override async Task OnLogoutAsync() { @@ -243,7 +230,7 @@ namespace Discord.WebSocket else { await _gatewayLogger.DebugAsync("Identifying").ConfigureAwait(false); - await ApiClient.SendIdentifyAsync(shardID: ShardId, totalShards: TotalShards, guildSubscriptions: _guildSubscriptions, gatewayIntents: _gatewayIntents, presence: BuildCurrentStatus()).ConfigureAwait(false); + await ApiClient.SendIdentifyAsync(shardID: ShardId, totalShards: TotalShards, gatewayIntents: _gatewayIntents, presence: BuildCurrentStatus()).ConfigureAwait(false); } } finally @@ -302,13 +289,51 @@ namespace Discord.WebSocket public override SocketChannel GetChannel(ulong id) => State.GetChannel(id); /// + /// Gets a generic channel from the cache or does a rest request if unavailable. + /// + /// + /// + /// var channel = await _client.GetChannelAsync(381889909113225237); + /// if (channel != null && channel is IMessageChannel msgChannel) + /// { + /// await msgChannel.SendMessageAsync($"{msgChannel} is created at {msgChannel.CreatedAt}"); + /// } + /// + /// + /// The snowflake identifier of the channel (e.g. `381889909113225237`). + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the channel associated + /// with the snowflake identifier; null when the channel cannot be found. + /// + public async ValueTask GetChannelAsync(ulong id, RequestOptions options = null) + => GetChannel(id) ?? (IChannel)await ClientHelper.GetChannelAsync(this, id, options).ConfigureAwait(false); + /// + /// Gets a user from the cache or does a rest request if unavailable. + /// + /// + /// + /// var user = await _client.GetUserAsync(168693960628371456); + /// if (user != null) + /// Console.WriteLine($"{user} is created at {user.CreatedAt}."; + /// + /// + /// The snowflake identifier of the user (e.g. `168693960628371456`). + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains the user associated with + /// the snowflake identifier; null if the user is not found. + /// + public async ValueTask GetUserAsync(ulong id, RequestOptions options = null) + => await ClientHelper.GetUserAsync(this, id, options).ConfigureAwait(false); + /// /// 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 void PurgeDMChannelCache() => RemoveDMChannels(); /// public override SocketUser GetUser(ulong id) @@ -322,12 +347,11 @@ namespace Discord.WebSocket public void PurgeUserCache() => State.PurgeUsers(); internal SocketGlobalUser GetOrCreateUser(ClientState state, Discord.API.User model) { - return state.GetOrAddUser(model.Id, x => - { - var user = SocketGlobalUser.Create(this, state, model); - user.GlobalUser.AddRef(); - return user; - }); + return state.GetOrAddUser(model.Id, x => SocketGlobalUser.Create(this, state, model)); + } + internal SocketUser GetOrCreateTemporaryUser(ClientState state, Discord.API.User model) + { + return state.GetUser(model.Id) ?? (SocketUser)SocketUnknownUser.Create(this, state, model); } internal SocketGlobalUser GetOrCreateSelfUser(ClientState state, Discord.API.User model) { @@ -335,18 +359,13 @@ namespace Discord.WebSocket { var user = SocketGlobalUser.Create(this, state, model); user.GlobalUser.AddRef(); - user.Presence = new SocketPresence(UserStatus.Online, null, null, null); + user.Presence = new SocketPresence(UserStatus.Online, null, null); return user; }); } internal void RemoveUser(ulong id) => State.RemoveUser(id); - /// - [Obsolete("This method is obsolete, use GetVoiceRegionAsync instead.")] - public override RestVoiceRegion GetVoiceRegion(string id) - => GetVoiceRegionAsync(id).GetAwaiter().GetResult(); - /// public override async ValueTask> GetVoiceRegionsAsync(RequestOptions options = null) { @@ -469,7 +488,8 @@ namespace Discord.WebSocket { if (CurrentUser == null) return; - CurrentUser.Presence = new SocketPresence(Status, Activity, null, null); + var activities = _activity.IsSpecified ? ImmutableList.Create(_activity.Value) : null; + CurrentUser.Presence = new SocketPresence(Status, null, activities); var presence = BuildCurrentStatus() ?? (UserStatus.Online, false, null, null); @@ -564,7 +584,7 @@ namespace Discord.WebSocket await _shardedClient.AcquireIdentifyLockAsync(ShardId, _connection.CancelToken).ConfigureAwait(false); try { - await ApiClient.SendIdentifyAsync(shardID: ShardId, totalShards: TotalShards, guildSubscriptions: _guildSubscriptions, gatewayIntents: _gatewayIntents, presence: BuildCurrentStatus()).ConfigureAwait(false); + await ApiClient.SendIdentifyAsync(shardID: ShardId, totalShards: TotalShards, gatewayIntents: _gatewayIntents, presence: BuildCurrentStatus()).ConfigureAwait(false); } finally { @@ -572,7 +592,7 @@ namespace Discord.WebSocket } } else - await ApiClient.SendIdentifyAsync(shardID: ShardId, totalShards: TotalShards, guildSubscriptions: _guildSubscriptions, gatewayIntents: _gatewayIntents, presence: BuildCurrentStatus()).ConfigureAwait(false); + await ApiClient.SendIdentifyAsync(shardID: ShardId, totalShards: TotalShards, gatewayIntents: _gatewayIntents, presence: BuildCurrentStatus()).ConfigureAwait(false); } break; case GatewayOpCode.Reconnect: @@ -595,8 +615,10 @@ namespace Discord.WebSocket var state = new ClientState(data.Guilds.Length, data.PrivateChannels.Length); var currentUser = SocketSelfUser.Create(this, state, data.User); - currentUser.Presence = new SocketPresence(Status, Activity, null, null); + var activities = _activity.IsSpecified ? ImmutableList.Create(_activity.Value) : null; + currentUser.Presence = new SocketPresence(Status, null, activities); ApiClient.CurrentUserId = currentUser.Id; + Rest.CurrentUser = RestSelfUser.Create(this, data.User); int unavailableGuilds = 0; for (int i = 0; i < data.Guilds.Length; i++) { @@ -749,7 +771,8 @@ namespace Discord.WebSocket break; case "GUILD_SYNC": { - await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_SYNC)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync("Ignored Dispatch (GUILD_SYNC)").ConfigureAwait(false); + /*await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_SYNC)").ConfigureAwait(false); //TODO remove? userbot related var data = (payload as JToken).ToObject(_serializer); var guild = State.GetGuild(data.Id); if (guild != null) @@ -766,7 +789,7 @@ namespace Discord.WebSocket { await UnknownGuildAsync(type, data.Id).ConfigureAwait(false); return; - } + }*/ } break; case "GUILD_DELETE": @@ -966,15 +989,15 @@ namespace Discord.WebSocket var before = user.Clone(); user.Update(State, data); - await TimedInvokeAsync(_guildMemberUpdatedEvent, nameof(GuildMemberUpdated), before, user).ConfigureAwait(false); + + var cacheableBefore = new Cacheable(before, user.Id, true, () => Task.FromResult((SocketGuildUser)null)); + await TimedInvokeAsync(_guildMemberUpdatedEvent, nameof(GuildMemberUpdated), cacheableBefore, user).ConfigureAwait(false); } else { - if (!guild.HasAllMembers) - await IncompleteGuildUserAsync(type, data.User.Id, data.GuildId).ConfigureAwait(false); - else - await UnknownGuildUserAsync(type, data.User.Id, data.GuildId).ConfigureAwait(false); - return; + user = guild.AddOrUpdateUser(data); + var cacheableBefore = new Cacheable(null, user.Id, false, () => Task.FromResult((SocketGuildUser)null)); + await TimedInvokeAsync(_guildMemberUpdatedEvent, nameof(GuildMemberUpdated), cacheableBefore, user).ConfigureAwait(false); } } else @@ -1237,56 +1260,63 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_CREATE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - if (State.GetChannel(data.ChannelId) is ISocketMessageChannel channel) + var channel = GetChannel(data.ChannelId) as ISocketMessageChannel; + + var guild = (channel as SocketGuildChannel)?.Guild; + if (guild != null && !guild.IsSynced) { - var guild = (channel as SocketGuildChannel)?.Guild; - if (guild != null && !guild.IsSynced) + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } + + if (channel == null) + { + if (!data.GuildId.IsSpecified) // assume it is a DM { - await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); - return; + channel = CreateDMChannel(data.ChannelId, data.Author.Value, State); } - - SocketUser author; - if (guild != null) + else { - if (data.WebhookId.IsSpecified) - author = SocketWebhookUser.Create(guild, State, data.Author.Value, data.WebhookId.Value); - else - author = guild.GetUser(data.Author.Value.Id); + await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); + return; } + } + + SocketUser author; + if (guild != null) + { + if (data.WebhookId.IsSpecified) + author = SocketWebhookUser.Create(guild, State, data.Author.Value, data.WebhookId.Value); else - author = (channel as SocketChannel).GetUser(data.Author.Value.Id); + author = guild.GetUser(data.Author.Value.Id); + } + else + author = (channel as SocketChannel).GetUser(data.Author.Value.Id); - if (author == null) + if (author == null) + { + if (guild != null) { - if (guild != null) + if (data.Member.IsSpecified) // member isn't always included, but use it when we can { - if (data.Member.IsSpecified) // member isn't always included, but use it when we can - { - data.Member.Value.User = data.Author.Value; - author = guild.AddOrUpdateUser(data.Member.Value); - } - else - author = guild.AddOrUpdateUser(data.Author.Value); // user has no guild-specific data + data.Member.Value.User = data.Author.Value; + author = guild.AddOrUpdateUser(data.Member.Value); } - else if (channel is SocketGroupChannel) - author = (channel as SocketGroupChannel).GetOrAddUser(data.Author.Value); else - { - await UnknownChannelUserAsync(type, data.Author.Value.Id, channel.Id).ConfigureAwait(false); - return; - } + author = guild.AddOrUpdateUser(data.Author.Value); // user has no guild-specific data + } + else if (channel is SocketGroupChannel groupChannel) + author = groupChannel.GetOrAddUser(data.Author.Value); + else + { + await UnknownChannelUserAsync(type, data.Author.Value.Id, channel.Id).ConfigureAwait(false); + return; } - - var msg = SocketMessage.Create(this, State, author, channel, data); - SocketChannelHelper.AddMessage(channel, this, msg); - await TimedInvokeAsync(_messageReceivedEvent, nameof(MessageReceived), msg).ConfigureAwait(false); - } - else - { - await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); - return; } + + var msg = SocketMessage.Create(this, State, author, channel, data); + SocketChannelHelper.AddMessage(channel, this, msg); + await TimedInvokeAsync(_messageReceivedEvent, nameof(MessageReceived), msg).ConfigureAwait(false); } break; case "MESSAGE_UPDATE": @@ -1294,52 +1324,85 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_UPDATE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - if (State.GetChannel(data.ChannelId) is ISocketMessageChannel channel) + var channel = GetChannel(data.ChannelId) as ISocketMessageChannel; + + var guild = (channel as SocketGuildChannel)?.Guild; + if (guild != null && !guild.IsSynced) { - var guild = (channel as SocketGuildChannel)?.Guild; - if (guild != null && !guild.IsSynced) - { - await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); - return; - } + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } - SocketMessage before = null, after = null; - SocketMessage cachedMsg = channel.GetCachedMessage(data.Id); - bool isCached = cachedMsg != null; - if (isCached) + SocketMessage before = null, after = null; + SocketMessage cachedMsg = channel?.GetCachedMessage(data.Id); + bool isCached = cachedMsg != null; + if (isCached) + { + before = cachedMsg.Clone(); + cachedMsg.Update(State, data); + after = cachedMsg; + } + else + { + //Edited message isnt in cache, create a detached one + SocketUser author; + if (data.Author.IsSpecified) { - before = cachedMsg.Clone(); - cachedMsg.Update(State, data); - after = cachedMsg; + if (guild != null) + { + if (data.WebhookId.IsSpecified) + author = SocketWebhookUser.Create(guild, State, data.Author.Value, data.WebhookId.Value); + else + author = guild.GetUser(data.Author.Value.Id); + } + else + author = (channel as SocketChannel)?.GetUser(data.Author.Value.Id); + + if (author == null) + { + if (guild != null) + { + if (data.Member.IsSpecified) // member isn't always included, but use it when we can + { + data.Member.Value.User = data.Author.Value; + author = guild.AddOrUpdateUser(data.Member.Value); + } + else + author = guild.AddOrUpdateUser(data.Author.Value); // user has no guild-specific data + } + else if (channel is SocketGroupChannel groupChannel) + author = groupChannel.GetOrAddUser(data.Author.Value); + } } else + // Message author wasn't specified in the payload, so create a completely anonymous unknown user + author = new SocketUnknownUser(this, id: 0); + + if (channel == null) { - //Edited message isnt in cache, create a detached one - SocketUser author; - if (data.Author.IsSpecified) + if (!data.GuildId.IsSpecified) // assume it is a DM { - if (guild != null) - author = guild.GetUser(data.Author.Value.Id); + if (data.Author.IsSpecified) + { + var dmChannel = CreateDMChannel(data.ChannelId, data.Author.Value, State); + channel = dmChannel; + author = dmChannel.Recipient; + } else - author = (channel as SocketChannel).GetUser(data.Author.Value.Id); - if (author == null) - author = SocketUnknownUser.Create(this, State, data.Author.Value); + channel = CreateDMChannel(data.ChannelId, author, State); } else - // Message author wasn't specified in the payload, so create a completely anonymous unknown user - author = new SocketUnknownUser(this, id: 0); - - after = SocketMessage.Create(this, State, author, channel, data); + { + await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); + return; + } } - var cacheableBefore = new Cacheable(before, data.Id, isCached, async () => await channel.GetMessageAsync(data.Id).ConfigureAwait(false)); - await TimedInvokeAsync(_messageUpdatedEvent, nameof(MessageUpdated), cacheableBefore, after, channel).ConfigureAwait(false); - } - else - { - await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); - return; + after = SocketMessage.Create(this, State, author, channel, data); } + var cacheableBefore = new Cacheable(before, data.Id, isCached, async () => await channel.GetMessageAsync(data.Id).ConfigureAwait(false)); + + await TimedInvokeAsync(_messageUpdatedEvent, nameof(MessageUpdated), cacheableBefore, after, channel).ConfigureAwait(false); } break; case "MESSAGE_DELETE": @@ -1347,26 +1410,22 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_DELETE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - if (State.GetChannel(data.ChannelId) is ISocketMessageChannel channel) - { - var guild = (channel as SocketGuildChannel)?.Guild; - if (!(guild?.IsSynced ?? true)) - { - await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); - return; - } - - var msg = SocketChannelHelper.RemoveMessage(channel, this, data.Id); - bool isCached = msg != null; - var cacheable = new Cacheable(msg, data.Id, isCached, async () => await channel.GetMessageAsync(data.Id).ConfigureAwait(false)); + var channel = GetChannel(data.ChannelId) as ISocketMessageChannel; - await TimedInvokeAsync(_messageDeletedEvent, nameof(MessageDeleted), cacheable, channel).ConfigureAwait(false); - } - else + var guild = (channel as SocketGuildChannel)?.Guild; + if (!(guild?.IsSynced ?? true)) { - await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); return; } + + SocketMessage msg = null; + if (channel != null) + msg = SocketChannelHelper.RemoveMessage(channel, this, data.Id); + var cacheableMsg = new Cacheable(msg, data.Id, msg != null, () => Task.FromResult((IMessage)null)); + var cacheableChannel = new Cacheable(channel, data.ChannelId, channel != null, async () => await GetChannelAsync(data.ChannelId).ConfigureAwait(false) as IMessageChannel); + + await TimedInvokeAsync(_messageDeletedEvent, nameof(MessageDeleted), cacheableMsg, cacheableChannel).ConfigureAwait(false); } break; case "MESSAGE_REACTION_ADD": @@ -1374,32 +1433,43 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_REACTION_ADD)").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 user = await channel.GetUserAsync(data.UserId, CacheMode.CacheOnly).ConfigureAwait(false); + var channel = GetChannel(data.ChannelId) as ISocketMessageChannel; - var optionalMsg = !isCached - ? Optional.Create() - : Optional.Create(cachedMsg); - - var optionalUser = user is null - ? Optional.Create() - : Optional.Create(user); + var cachedMsg = channel?.GetCachedMessage(data.MessageId) as SocketUserMessage; + bool isMsgCached = cachedMsg != null; + IUser user = null; + if (channel != null) + user = await channel.GetUserAsync(data.UserId, CacheMode.CacheOnly).ConfigureAwait(false); - var reaction = SocketReaction.Create(data, channel, optionalMsg, optionalUser); - var cacheable = new Cacheable(cachedMsg, data.MessageId, isCached, async () => await channel.GetMessageAsync(data.MessageId).ConfigureAwait(false) as IUserMessage); + var optionalMsg = !isMsgCached + ? Optional.Create() + : Optional.Create(cachedMsg); - cachedMsg?.AddReaction(reaction); + if (data.Member.IsSpecified) + { + var guild = (channel as SocketGuildChannel)?.Guild; - await TimedInvokeAsync(_reactionAddedEvent, nameof(ReactionAdded), cacheable, channel, reaction).ConfigureAwait(false); + if (guild != null) + user = guild.AddOrUpdateUser(data.Member.Value); } else + user = GetUser(data.UserId); + + var optionalUser = user is null + ? Optional.Create() + : Optional.Create(user); + + var cacheableChannel = new Cacheable(channel, data.ChannelId, channel != null, async () => await GetChannelAsync(data.ChannelId).ConfigureAwait(false) as IMessageChannel); + var cacheableMsg = new Cacheable(cachedMsg, data.MessageId, isMsgCached, async () => { - await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); - return; - } + var channelObj = await cacheableChannel.GetOrDownloadAsync().ConfigureAwait(false); + return await channelObj.GetMessageAsync(data.MessageId).ConfigureAwait(false) as IUserMessage; + }); + var reaction = SocketReaction.Create(data, channel, optionalMsg, optionalUser); + + cachedMsg?.AddReaction(reaction); + + await TimedInvokeAsync(_reactionAddedEvent, nameof(ReactionAdded), cacheableMsg, cacheableChannel, reaction).ConfigureAwait(false); } break; case "MESSAGE_REACTION_REMOVE": @@ -1407,32 +1477,35 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_REACTION_REMOVE)").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 user = await channel.GetUserAsync(data.UserId, CacheMode.CacheOnly).ConfigureAwait(false); - - var optionalMsg = !isCached - ? Optional.Create() - : Optional.Create(cachedMsg); + var channel = GetChannel(data.ChannelId) as ISocketMessageChannel; - var optionalUser = user is null - ? Optional.Create() - : Optional.Create(user); + var cachedMsg = channel?.GetCachedMessage(data.MessageId) as SocketUserMessage; + bool isMsgCached = cachedMsg != null; + IUser user = null; + if (channel != null) + user = await channel.GetUserAsync(data.UserId, CacheMode.CacheOnly).ConfigureAwait(false); + else if (!data.GuildId.IsSpecified) + user = GetUser(data.UserId); - var reaction = SocketReaction.Create(data, channel, optionalMsg, optionalUser); - var cacheable = new Cacheable(cachedMsg, data.MessageId, isCached, async () => await channel.GetMessageAsync(data.MessageId).ConfigureAwait(false) as IUserMessage); + var optionalMsg = !isMsgCached + ? Optional.Create() + : Optional.Create(cachedMsg); - cachedMsg?.RemoveReaction(reaction); + var optionalUser = user is null + ? Optional.Create() + : Optional.Create(user); - await TimedInvokeAsync(_reactionRemovedEvent, nameof(ReactionRemoved), cacheable, channel, reaction).ConfigureAwait(false); - } - else + var cacheableChannel = new Cacheable(channel, data.ChannelId, channel != null, async () => await GetChannelAsync(data.ChannelId).ConfigureAwait(false) as IMessageChannel); + var cacheableMsg = new Cacheable(cachedMsg, data.MessageId, isMsgCached, async () => { - await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); - return; - } + var channelObj = await cacheableChannel.GetOrDownloadAsync().ConfigureAwait(false); + return await channelObj.GetMessageAsync(data.MessageId).ConfigureAwait(false) as IUserMessage; + }); + var reaction = SocketReaction.Create(data, channel, optionalMsg, optionalUser); + + cachedMsg?.RemoveReaction(reaction); + + await TimedInvokeAsync(_reactionRemovedEvent, nameof(ReactionRemoved), cacheableMsg, cacheableChannel, reaction).ConfigureAwait(false); } break; case "MESSAGE_REACTION_REMOVE_ALL": @@ -1440,21 +1513,20 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_REACTION_REMOVE_ALL)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - if (State.GetChannel(data.ChannelId) is ISocketMessageChannel channel) + var channel = GetChannel(data.ChannelId) as ISocketMessageChannel; + + var cacheableChannel = new Cacheable(channel, data.ChannelId, channel != null, async () => await GetChannelAsync(data.ChannelId).ConfigureAwait(false) as IMessageChannel); + var cachedMsg = channel?.GetCachedMessage(data.MessageId) as SocketUserMessage; + bool isMsgCached = cachedMsg != null; + var cacheableMsg = new Cacheable(cachedMsg, data.MessageId, isMsgCached, async () => { - var cachedMsg = channel.GetCachedMessage(data.MessageId) as SocketUserMessage; - bool isCached = cachedMsg != null; - var cacheable = new Cacheable(cachedMsg, data.MessageId, isCached, async () => (await channel.GetMessageAsync(data.MessageId).ConfigureAwait(false)) as IUserMessage); + var channelObj = await cacheableChannel.GetOrDownloadAsync().ConfigureAwait(false); + return await channelObj.GetMessageAsync(data.MessageId).ConfigureAwait(false) as IUserMessage; + }); - cachedMsg?.ClearReactions(); + cachedMsg?.ClearReactions(); - await TimedInvokeAsync(_reactionsClearedEvent, nameof(ReactionsCleared), cacheable, channel).ConfigureAwait(false); - } - else - { - await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); - return; - } + await TimedInvokeAsync(_reactionsClearedEvent, nameof(ReactionsCleared), cacheableMsg, cacheableChannel).ConfigureAwait(false); } break; case "MESSAGE_REACTION_REMOVE_EMOJI": @@ -1462,70 +1534,55 @@ namespace Discord.WebSocket 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 channel = GetChannel(data.ChannelId) as ISocketMessageChannel; - var cacheable = new Cacheable(cachedMsg, data.MessageId, isCached, async () => await channel.GetMessageAsync(data.MessageId).ConfigureAwait(false) as IUserMessage); - var emote = data.Emoji.ToIEmote(); + var cachedMsg = channel?.GetCachedMessage(data.MessageId) as SocketUserMessage; + bool isMsgCached = cachedMsg != null; - cachedMsg?.RemoveAllReactionsForEmoteAsync(emote); + var optionalMsg = !isMsgCached + ? Optional.Create() + : Optional.Create(cachedMsg); - await TimedInvokeAsync(_reactionsRemovedForEmoteEvent, nameof(ReactionsRemovedForEmote), cacheable, channel, emote).ConfigureAwait(false); - } - else + var cacheableChannel = new Cacheable(channel, data.ChannelId, channel != null, async () => await GetChannelAsync(data.ChannelId).ConfigureAwait(false) as IMessageChannel); + var cacheableMsg = new Cacheable(cachedMsg, data.MessageId, isMsgCached, async () => { - await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); - return; - } + var channelObj = await cacheableChannel.GetOrDownloadAsync().ConfigureAwait(false); + return await channelObj.GetMessageAsync(data.MessageId).ConfigureAwait(false) as IUserMessage; + }); + var emote = data.Emoji.ToIEmote(); + + cachedMsg?.RemoveReactionsForEmote(emote); + + await TimedInvokeAsync(_reactionsRemovedForEmoteEvent, nameof(ReactionsRemovedForEmote), cacheableMsg, cacheableChannel, emote).ConfigureAwait(false); } break; case "MESSAGE_DELETE_BULK": { await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_DELETE_BULK)").ConfigureAwait(false); - if (!ExclusiveBulkDelete.HasValue) - { - await _gatewayLogger.WarningAsync("A bulk delete event has been received, but the event handling behavior has not been set. " + - "To suppress this message, set the ExclusiveBulkDelete configuration property. " + - "This message will appear only once."); - ExclusiveBulkDelete = false; - } - var data = (payload as JToken).ToObject(_serializer); - if (State.GetChannel(data.ChannelId) is ISocketMessageChannel channel) - { - var guild = (channel as SocketGuildChannel)?.Guild; - if (!(guild?.IsSynced ?? true)) - { - await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); - return; - } - - var cacheableList = new List>(data.Ids.Length); - foreach (ulong id in data.Ids) - { - var msg = SocketChannelHelper.RemoveMessage(channel, this, id); - bool isCached = msg != null; - var cacheable = new Cacheable(msg, id, isCached, async () => await channel.GetMessageAsync(id).ConfigureAwait(false)); - cacheableList.Add(cacheable); + var channel = GetChannel(data.ChannelId) as ISocketMessageChannel; - if (!ExclusiveBulkDelete ?? false) // this shouldn't happen, but we'll play it safe anyways - await TimedInvokeAsync(_messageDeletedEvent, nameof(MessageDeleted), cacheable, channel).ConfigureAwait(false); - } - - await TimedInvokeAsync(_messagesBulkDeletedEvent, nameof(MessagesBulkDeleted), cacheableList, channel).ConfigureAwait(false); - } - else + var guild = (channel as SocketGuildChannel)?.Guild; + if (!(guild?.IsSynced ?? true)) { - await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); return; } + + var cacheableChannel = new Cacheable(channel, data.ChannelId, channel != null, async () => await GetChannelAsync(data.ChannelId).ConfigureAwait(false) as IMessageChannel); + var cacheableList = new List>(data.Ids.Length); + foreach (ulong id in data.Ids) + { + SocketMessage msg = null; + if (channel != null) + msg = SocketChannelHelper.RemoveMessage(channel, this, id); + bool isMsgCached = msg != null; + var cacheableMsg = new Cacheable(msg, id, isMsgCached, () => Task.FromResult((IMessage)null)); + cacheableList.Add(cacheableMsg); + } + + await TimedInvokeAsync(_messagesBulkDeletedEvent, nameof(MessagesBulkDeleted), cacheableList, cacheableChannel).ConfigureAwait(false); } break; @@ -1571,7 +1628,8 @@ namespace Discord.WebSocket var before = user.Clone(); user.Update(State, data, true); - await TimedInvokeAsync(_guildMemberUpdatedEvent, nameof(GuildMemberUpdated), before, user).ConfigureAwait(false); + var cacheableBefore = new Cacheable(before, user.Id, true, () => Task.FromResult(user)); + await TimedInvokeAsync(_guildMemberUpdatedEvent, nameof(GuildMemberUpdated), cacheableBefore, user).ConfigureAwait(false); } else { @@ -1594,24 +1652,26 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (TYPING_START)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - if (State.GetChannel(data.ChannelId) is ISocketMessageChannel channel) + var channel = GetChannel(data.ChannelId) as ISocketMessageChannel; + + var guild = (channel as SocketGuildChannel)?.Guild; + if (!(guild?.IsSynced ?? true)) { - var guild = (channel as SocketGuildChannel)?.Guild; - if (!(guild?.IsSynced ?? true)) - { - await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); - return; - } + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } - var user = (channel as SocketChannel).GetUser(data.UserId); - if (user == null) - { - if (guild != null) - user = guild.AddOrUpdateUser(data.Member); - } - if (user != null) - await TimedInvokeAsync(_userIsTypingEvent, nameof(UserIsTyping), user, channel).ConfigureAwait(false); + var cacheableChannel = new Cacheable(channel, data.ChannelId, channel != null, async () => await GetChannelAsync(data.ChannelId).ConfigureAwait(false) as IMessageChannel); + + var user = (channel as SocketChannel)?.GetUser(data.UserId); + if (user == null) + { + if (guild != null) + user = guild.AddOrUpdateUser(data.Member); } + var cacheableUser = new Cacheable(user, data.UserId, user != null, async () => await GetUserAsync(data.UserId).ConfigureAwait(false)); + + await TimedInvokeAsync(_userIsTypingEvent, nameof(UserIsTyping), cacheableUser, cacheableChannel).ConfigureAwait(false); } break; @@ -1683,7 +1743,7 @@ namespace Discord.WebSocket } else { - var groupChannel = State.GetChannel(data.ChannelId.Value) as SocketGroupChannel; + var groupChannel = GetChannel(data.ChannelId.Value) as SocketGroupChannel; if (groupChannel == null) { await UnknownChannelAsync(type, data.ChannelId.Value).ConfigureAwait(false); @@ -1930,24 +1990,33 @@ namespace Discord.WebSocket { var channel = SocketChannel.CreatePrivate(this, state, model); state.AddChannel(channel as SocketChannel); - if (channel is SocketDMChannel dm) - dm.Recipient.GlobalUser.DMChannel = dm; - return channel; } + internal SocketDMChannel CreateDMChannel(ulong channelId, API.User model, ClientState state) + { + return SocketDMChannel.Create(this, state, channelId, model); + } + internal SocketDMChannel CreateDMChannel(ulong channelId, SocketUser user, ClientState state) + { + return new SocketDMChannel(this, channelId, user); + } internal ISocketPrivateChannel RemovePrivateChannel(ulong id) { var channel = State.RemoveChannel(id) as ISocketPrivateChannel; if (channel != null) { - if (channel is SocketDMChannel dmChannel) - dmChannel.Recipient.GlobalUser.DMChannel = null; - foreach (var recipient in channel.Recipients) recipient.GlobalUser.RemoveRef(this); } return channel; } + internal void RemoveDMChannels() + { + var channels = State.DMChannels; + State.PurgeDMChannels(); + foreach (var channel in channels) + channel.Recipient.GlobalUser.RemoveRef(this); + } private async Task GuildAvailableAsync(SocketGuild guild) { @@ -2103,8 +2172,8 @@ namespace Discord.WebSocket => await GetApplicationInfoAsync().ConfigureAwait(false); /// - Task IDiscordClient.GetChannelAsync(ulong id, CacheMode mode, RequestOptions options) - => Task.FromResult(GetChannel(id)); + async Task IDiscordClient.GetChannelAsync(ulong id, CacheMode mode, RequestOptions options) + => mode == CacheMode.AllowDownload ? await GetChannelAsync(id, options).ConfigureAwait(false) : GetChannel(id); /// Task> IDiscordClient.GetPrivateChannelsAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(PrivateChannels); @@ -2134,8 +2203,8 @@ namespace Discord.WebSocket => await CreateGuildAsync(name, region, jpegIcon).ConfigureAwait(false); /// - Task IDiscordClient.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) - => Task.FromResult(GetUser(id)); + async Task IDiscordClient.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) + => mode == CacheMode.AllowDownload ? await GetUserAsync(id, options).ConfigureAwait(false) : GetUser(id); /// Task IDiscordClient.GetUserAsync(string username, string discriminator, RequestOptions options) => Task.FromResult(GetUser(username, discriminator)); diff --git a/src/Discord.Net.WebSocket/DiscordSocketConfig.cs b/src/Discord.Net.WebSocket/DiscordSocketConfig.cs index a45d4f5be..22a201c67 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketConfig.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketConfig.cs @@ -111,28 +111,6 @@ namespace Discord.WebSocket /// public int? HandlerTimeout { get; set; } = 3000; - /// - /// Gets or sets the behavior for on bulk deletes. - /// - /// - /// - /// If true, the event will not be raised for bulk - /// deletes, and only the will be raised. If false - /// , both events will be raised. - /// - /// - /// If unset, both events will be raised, but a warning will be raised the first time a bulk delete event is - /// received. - /// - /// - public bool? ExclusiveBulkDelete { get; set; } = null; - - /// - /// 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 identify concurrency. /// @@ -172,14 +150,15 @@ namespace Discord.WebSocket 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. + /// Gets or sets gateway intents to limit what events are sent from Discord. + /// The default is . /// /// /// For more information, please see /// GatewayIntents /// on the official Discord API documentation. /// - public GatewayIntents? GatewayIntents { get; set; } + public GatewayIntents GatewayIntents { get; set; } = GatewayIntents.AllUnprivileged; /// /// Initializes a new instance of the class with the default configuration. diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs index c123a9d4e..459707dc7 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs @@ -16,32 +16,27 @@ namespace Discord.WebSocket [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class SocketDMChannel : SocketChannel, IDMChannel, ISocketPrivateChannel, ISocketMessageChannel { - private readonly MessageCache _messages; - /// /// Gets the recipient of the channel. /// public SocketUser Recipient { get; } /// - public IReadOnlyCollection CachedMessages => _messages?.Messages ?? ImmutableArray.Create(); + public IReadOnlyCollection CachedMessages => ImmutableArray.Create(); /// /// Gets a collection that is the current logged-in user and the recipient. /// public new IReadOnlyCollection Users => ImmutableArray.Create(Discord.CurrentUser, Recipient); - internal SocketDMChannel(DiscordSocketClient discord, ulong id, SocketGlobalUser recipient) + internal SocketDMChannel(DiscordSocketClient discord, ulong id, SocketUser recipient) : base(discord, id) { Recipient = recipient; - recipient.GlobalUser.AddRef(); - if (Discord.MessageCacheSize > 0) - _messages = new MessageCache(Discord); } internal static SocketDMChannel Create(DiscordSocketClient discord, ClientState state, Model model) { - var entity = new SocketDMChannel(discord, model.Id, discord.GetOrCreateUser(state, model.Recipients.Value[0])); + var entity = new SocketDMChannel(discord, model.Id, discord.GetOrCreateTemporaryUser(state, model.Recipients.Value[0])); entity.Update(state, model); return entity; } @@ -49,6 +44,16 @@ namespace Discord.WebSocket { Recipient.Update(state, model.Recipients.Value[0]); } + internal static SocketDMChannel Create(DiscordSocketClient discord, ClientState state, ulong channelId, API.User recipient) + { + var entity = new SocketDMChannel(discord, channelId, discord.GetOrCreateTemporaryUser(state, recipient)); + entity.Update(state, recipient); + return entity; + } + internal void Update(ClientState state, API.User recipient) + { + Recipient.Update(state, recipient); + } /// public Task CloseAsync(RequestOptions options = null) @@ -57,7 +62,7 @@ namespace Discord.WebSocket //Messages /// public SocketMessage GetCachedMessage(ulong id) - => _messages?.Get(id); + => null; /// /// Gets the message associated with the given . /// @@ -68,10 +73,7 @@ namespace Discord.WebSocket /// public async Task GetMessageAsync(ulong id, RequestOptions options = null) { - IMessage msg = _messages?.Get(id); - if (msg == null) - msg = await ChannelHelper.GetMessageAsync(this, Discord, id, options).ConfigureAwait(false); - return msg; + return await ChannelHelper.GetMessageAsync(this, Discord, id, options).ConfigureAwait(false); } /// @@ -87,7 +89,7 @@ namespace Discord.WebSocket /// Paged collection of messages. /// public IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) - => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, null, Direction.Before, limit, CacheMode.AllowDownload, options); + => ChannelHelper.GetMessagesAsync(this, Discord, null, Direction.Before, limit, options); /// /// Gets a collection of messages in this channel. /// @@ -103,7 +105,7 @@ namespace Discord.WebSocket /// Paged collection of messages. /// public IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) - => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessageId, dir, limit, CacheMode.AllowDownload, options); + => ChannelHelper.GetMessagesAsync(this, Discord, fromMessageId, dir, limit, options); /// /// Gets a collection of messages in this channel. /// @@ -119,16 +121,16 @@ namespace Discord.WebSocket /// Paged collection of messages. /// public IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) - => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessage.Id, dir, limit, CacheMode.AllowDownload, options); + => ChannelHelper.GetMessagesAsync(this, Discord, fromMessage.Id, dir, limit, options); /// public IReadOnlyCollection GetCachedMessages(int limit = DiscordConfig.MaxMessagesPerBatch) - => SocketChannelHelper.GetCachedMessages(this, Discord, _messages, null, Direction.Before, limit); + => ImmutableArray.Create(); /// public IReadOnlyCollection GetCachedMessages(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) - => SocketChannelHelper.GetCachedMessages(this, Discord, _messages, fromMessageId, dir, limit); + => ImmutableArray.Create(); /// public IReadOnlyCollection GetCachedMessages(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) - => SocketChannelHelper.GetCachedMessages(this, Discord, _messages, fromMessage.Id, dir, limit); + => ImmutableArray.Create(); /// public Task> GetPinnedMessagesAsync(RequestOptions options = null) => ChannelHelper.GetPinnedMessagesAsync(this, Discord, options); @@ -152,6 +154,10 @@ namespace Discord.WebSocket public Task DeleteMessageAsync(IMessage message, RequestOptions options = null) => ChannelHelper.DeleteMessageAsync(this, message.Id, Discord, options); + /// + public async Task ModifyMessageAsync(ulong messageId, Action func, RequestOptions options = null) + => await ChannelHelper.ModifyMessageAsync(this, messageId, func, Discord, options).ConfigureAwait(false); + /// public Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); @@ -160,9 +166,12 @@ namespace Discord.WebSocket => ChannelHelper.EnterTypingState(this, Discord, options); internal void AddMessage(SocketMessage msg) - => _messages?.Add(msg); + { + } internal SocketMessage RemoveMessage(ulong id) - => _messages?.Remove(id); + { + return null; + } //Users /// @@ -218,13 +227,13 @@ namespace Discord.WebSocket } /// IAsyncEnumerable> IMessageChannel.GetMessagesAsync(int limit, CacheMode mode, RequestOptions options) - => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, null, Direction.Before, limit, mode, options); + => mode == CacheMode.CacheOnly ? null : GetMessagesAsync(limit, options); /// IAsyncEnumerable> IMessageChannel.GetMessagesAsync(ulong fromMessageId, Direction dir, int limit, CacheMode mode, RequestOptions options) - => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessageId, dir, limit, mode, options); + => mode == CacheMode.CacheOnly ? null : GetMessagesAsync(fromMessageId, dir, limit, options); /// IAsyncEnumerable> IMessageChannel.GetMessagesAsync(IMessage fromMessage, Direction dir, int limit, CacheMode mode, RequestOptions options) - => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessage.Id, dir, limit, mode, options); + => mode == CacheMode.CacheOnly ? null : GetMessagesAsync(fromMessage.Id, dir, limit, options); /// async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) => await GetPinnedMessagesAsync(options).ConfigureAwait(false); diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs index dc5ac4222..ab8c76aeb 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs @@ -180,6 +180,10 @@ namespace Discord.WebSocket public Task DeleteMessageAsync(IMessage message, RequestOptions options = null) => ChannelHelper.DeleteMessageAsync(this, message.Id, Discord, options); + /// + public async Task ModifyMessageAsync(ulong messageId, Action func, RequestOptions options = null) + => await ChannelHelper.ModifyMessageAsync(this, messageId, func, Discord, options).ConfigureAwait(false); + /// public Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs index d7d918f9f..71a20c198 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs @@ -180,6 +180,10 @@ namespace Discord.WebSocket public Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) => ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options); + /// + public async Task ModifyMessageAsync(ulong messageId, Action func, RequestOptions options = null) + => await ChannelHelper.ModifyMessageAsync(this, messageId, func, Discord, options).ConfigureAwait(false); + /// public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) => ChannelHelper.DeleteMessageAsync(this, messageId, Discord, options); diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index e9e535998..9af4ad57e 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -46,8 +46,6 @@ namespace Discord.WebSocket /// public int AFKTimeout { get; private set; } /// - public bool IsEmbeddable { get; private set; } - /// public bool IsWidgetEnabled { get; private set; } /// public VerificationLevel VerificationLevel { get; private set; } @@ -84,7 +82,6 @@ namespace Discord.WebSocket public ulong? ApplicationId { get; internal set; } internal ulong? AFKChannelId { get; private set; } - internal ulong? EmbedChannelId { get; private set; } internal ulong? WidgetChannelId { get; private set; } internal ulong? SystemChannelId { get; private set; } internal ulong? RulesChannelId { get; private set; } @@ -198,21 +195,6 @@ namespace Discord.WebSocket } } /// - /// Gets the embed channel (i.e. the channel set in the guild's widget settings) in this guild. - /// - /// - /// A channel set within the server's widget settings; if none is set. - /// - [Obsolete("This property is deprecated, use WidgetChannel instead.")] - public SocketGuildChannel EmbedChannel - { - get - { - var id = EmbedChannelId; - return id.HasValue ? GetChannel(id.Value) : null; - } - } - /// /// Gets the widget channel (i.e. the channel set in the guild's widget settings) in this guild. /// /// @@ -405,7 +387,8 @@ namespace Discord.WebSocket for (int i = 0; i < model.Members.Length; i++) { var member = SocketGuildUser.Create(this, state, model.Members[i]); - members.TryAdd(member.Id, member); + if (members.TryAdd(member.Id, member)) + member.GlobalUser.AddRef(); } DownloadedMemberCount = members.Count; @@ -440,16 +423,12 @@ namespace Discord.WebSocket internal void Update(ClientState state, Model model) { AFKChannelId = model.AFKChannelId; - if (model.EmbedChannelId.IsSpecified) - EmbedChannelId = model.EmbedChannelId.Value; if (model.WidgetChannelId.IsSpecified) WidgetChannelId = model.WidgetChannelId.Value; SystemChannelId = model.SystemChannelId; RulesChannelId = model.RulesChannelId; PublicUpdatesChannelId = model.PublicUpdatesChannelId; AFKTimeout = model.AFKTimeout; - if (model.EmbedEnabled.IsSpecified) - IsEmbeddable = model.EmbedEnabled.Value; if (model.WidgetEnabled.IsSpecified) IsWidgetEnabled = model.WidgetEnabled.Value; IconId = model.Icon; @@ -504,7 +483,7 @@ namespace Discord.WebSocket } _roles = roles; } - internal void Update(ClientState state, GuildSyncModel model) + /*internal void Update(ClientState state, GuildSyncModel model) //TODO remove? userbot related { var members = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(model.Members.Length * 1.05)); { @@ -524,9 +503,9 @@ namespace Discord.WebSocket _members = members; var _ = _syncPromise.TrySetResultAsync(true); - /*if (!model.Large) - _ = _downloaderPromise.TrySetResultAsync(true);*/ - } + //if (!model.Large) + // _ = _downloaderPromise.TrySetResultAsync(true); + }*/ internal void Update(ClientState state, EmojiUpdateModel model) { @@ -548,11 +527,6 @@ namespace Discord.WebSocket /// /// is . - [Obsolete("This endpoint is deprecated, use ModifyWidgetAsync instead.")] - public Task ModifyEmbedAsync(Action func, RequestOptions options = null) - => GuildHelper.ModifyEmbedAsync(this, Discord, func, options); - /// - /// is . public Task ModifyWidgetAsync(Action func, RequestOptions options = null) => GuildHelper.ModifyWidgetAsync(this, Discord, func, options); /// @@ -869,16 +843,10 @@ namespace Discord.WebSocket else { member = SocketGuildUser.Create(this, Discord.State, model); - if (member == null) - throw new InvalidOperationException("SocketGuildUser.Create failed to produce a member"); // TODO 2.2rel: delete this - if (member.GlobalUser == null) - throw new InvalidOperationException("Member was created without global user"); // TODO 2.2rel: delete this member.GlobalUser.AddRef(); _members[member.Id] = member; DownloadedMemberCount++; } - if (member == null) - throw new InvalidOperationException("AddOrUpdateUser failed to produce a user"); // TODO 2.2rel: delete this return member; } internal SocketGuildUser AddOrUpdateUser(PresenceModel model) @@ -912,6 +880,7 @@ namespace Discord.WebSocket if (self != null) _members.TryAdd(self.Id, self); + _downloaderPromise = new TaskCompletionSource(); DownloadedMemberCount = _members.Count; foreach (var member in members) @@ -1008,6 +977,9 @@ namespace Discord.WebSocket //Emotes /// + public Task> GetEmotesAsync(RequestOptions options = null) + => GuildHelper.GetEmotesAsync(this, Discord, options); + /// public Task GetEmoteAsync(ulong id, RequestOptions options = null) => GuildHelper.GetEmoteAsync(this, Discord, id, options); /// @@ -1229,10 +1201,6 @@ namespace Discord.WebSocket /// bool IGuild.Available => true; /// - ulong IGuild.DefaultChannelId => DefaultChannel?.Id ?? 0; - /// - ulong? IGuild.EmbedChannelId => EmbedChannelId; - /// ulong? IGuild.WidgetChannelId => WidgetChannelId; /// ulong? IGuild.SystemChannelId => SystemChannelId; @@ -1287,10 +1255,6 @@ namespace Discord.WebSocket Task IGuild.GetDefaultChannelAsync(CacheMode mode, RequestOptions options) => Task.FromResult(DefaultChannel); /// - [Obsolete("This method is deprecated, use GetWidgetChannelAsync instead.")] - Task IGuild.GetEmbedChannelAsync(CacheMode mode, RequestOptions options) - => Task.FromResult(EmbedChannel); - /// Task IGuild.GetWidgetChannelAsync(CacheMode mode, RequestOptions options) => Task.FromResult(WidgetChannel); /// diff --git a/src/Discord.Net.WebSocket/Entities/Invites/SocketInvite.cs b/src/Discord.Net.WebSocket/Entities/Invites/SocketInvite.cs index 5dc53a833..845b48b8b 100644 --- a/src/Discord.Net.WebSocket/Entities/Invites/SocketInvite.cs +++ b/src/Discord.Net.WebSocket/Entities/Invites/SocketInvite.cs @@ -49,9 +49,6 @@ namespace Discord.WebSocket /// int? IInvite.MemberCount => throw new NotImplementedException(); /// - [Obsolete("This property doesn't exist anymore and shouldn't be used.")] - bool IInviteMetadata.IsRevoked => throw new NotImplementedException(); - /// public bool IsTemporary { get; private set; } /// int? IInviteMetadata.MaxAge { get => MaxAge; } diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs index 2ca53cbb9..353c26fb8 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs @@ -61,6 +61,9 @@ namespace Discord.WebSocket /// public MessageFlags? Flags { get; private set; } + /// + public MessageType Type { get; private set; } + /// /// Returns all attachments included in this message. /// @@ -99,6 +102,8 @@ namespace Discord.WebSocket /// public virtual IReadOnlyCollection Tags => ImmutableArray.Create(); /// + public virtual IReadOnlyCollection Stickers => ImmutableArray.Create(); + /// public IReadOnlyDictionary Reactions => _reactions.GroupBy(r => r.Emote).ToDictionary(x => x.Key, x => new ReactionMetadata { ReactionCount = x.Count(), IsMe = x.Any(y => y.UserId == Discord.CurrentUser.Id) }); /// @@ -120,6 +125,8 @@ namespace Discord.WebSocket } internal virtual void Update(ClientState state, Model model) { + Type = model.Type; + if (model.Timestamp.IsSpecified) _timestampTicks = model.Timestamp.Value.UtcTicks; @@ -183,8 +190,6 @@ namespace Discord.WebSocket /// IMessageChannel IMessage.Channel => Channel; /// - MessageType IMessage.Type => MessageType.Default; - /// IReadOnlyCollection IMessage.Attachments => Attachments; /// IReadOnlyCollection IMessage.Embeds => Embeds; @@ -194,6 +199,8 @@ namespace Discord.WebSocket IReadOnlyCollection IMessage.MentionedRoleIds => MentionedRoles.Select(x => x.Id).ToImmutableArray(); /// IReadOnlyCollection IMessage.MentionedUserIds => MentionedUsers.Select(x => x.Id).ToImmutableArray(); + /// + IReadOnlyCollection IMessage.Stickers => Stickers; internal void AddReaction(SocketReaction reaction) { diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketSystemMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketSystemMessage.cs index d0ce5025b..ec22a7703 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketSystemMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketSystemMessage.cs @@ -9,9 +9,6 @@ namespace Discord.WebSocket [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class SocketSystemMessage : SocketMessage, ISystemMessage { - /// - public MessageType Type { get; private set; } - internal SocketSystemMessage(DiscordSocketClient discord, ulong id, ISocketMessageChannel channel, SocketUser author) : base(discord, id, channel, author, MessageSource.System) { @@ -25,8 +22,6 @@ namespace Discord.WebSocket internal override void Update(ClientState state, Model model) { base.Update(state, model); - - Type = model.Type; } private string DebuggerDisplay => $"{Author}: {Content} ({Id}, {Type})"; diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs index 859b1b80a..597544f4d 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs @@ -23,6 +23,7 @@ namespace Discord.WebSocket private ImmutableArray _tags = ImmutableArray.Create(); private ImmutableArray _roleMentions = ImmutableArray.Create(); private ImmutableArray _userMentions = ImmutableArray.Create(); + private ImmutableArray _stickers = ImmutableArray.Create(); /// public override bool IsTTS => _isTTS; @@ -47,6 +48,8 @@ namespace Discord.WebSocket /// public override IReadOnlyCollection MentionedUsers => _userMentions; /// + public override IReadOnlyCollection Stickers => _stickers; + /// public IUserMessage ReferencedMessage => _referencedMessage; internal SocketUserMessage(DiscordSocketClient discord, ulong id, ISocketMessageChannel channel, SocketUser author, MessageSource source) @@ -158,6 +161,20 @@ namespace Discord.WebSocket refMsgAuthor = new SocketUnknownUser(Discord, id: 0); _referencedMessage = SocketUserMessage.Create(Discord, state, refMsgAuthor, Channel, refMsg); } + + if (model.Stickers.IsSpecified) + { + var value = model.Stickers.Value; + if (value.Length > 0) + { + var stickers = ImmutableArray.CreateBuilder(value.Length); + for (int i = 0; i < value.Length; i++) + stickers.Add(Sticker.Create(value[i])); + _stickers = stickers.ToImmutable(); + } + else + _stickers = ImmutableArray.Create(); + } } /// @@ -172,9 +189,6 @@ namespace Discord.WebSocket /// public Task UnpinAsync(RequestOptions options = null) => MessageHelper.UnpinAsync(this, Discord, options); - /// - public Task ModifySuppressionAsync(bool suppressEmbeds, RequestOptions options = null) - => MessageHelper.SuppressEmbedsAsync(this, Discord, suppressEmbeds, options); public string Resolve(int startIndex, TagHandling userHandling = TagHandling.Name, TagHandling channelHandling = TagHandling.Name, TagHandling roleHandling = TagHandling.Name, TagHandling everyoneHandling = TagHandling.Ignore, TagHandling emojiHandling = TagHandling.Name) diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs index 48de7552a..15c5182fc 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs @@ -12,7 +12,6 @@ namespace Discord.WebSocket public override string Username { get; internal set; } public override ushort DiscriminatorValue { get; internal set; } public override string AvatarId { get; internal set; } - public SocketDMChannel DMChannel { get; internal set; } internal override SocketPresence Presence { get; set; } public override bool IsWebhook => false; @@ -52,7 +51,6 @@ namespace Discord.WebSocket internal void Update(ClientState state, PresenceModel model) { Presence = SocketPresence.Create(model); - DMChannel = state.DMChannels.FirstOrDefault(x => x.Recipient.Id == Id); } private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Global)"; diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs index c0a681d9d..444c76ffa 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs @@ -63,7 +63,7 @@ namespace Discord.WebSocket /// /// Returns a collection of roles that the user possesses. /// - public IReadOnlyCollection Roles + public IReadOnlyCollection Roles => _roleIds.Select(id => Guild.GetRole(id)).Where(x => x != null).ToReadOnlyCollection(() => _roleIds.Length); /// /// Returns the voice channel the user is in, or null if none. @@ -125,12 +125,16 @@ namespace Discord.WebSocket { var entity = new SocketGuildUser(guild, guild.Discord.GetOrCreateUser(state, model.User)); entity.Update(state, model); + if (!model.Roles.IsSpecified) + entity.UpdateRoles(new ulong[0]); return entity; } internal static SocketGuildUser Create(SocketGuild guild, ClientState state, PresenceModel model) { var entity = new SocketGuildUser(guild, guild.Discord.GetOrCreateUser(state, model.User)); entity.Update(state, model, false); + if (!model.Roles.IsSpecified) + entity.UpdateRoles(new ulong[0]); return entity; } internal void Update(ClientState state, MemberModel model) @@ -177,17 +181,29 @@ namespace Discord.WebSocket public Task KickAsync(string reason = null, RequestOptions options = null) => UserHelper.KickAsync(this, Discord, reason, options); /// + public Task AddRoleAsync(ulong roleId, RequestOptions options = null) + => AddRolesAsync(new[] { roleId }, options); + /// public Task AddRoleAsync(IRole role, RequestOptions options = null) - => AddRolesAsync(new[] { role }, options); + => AddRoleAsync(role.Id, options); + /// + public Task AddRolesAsync(IEnumerable roleIds, RequestOptions options = null) + => UserHelper.AddRolesAsync(this, Discord, roleIds, options); /// public Task AddRolesAsync(IEnumerable roles, RequestOptions options = null) - => UserHelper.AddRolesAsync(this, Discord, roles, options); + => AddRolesAsync(roles.Select(x => x.Id), options); + /// + public Task RemoveRoleAsync(ulong roleId, RequestOptions options = null) + => RemoveRolesAsync(new[] { roleId }, options); /// public Task RemoveRoleAsync(IRole role, RequestOptions options = null) - => RemoveRolesAsync(new[] { role }, options); + => RemoveRoleAsync(role.Id, options); + /// + public Task RemoveRolesAsync(IEnumerable roleIds, RequestOptions options = null) + => UserHelper.RemoveRolesAsync(this, Discord, roleIds, options); /// public Task RemoveRolesAsync(IEnumerable roles, RequestOptions options = null) - => UserHelper.RemoveRolesAsync(this, Discord, roles, options); + => RemoveRolesAsync(roles.Select(x => x.Id)); /// public ChannelPermissions GetPermissions(IGuildChannel channel) diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs index 407e14419..fe672a4d6 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; +using System.Linq; using Model = Discord.API.Presence; namespace Discord.WebSocket @@ -15,15 +16,12 @@ namespace Discord.WebSocket /// public UserStatus Status { get; } /// - public IActivity Activity { get; } - /// public IImmutableSet ActiveClients { get; } /// public IImmutableList Activities { get; } - internal SocketPresence(UserStatus status, IActivity activity, IImmutableSet activeClients, IImmutableList activities) + internal SocketPresence(UserStatus status, IImmutableSet activeClients, IImmutableList activities) { Status = status; - Activity = activity; ActiveClients = activeClients ?? ImmutableHashSet.Empty; Activities = activities ?? ImmutableList.Empty; } @@ -31,7 +29,7 @@ namespace Discord.WebSocket { var clients = ConvertClientTypesDict(model.ClientStatus.GetValueOrDefault()); var activities = ConvertActivitiesList(model.Activities); - return new SocketPresence(model.Status, model.Game?.ToEntity(), clients, activities); + return new SocketPresence(model.Status, clients, activities); } /// /// Creates a new containing all of the client types @@ -84,7 +82,7 @@ namespace Discord.WebSocket /// A string that resolves to . /// public override string ToString() => Status.ToString(); - private string DebuggerDisplay => $"{Status}{(Activity != null ? $", {Activity.Name}": "")}"; + private string DebuggerDisplay => $"{Status}{(Activities?.FirstOrDefault()?.Name ?? "")}"; internal SocketPresence Clone() => this; } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs index dd2e747b4..840a1c30b 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs +++ b/src/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, null); } set { } } + internal override SocketPresence Presence { get { return new SocketPresence(UserStatus.Offline, null, null); } set { } } /// /// This field is not supported for an unknown user. internal override SocketGlobalUser GlobalUser => diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs index 4e6d4b3f8..025daf29a 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs @@ -38,8 +38,6 @@ namespace Discord.WebSocket /// public string Mention => MentionUtils.MentionUser(Id); /// - public IActivity Activity => Presence.Activity; - /// public UserStatus Status => Presence.Status; /// public IImmutableSet ActiveClients => Presence.ActiveClients ?? ImmutableHashSet.Empty; @@ -94,8 +92,8 @@ namespace Discord.WebSocket } /// - public async Task GetOrCreateDMChannelAsync(RequestOptions options = null) - => GlobalUser.DMChannel ?? await UserHelper.CreateDMChannelAsync(this, Discord, options).ConfigureAwait(false) as IDMChannel; + public async Task CreateDMChannelAsync(RequestOptions options = null) + => await UserHelper.CreateDMChannelAsync(this, Discord, options).ConfigureAwait(false); /// public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs index 5103aa8b3..404ab116d 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs @@ -30,8 +30,8 @@ namespace Discord.WebSocket /// public override bool IsWebhook => true; /// - internal override SocketPresence Presence { get { return new SocketPresence(UserStatus.Offline, null, null, null); } set { } } - internal override SocketGlobalUser GlobalUser => + internal override SocketPresence Presence { get { return new SocketPresence(UserStatus.Offline, null, null); } set { } } + internal override SocketGlobalUser GlobalUser => throw new NotSupportedException(); internal SocketWebhookUser(SocketGuild guild, ulong id, ulong webhookId) @@ -73,32 +73,52 @@ namespace Discord.WebSocket ChannelPermissions IGuildUser.GetPermissions(IGuildChannel channel) => Permissions.ToChannelPerms(channel, GuildPermissions.Webhook.RawValue); /// /// Webhook users cannot be kicked. - Task IGuildUser.KickAsync(string reason, RequestOptions options) => + Task IGuildUser.KickAsync(string reason, RequestOptions options) => throw new NotSupportedException("Webhook users cannot be kicked."); /// /// Webhook users cannot be modified. - Task IGuildUser.ModifyAsync(Action func, RequestOptions options) => + Task IGuildUser.ModifyAsync(Action func, RequestOptions options) => throw new NotSupportedException("Webhook users cannot be modified."); /// /// Roles are not supported on webhook users. - Task IGuildUser.AddRoleAsync(IRole role, RequestOptions options) => + Task IGuildUser.AddRoleAsync(ulong roleId, RequestOptions options) => throw new NotSupportedException("Roles are not supported on webhook users."); /// /// Roles are not supported on webhook users. - Task IGuildUser.AddRolesAsync(IEnumerable roles, RequestOptions options) => + Task IGuildUser.AddRoleAsync(IRole role, RequestOptions options) => throw new NotSupportedException("Roles are not supported on webhook users."); /// /// Roles are not supported on webhook users. - Task IGuildUser.RemoveRoleAsync(IRole role, RequestOptions options) => + Task IGuildUser.AddRolesAsync(IEnumerable roleIds, RequestOptions options) => throw new NotSupportedException("Roles are not supported on webhook users."); /// /// Roles are not supported on webhook users. - Task IGuildUser.RemoveRolesAsync(IEnumerable roles, RequestOptions options) => + Task IGuildUser.AddRolesAsync(IEnumerable roles, RequestOptions options) => + throw new NotSupportedException("Roles are not supported on webhook users."); + + /// + /// Roles are not supported on webhook users. + Task IGuildUser.RemoveRoleAsync(ulong roleId, RequestOptions options) => + throw new NotSupportedException("Roles are not supported on webhook users."); + + /// + /// Roles are not supported on webhook users. + Task IGuildUser.RemoveRoleAsync(IRole role, RequestOptions options) => + throw new NotSupportedException("Roles are not supported on webhook users."); + + /// + /// Roles are not supported on webhook users. + Task IGuildUser.RemoveRolesAsync(IEnumerable roles, RequestOptions options) => + throw new NotSupportedException("Roles are not supported on webhook users."); + + /// + /// Roles are not supported on webhook users. + Task IGuildUser.RemoveRolesAsync(IEnumerable roles, RequestOptions options) => throw new NotSupportedException("Roles are not supported on webhook users."); //IVoiceState diff --git a/src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs b/src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs index 4723ae57a..82e2f0573 100644 --- a/src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs +++ b/src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs @@ -108,11 +108,11 @@ namespace Discord.Net.WebSockets } private async Task DisconnectInternalAsync(int closeCode = 1000, bool isDisposing = false) { + _isDisconnecting = true; + try { _disconnectTokenSource.Cancel(false); } catch { } - _isDisconnecting = true; - if (_client != null) { if (!isDisposing) @@ -166,7 +166,14 @@ namespace Discord.Net.WebSockets public async Task SendAsync(byte[] data, int index, int count, bool isText) { - await _lock.WaitAsync().ConfigureAwait(false); + try + { + await _lock.WaitAsync(_cancelToken).ConfigureAwait(false); + } + catch (TaskCanceledException) + { + return; + } try { if (_client == null) return; @@ -201,7 +208,7 @@ namespace Discord.Net.WebSockets { while (!cancelToken.IsCancellationRequested) { - WebSocketReceiveResult socketResult = await _client.ReceiveAsync(buffer, CancellationToken.None).ConfigureAwait(false); + WebSocketReceiveResult socketResult = await _client.ReceiveAsync(buffer, cancelToken).ConfigureAwait(false); byte[] result; int resultCount; diff --git a/src/Discord.Net.Webhook/DiscordWebhookClient.cs b/src/Discord.Net.Webhook/DiscordWebhookClient.cs index a6d4ef183..91d077411 100644 --- a/src/Discord.Net.Webhook/DiscordWebhookClient.cs +++ b/src/Discord.Net.Webhook/DiscordWebhookClient.cs @@ -91,6 +91,35 @@ namespace Discord.Webhook string username = null, string avatarUrl = null, RequestOptions options = null, AllowedMentions allowedMentions = null) => WebhookClientHelper.SendMessageAsync(this, text, isTTS, embeds, username, avatarUrl, allowedMentions, options); + /// + /// Modifies a message posted using this webhook. + /// + /// + /// This method can only modify messages that were sent using the same webhook. + /// + /// ID of the modified message. + /// A delegate containing the properties to modify the message with. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous modification operation. + /// + public Task ModifyMessageAsync(ulong messageId, Action func, RequestOptions options = null) + => WebhookClientHelper.ModifyMessageAsync(this, messageId, func, options); + + /// + /// Deletes a message posted using this webhook. + /// + /// + /// This method can only delete messages that were sent using the same webhook. + /// + /// ID of the deleted message. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous deletion operation. + /// + public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) + => WebhookClientHelper.DeleteMessageAsync(this, messageId, 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, diff --git a/src/Discord.Net.Webhook/Entities/Messages/WebhookMessageProperties.cs b/src/Discord.Net.Webhook/Entities/Messages/WebhookMessageProperties.cs new file mode 100644 index 000000000..dec7b6e3b --- /dev/null +++ b/src/Discord.Net.Webhook/Entities/Messages/WebhookMessageProperties.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; + +namespace Discord.Webhook +{ + /// + /// Properties that are used to modify an Webhook message with the specified changes. + /// + public class WebhookMessageProperties + { + /// + /// Gets or sets the content of the message. + /// + /// + /// This must be less than the constant defined by . + /// + public Optional Content { get; set; } + /// + /// Gets or sets the embed array that the message should display. + /// + public Optional> Embeds { get; set; } + /// + /// Gets or sets the allowed mentions of the message. + /// + public Optional AllowedMentions { get; set; } + } +} diff --git a/src/Discord.Net.Webhook/WebhookClientHelper.cs b/src/Discord.Net.Webhook/WebhookClientHelper.cs index 4bc2eaca9..886ff234d 100644 --- a/src/Discord.Net.Webhook/WebhookClientHelper.cs +++ b/src/Discord.Net.Webhook/WebhookClientHelper.cs @@ -36,7 +36,59 @@ namespace Discord.Webhook 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, + public static async Task ModifyMessageAsync(DiscordWebhookClient client, ulong messageId, + Action func, RequestOptions options) + { + var args = new WebhookMessageProperties(); + func(args); + + if (args.AllowedMentions.IsSpecified) + { + var allowedMentions = args.AllowedMentions.Value; + Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), + "A max of 100 role Ids are allowed."); + Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), + "A max of 100 user Ids are allowed."); + + // check that user flag and user Id list are exclusive, same with role flag and role Id list + if (allowedMentions?.AllowedTypes != null) + { + 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 apiArgs = new ModifyWebhookMessageParams + { + Content = args.Content.IsSpecified ? args.Content.Value : Optional.Create(), + Embeds = + args.Embeds.IsSpecified + ? args.Embeds.Value.Select(embed => embed.ToModel()).ToArray() + : Optional.Create(), + AllowedMentions = args.AllowedMentions.IsSpecified + ? args.AllowedMentions.Value.ToModel() + : Optional.Create() + }; + + await client.ApiClient.ModifyWebhookMessageAsync(client.Webhook.Id, messageId, apiArgs, options) + .ConfigureAwait(false); + } + public static async Task DeleteMessageAsync(DiscordWebhookClient client, ulong messageId, RequestOptions options) + { + await client.ApiClient.DeleteWebhookMessageAsync(client.Webhook.Id, messageId, options).ConfigureAwait(false); + } + public static async Task SendFileAsync(DiscordWebhookClient client, string filePath, string text, bool isTTS, IEnumerable embeds, string username, string avatarUrl, AllowedMentions allowedMentions, RequestOptions options, bool isSpoiler) { string filename = Path.GetFileName(filePath); diff --git a/src/Discord.Net/Discord.Net.nuspec b/src/Discord.Net/Discord.Net.nuspec index db3f29ca7..e3f0150f9 100644 --- a/src/Discord.Net/Discord.Net.nuspec +++ b/src/Discord.Net/Discord.Net.nuspec @@ -2,7 +2,7 @@ Discord.Net - 2.3.1$suffix$ + 3.0.0-dev$suffix$ Discord.Net Discord.Net Contributors foxbot @@ -14,25 +14,25 @@ https://github.com/RogueException/Discord.Net/raw/dev/docs/marketing/logo/PackageLogo.png - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + diff --git a/test/Discord.Net.Tests.Unit/EmbedBuilderTests.cs b/test/Discord.Net.Tests.Unit/EmbedBuilderTests.cs index 12ec1a0bd..da21afee1 100644 --- a/test/Discord.Net.Tests.Unit/EmbedBuilderTests.cs +++ b/test/Discord.Net.Tests.Unit/EmbedBuilderTests.cs @@ -126,7 +126,7 @@ namespace Discord { IEnumerable GetInvalid() { - yield return new string('a', 2049); + yield return new string('a', 4097); } foreach (var description in GetInvalid()) { @@ -149,7 +149,7 @@ namespace Discord { yield return string.Empty; yield return null; - yield return new string('a', 2048); + yield return new string('a', 4096); } foreach (var description in GetValid()) { @@ -190,42 +190,6 @@ namespace Discord Assert.Equal(result.ThumbnailUrl, url); } - /// - /// Tests that invalid urls throw an . - /// - /// The url to set. - [Theory] - [InlineData(" ")] - [InlineData("not a url")] - public void Url_Invalid(string url) - { - Assert.Throws(() - => new EmbedBuilder() - .WithUrl(url)); - Assert.Throws(() - => new EmbedBuilder() - .WithImageUrl(url)); - Assert.Throws(() - => new EmbedBuilder() - .WithThumbnailUrl(url)); - - Assert.Throws(() => - { - var b = new EmbedBuilder(); - b.Url = url; - }); - Assert.Throws(() => - { - var b = new EmbedBuilder(); - b.ImageUrl = url; - }); - Assert.Throws(() => - { - var b = new EmbedBuilder(); - b.ThumbnailUrl = url; - }); - } - /// /// Tests the value of the property when there are no fields set. /// @@ -343,24 +307,6 @@ namespace Discord Assert.Equal(name, footer.Text); } /// - /// Tests that invalid URLs throw an . - /// - [Fact] - public void EmbedFooterBuilder_InvalidURL() - { - IEnumerable InvalidUrls() - { - yield return "not a url"; - } - foreach (var url in InvalidUrls()) - { - Assert.Throws(() => - { - new EmbedFooterBuilder().WithIconUrl(url); - }); - } - } - /// /// Tests that invalid text throws an . /// [Fact] diff --git a/test/Discord.Net.Tests.Unit/MockedEntities/MockedDMChannel.cs b/test/Discord.Net.Tests.Unit/MockedEntities/MockedDMChannel.cs index f169fb717..593b9201a 100644 --- a/test/Discord.Net.Tests.Unit/MockedEntities/MockedDMChannel.cs +++ b/test/Discord.Net.Tests.Unit/MockedEntities/MockedDMChannel.cs @@ -33,6 +33,11 @@ namespace Discord throw new NotImplementedException(); } + public Task ModifyMessageAsync(ulong messageId, Action func, RequestOptions options = null) + { + throw new NotImplementedException(); + } + public IDisposable EnterTypingState(RequestOptions options = null) { throw new NotImplementedException(); diff --git a/test/Discord.Net.Tests.Unit/MockedEntities/MockedGroupChannel.cs b/test/Discord.Net.Tests.Unit/MockedEntities/MockedGroupChannel.cs index e662d628a..6daf6a9c8 100644 --- a/test/Discord.Net.Tests.Unit/MockedEntities/MockedGroupChannel.cs +++ b/test/Discord.Net.Tests.Unit/MockedEntities/MockedGroupChannel.cs @@ -31,6 +31,11 @@ namespace Discord throw new NotImplementedException(); } + public Task ModifyMessageAsync(ulong messageId, Action func, RequestOptions options = null) + { + throw new NotImplementedException(); + } + public Task DisconnectAsync() { throw new NotImplementedException(); diff --git a/test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs b/test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs index fbaaf9a18..51aece5f2 100644 --- a/test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs +++ b/test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs @@ -77,6 +77,11 @@ namespace Discord throw new NotImplementedException(); } + public Task ModifyMessageAsync(ulong messageId, Action func, RequestOptions options = null) + { + throw new NotImplementedException(); + } + public IDisposable EnterTypingState(RequestOptions options = null) { throw new NotImplementedException(); diff --git a/test/Discord.Net.Tests.Unit/TokenUtilsTests.cs b/test/Discord.Net.Tests.Unit/TokenUtilsTests.cs index e9526b761..4306fa9e2 100644 --- a/test/Discord.Net.Tests.Unit/TokenUtilsTests.cs +++ b/test/Discord.Net.Tests.Unit/TokenUtilsTests.cs @@ -127,8 +127,6 @@ namespace Discord /// The type is treated as an invalid . /// [Theory] - // TokenType.User - [InlineData(0)] // out of range TokenType [InlineData(-1)] [InlineData(4)]