diff --git a/CHANGELOG.md b/CHANGELOG.md index 886754052..21e37b295 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,127 @@ # 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) +- #1773 Private methods aren't added as commands (0fc713a) +- #1780 NullReferenceException in pin/unpin audit logs (f794163) +- #1786 Add ChannelType property to ChannelInfo audit log (6ac5ea1) +- #1791 Update Webhook ChannelId from model change (d2518db) +- #1794 Audit log UserId can be null (d41aeee) + +### Misc +- #1774 Add remark regarding CustomStatus as the activity (51b7afe) + +## [2.3.0] - 2021-01-28 +### Added +- #1491 Add INVITE_CREATE and INVITE_DELETE events (1ab670b) +- #1520 Support reading multiple activities (421a0c1) +- #1521 Allow for inherited commands in modules (a51cdf6) +- #1526 Add Direction.Around to GetMessagesAsync (f2130f8) +- #1537 Implement gateway ratelimit (ec673e1) +- #1544 Add MESSAGE_REACTION_REMOVE_EMOJI and RemoveAllReactionsForEmoteAsync (a89f076) +- #1549 Add GetUsersAsync to SocketGuild (30b5a83) +- #1566 Support Gateway Intents (d5d10d3) +- #1573 Add missing properties to Guild and deprecate GuildEmbed (ec212b1) +- #1581 Add includeRoleIds to PruneUsersAsync (a80e5ff) +- #1588 Add GetStreams to AudioClient (1e012ac) +- #1596 Add missing channel properties (2d80037) +- #1604 Add missing application properties (including Teams) (10fcde0) +- #1619 Add "View Guild Insights" to GuildPermission (2592264) +- #1637 Added CultureInvariant RegexOption to WebhookUrlRegex (e3925a7) +- #1659 Add inline replies (e3850e1) +- #1688 Send presence on Identify payload (25d5d36) +- #1721 Add role tags (6a62c47) +- #1722 Add user public flags (c683b29) +- #1724 Add MessageFlags and AllowedMentions to message modify (225550d) +- #1731 Add GuildUser IsPending property (8b25c9b) +- #1690 Add max bitrate value to SocketGuild (aacfea0) + +### Fixed +- #1244 Missing AddReactions permission for DM channels. (e40ca4a) +- #1469 unsupported property causes an exception (468f826) +- #1525 AllowedMentions and AllowedMentionTypes (3325031) +- #1531 Add AllowedMentions to SendFileAsync (ab32607) +- #1532 GuildEmbed.ChannelId as nullable per API documentation (971d519) +- #1546 Different ratelimits for the same route (implement discord buckets) (2f6c017) +- #1548 Incomplete Ready, DownloadUsersAsync, and optimize AlwaysDownloadUsers (dc8c959) +- #1555 InvalidOperationException at MESSAGE_CREATE (bd4672a) +- #1557 Sending 2 requests instead of 1 to create a Guild role. (5430cc8) +- #1571 Not using the new domain name. (df8a0f7) +- #1578 Trim token before passing it to the authorization header (42ba372) +- #1580 Stop TaskCanceledException from bubbling up (b8fa464) +- #1599 Invite audit log without inviter (b95b95b) +- #1602 Add AllowedMentions to webhooks (bd4516b) +- #1603 Cancel reconnection when 4014 (f396cd9) +- #1608 Voice overwrites and CategoryId remarks (43c8fc0) +- #1614 Check error 404 and return null for GetBanAsync (ae9fff6) +- #1621 Parse mentions from message payload (366ca9a) +- #1622 Do not update overwrite cache locally (3860da0) +- #1623 Invoke UserUpdated from GuildMemberUpdated if needed (3085e88) +- #1624 Handle null PreferredLocale in rare cases (c1d04b4) +- #1639 Invite and InviteMetadata properties (dd2e524) +- #1642 Add missing permissions (4b389f3) +- #1647 handicap member downloading for verified bots (fa5ef5e) +- #1652 Update README.MD to reflect new discord domain (03b831e) +- #1667 Audio stream dispose (a2af985) +- #1671 Crosspost throwing InvalidOperationException (9134443) +- #1672 Team is nullable, not optional (be60d81) +- #1681 Emoji url encode (04389a4) +- #1683 SocketGuild.HasAllMembers is false if a user left a guild (47f571e) +- #1686 Revert PremiumSubscriptionCount type (97e71cd) +- #1695 Possible NullReferenceException when receiving InvalidSession (5213916) +- #1702 Rollback Activities to Game (9d7cb39) +- #1727 Move and fix internal AllowedMentions object (4a7f8fe) +- limit request members batch size (084db25) +- UserMentions throwing NullRef (5ed01a3) +- Wrong author for SocketUserMessage.ReferencedMessage (1e9b252) +- Discord sends null when there's no team (05a1f0a) +- IMessage.Embeds docs remarks (a4d32d3) +- Missing MessageReference when sending files (2095701) + +### Misc +- #1545 MutualGuilds optimization (323a677) +- #1551 Update webhook regex to support discord.com (7585789) +- #1556 Add SearchUsersAsync (57880de) +- #1561 Minor refactor to switch expression (42826df) +- #1576 Updating comments for privileged intents (c42bfa6) +- #1678 Change ratelimit messages (47ed806) +- #1714 Update summary of SocketVoiceChannel.Users (e385c40) +- #1720 VoiceRegions and related changes (5934c79) +- Add updated libraries for LastModified (d761846) +- Add alternative documentation link (accd351) +- Temporarily disable StyleCops until all the fixes are impl'd (36de7b2) +- Remove redundant CreateGuildRoleParams (3df0539) +- Add minor tweaks to DiscordSocketConfig docs strings (2cd1880) +- Fix MaxWaitBetweenGuildAvailablesBeforeReady docs string (e31cdc7) +- Missing summary tag for GatewayIntents (3a10018) +- Add new method of role ID copy (857ef77) +- Resolve inheritdocs for IAttachment (9ea3291) +- Mark null as a specific langword in summary (13a41f8) +- Cleanup GatewayReconnectException docs (833ee42) +- Update Docfx.Plugins.LastModified to v1.2.4 (28a6f97) +- Update framework version for tests to Core 3.1 to comply with LTS (4988a07) +- Move bulk deletes remarks from to (62539f0) + ## [2.2.0] - 2020-04-16 ### Added - #1247 Implement Client Status Support (9da11b4) diff --git a/Discord.Net.targets b/Discord.Net.targets index 9502e91dd..febd921d1 100644 --- a/Discord.Net.targets +++ b/Discord.Net.targets @@ -1,6 +1,6 @@ - 2.3.0 + 3.0.0 dev latest Discord.Net Contributors 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/Builders/ModuleClassBuilder.cs b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs index 28037b0fa..7a752090e 100644 --- a/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs +++ b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs @@ -136,7 +136,7 @@ namespace Discord.Commands builder.Name = typeInfo.Name; // Get all methods (including from inherited members), that are valid commands - var validCommands = typeInfo.GetMethods().Where(IsValidCommandDefinition); + var validCommands = typeInfo.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).Where(IsValidCommandDefinition); foreach (var method in validCommands) { 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 656b67a61..11998c28b 100644 --- a/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs @@ -260,6 +260,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/IInvite.cs b/src/Discord.Net.Core/Entities/Invites/IInvite.cs index 993f1f047..47ffffacb 100644 --- a/src/Discord.Net.Core/Entities/Invites/IInvite.cs +++ b/src/Discord.Net.Core/Entities/Invites/IInvite.cs @@ -20,6 +20,13 @@ namespace Discord /// string Url { get; } + /// + /// Gets the user that created this invite. + /// + /// + /// A user that created this invite. + /// + IUser Inviter { get; } /// /// Gets the channel this invite is linked to. /// @@ -83,5 +90,19 @@ namespace Discord /// invite points to; null if one cannot be obtained. /// int? MemberCount { get; } + /// + /// Gets the user this invite is linked to via . + /// + /// + /// A user that is linked to this invite. + /// + IUser TargetUser { get; } + /// + /// Gets the type of the linked for this invite. + /// + /// + /// The type of the linked user that is linked to this invite. + /// + TargetUserType TargetUserType { get; } } } diff --git a/src/Discord.Net.Core/Entities/Invites/IInviteMetadata.cs b/src/Discord.Net.Core/Entities/Invites/IInviteMetadata.cs index 471dc377f..c2580c853 100644 --- a/src/Discord.Net.Core/Entities/Invites/IInviteMetadata.cs +++ b/src/Discord.Net.Core/Entities/Invites/IInviteMetadata.cs @@ -7,20 +7,6 @@ namespace Discord /// public interface IInviteMetadata : IInvite { - /// - /// Gets the user that created this invite. - /// - /// - /// A user that created this invite. - /// - IUser Inviter { get; } - /// - /// Gets a value that indicates whether the invite has been revoked. - /// - /// - /// true if this invite was revoked; otherwise false. - /// - bool IsRevoked { get; } /// /// Gets a value that indicates whether the invite is a temporary one. /// diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs b/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs index 555fd95df..f1238ddcf 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; @@ -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 d13b6b592..cba8ce29f 100644 --- a/src/Discord.Net.Core/Entities/Messages/IMessage.cs +++ b/src/Discord.Net.Core/Entities/Messages/IMessage.cs @@ -92,10 +92,10 @@ namespace Discord /// Gets all embeds included in this message. /// /// - /// /// This property gets a read-only collection of embeds associated with this message. Depending on the /// message, a sent message may contain one or more embeds. This is usually true when multiple link previews /// are generated; however, only one can be featured. + /// /// /// A read-only collection of embed objects. /// @@ -168,6 +168,25 @@ namespace Discord /// The 's attached to this message /// IReadOnlyCollection Components { 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. + /// + /// + /// This value is determined by bitwise OR-ing values together. + /// + /// + /// A message's flags, if any is associated. + /// + MessageFlags? Flags { get; } + /// /// Adds a reaction 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/MessageFlags.cs b/src/Discord.Net.Core/Entities/Messages/MessageFlags.cs new file mode 100644 index 000000000..52d0f0e9e --- /dev/null +++ b/src/Discord.Net.Core/Entities/Messages/MessageFlags.cs @@ -0,0 +1,36 @@ +using System; + +namespace Discord +{ + [Flags] + public enum MessageFlags + { + /// + /// Default value for flags, when none are given to a message. + /// + None = 0, + /// + /// Flag given to messages that have been published to subscribed + /// channels (via Channel Following). + /// + Crossposted = 1 << 0, + /// + /// Flag given to messages that originated from a message in another + /// channel (via Channel Following). + /// + IsCrosspost = 1 << 1, + /// + /// Flag given to messages that do not display any embeds. + /// + SuppressEmbeds = 1 << 2, + /// + /// Flag given to messages that the source message for this crosspost + /// has been deleted (via Channel Following). + /// + SourceMessageDeleted = 1 << 3, + /// + /// Flag given to messages that came from the urgent message system. + /// + Urgent = 1 << 4, + } +} diff --git a/src/Discord.Net.Core/Entities/Messages/MessageProperties.cs b/src/Discord.Net.Core/Entities/Messages/MessageProperties.cs index 1e4846a94..c71d29520 100644 --- a/src/Discord.Net.Core/Entities/Messages/MessageProperties.cs +++ b/src/Discord.Net.Core/Entities/Messages/MessageProperties.cs @@ -26,5 +26,18 @@ namespace Discord /// Gets or sets the components for this message. /// public Optional Components { get; set; } + + /// + /// Gets or sets the flags of the message. + /// + /// + /// Only can be set/unset and you need to be + /// the author of the message. + /// + public Optional Flags { get; set; } + /// + /// Gets or sets the allowed mentions of the message. + /// + public Optional AllowedMentions { get; set; } } } diff --git a/src/Discord.Net.Core/Entities/Messages/MessageType.cs b/src/Discord.Net.Core/Entities/Messages/MessageType.cs index e6a117ba5..c384e29ea 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, /// /// The message is an Application Command 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/Roles/IRole.cs b/src/Discord.Net.Core/Entities/Roles/IRole.cs index 66556fc2c..c02322be9 100644 --- a/src/Discord.Net.Core/Entities/Roles/IRole.cs +++ b/src/Discord.Net.Core/Entities/Roles/IRole.cs @@ -65,6 +65,13 @@ namespace Discord /// An representing the position of the role in the role list of the guild. /// int Position { get; } + /// + /// Gets the tags related to this role. + /// + /// + /// A object containing all tags related to this role. + /// + RoleTags Tags { get; } /// /// Modifies this role. diff --git a/src/Discord.Net.Core/Entities/Roles/RoleTags.cs b/src/Discord.Net.Core/Entities/Roles/RoleTags.cs new file mode 100644 index 000000000..d0cbd3580 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Roles/RoleTags.cs @@ -0,0 +1,40 @@ +namespace Discord +{ + /// + /// Provides tags related to a discord role. + /// + public class RoleTags + { + /// + /// Gets the identifier of the bot that this role belongs to, if it does. + /// + /// + /// A if this role belongs to a bot; otherwise + /// . + /// + public ulong? BotId { get; } + /// + /// Gets the identifier of the integration that this role belongs to, if it does. + /// + /// + /// A if this role belongs to an integration; otherwise + /// . + /// + public ulong? IntegrationId { get; } + /// + /// Gets if this role is the guild's premium subscriber (booster) role. + /// + /// + /// if this role is the guild's premium subscriber role; + /// otherwise . + /// + public bool IsPremiumSubscriberRole { get; } + + internal RoleTags(ulong? botId, ulong? integrationId, bool isPremiumSubscriber) + { + BotId = botId; + IntegrationId = integrationId; + IsPremiumSubscriberRole = isPremiumSubscriber; + } + } +} diff --git a/src/Discord.Net.Core/Entities/Users/IGuildUser.cs b/src/Discord.Net.Core/Entities/Users/IGuildUser.cs index 0e4004c46..492cb9566 100644 --- a/src/Discord.Net.Core/Entities/Users/IGuildUser.cs +++ b/src/Discord.Net.Core/Entities/Users/IGuildUser.cs @@ -68,6 +68,11 @@ namespace Discord /// IReadOnlyCollection RoleIds { get; } + /// + /// Whether the user has passed the guild's Membership Screening requirements. + /// + bool? IsPending { get; } + /// /// Gets the level permissions granted to this user to a given channel. /// @@ -108,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. /// @@ -119,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. @@ -128,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. @@ -137,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 c36fb2326..9596a8338 100644 --- a/src/Discord.Net.Core/Entities/Users/IUser.cs +++ b/src/Discord.Net.Core/Entities/Users/IUser.cs @@ -75,9 +75,19 @@ namespace Discord /// Gets the username for this user. /// string Username { get; } + /// + /// Gets the public flags that are applied to this user's account. + /// + /// + /// This value is determined by bitwise OR-ing values together. + /// + /// + /// The value of public flags for this user. + /// + 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. @@ -92,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. @@ -100,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 4f7272daa..68232b254 100644 --- a/src/Discord.Net.Core/Entities/Users/UserProperties.cs +++ b/src/Discord.Net.Core/Entities/Users/UserProperties.cs @@ -10,32 +10,60 @@ namespace Discord /// None = 0, /// - /// Flag given to Discord staff. + /// Flag given to users who are a Discord employee. /// - Staff = 0b1, + Staff = 1 << 0, /// - /// Flag given to Discord partners. + /// Flag given to users who are owners of a partnered Discord server. /// - Partner = 0b10, + Partner = 1 << 1, /// - /// Flag given to users who have participated in the bug report program. + /// Flag given to users in HypeSquad events. /// - BugHunter = 0b1000, + HypeSquadEvents = 1 << 2, + /// + /// Flag given to users who have participated in the bug report program and are level 1. + /// + BugHunterLevel1 = 1 << 3, /// /// Flag given to users who are in the HypeSquad House of Bravery. /// - HypeSquadBravery = 0b100_0000, + HypeSquadBravery = 1 << 6, /// /// Flag given to users who are in the HypeSquad House of Brilliance. /// - HypeSquadBrilliance = 0b1000_0000, + HypeSquadBrilliance = 1 << 7, /// /// Flag given to users who are in the HypeSquad House of Balance. /// - HypeSquadBalance = 0b1_0000_0000, + HypeSquadBalance = 1 << 8, /// /// Flag given to users who subscribed to Nitro before games were added. /// - EarlySupporter = 0b10_0000_0000, + EarlySupporter = 1 << 9, + /// + /// Flag given to users who are part of a team. + /// + TeamUser = 1 << 10, + /// + /// Flag given to users who represent Discord (System). + /// + System = 1 << 12, + /// + /// Flag given to users who have participated in the bug report program and are level 2. + /// + BugHunterLevel2 = 1 << 14, + /// + /// Flag given to users who are verified bots. + /// + VerifiedBot = 1 << 16, + /// + /// 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/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 fdcc12295..01d9f4dde 100644 --- a/src/Discord.Net.Core/Extensions/UserExtensions.cs +++ b/src/Discord.Net.Core/Extensions/UserExtensions.cs @@ -43,7 +43,7 @@ namespace Discord AllowedMentions allowedMentions = null, MessageComponent component = null) { - return await (await user.GetOrCreateDMChannelAsync().ConfigureAwait(false)).SendMessageAsync(text, isTTS, embed, options, allowedMentions, component: component).ConfigureAwait(false); + return await (await user.CreateDMChannelAsync().ConfigureAwait(false)).SendMessageAsync(text, isTTS, embed, options, allowedMentions, component: component).ConfigureAwait(false); } /// @@ -95,7 +95,7 @@ namespace Discord RequestOptions options = null, MessageComponent component = null) { - return await (await user.GetOrCreateDMChannelAsync().ConfigureAwait(false)).SendFileAsync(stream, filename, text, isTTS, embed, options, component: component).ConfigureAwait(false); + return await (await user.CreateDMChannelAsync().ConfigureAwait(false)).SendFileAsync(stream, filename, text, isTTS, embed, options, component: component).ConfigureAwait(false); } /// @@ -151,7 +151,7 @@ namespace Discord RequestOptions options = null, MessageComponent component = null) { - return await (await user.GetOrCreateDMChannelAsync().ConfigureAwait(false)).SendFileAsync(filePath, text, isTTS, embed, options, component: component).ConfigureAwait(false); + return await (await user.CreateDMChannelAsync().ConfigureAwait(false)).SendFileAsync(filePath, text, isTTS, embed, options, component: component).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 8ca3f031c..03b840830 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/Entities/Messages/AllowedMentions.cs b/src/Discord.Net.Rest/API/Common/AllowedMentions.cs similarity index 92% rename from src/Discord.Net.Rest/Entities/Messages/AllowedMentions.cs rename to src/Discord.Net.Rest/API/Common/AllowedMentions.cs index 8b8870be2..7737a464f 100644 --- a/src/Discord.Net.Rest/Entities/Messages/AllowedMentions.cs +++ b/src/Discord.Net.Rest/API/Common/AllowedMentions.cs @@ -2,7 +2,7 @@ using Newtonsoft.Json; namespace Discord.API { - public class AllowedMentions + internal class AllowedMentions { [JsonProperty("parse")] public Optional Parse { get; set; } diff --git a/src/Discord.Net.Rest/API/Common/AuditLogEntry.cs b/src/Discord.Net.Rest/API/Common/AuditLogEntry.cs index 80d9a9e97..7458a19cb 100644 --- a/src/Discord.Net.Rest/API/Common/AuditLogEntry.cs +++ b/src/Discord.Net.Rest/API/Common/AuditLogEntry.cs @@ -7,7 +7,7 @@ namespace Discord.API [JsonProperty("target_id")] public ulong? TargetId { get; set; } [JsonProperty("user_id")] - public ulong UserId { get; set; } + public ulong? UserId { get; set; } [JsonProperty("changes")] public AuditLogChange[] Changes { get; set; } 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/GuildMember.cs b/src/Discord.Net.Rest/API/Common/GuildMember.cs index 940eb925a..fc2092d6c 100644 --- a/src/Discord.Net.Rest/API/Common/GuildMember.cs +++ b/src/Discord.Net.Rest/API/Common/GuildMember.cs @@ -18,6 +18,8 @@ namespace Discord.API public Optional Deaf { get; set; } [JsonProperty("mute")] public Optional Mute { get; set; } + [JsonProperty("pending")] + public Optional Pending { get; set; } [JsonProperty("premium_since")] public Optional PremiumSince { get; set; } } diff --git a/src/Discord.Net.Rest/API/Common/Invite.cs b/src/Discord.Net.Rest/API/Common/Invite.cs index 649bc37ec..aba267f34 100644 --- a/src/Discord.Net.Rest/API/Common/Invite.cs +++ b/src/Discord.Net.Rest/API/Common/Invite.cs @@ -11,9 +11,15 @@ namespace Discord.API public Optional Guild { get; set; } [JsonProperty("channel")] public InviteChannel Channel { get; set; } + [JsonProperty("inviter")] + public Optional Inviter { get; set; } [JsonProperty("approximate_presence_count")] public Optional PresenceCount { get; set; } [JsonProperty("approximate_member_count")] public Optional MemberCount { get; set; } + [JsonProperty("target_user")] + public Optional TargetUser { get; set; } + [JsonProperty("target_user_type")] + public Optional TargetUserType { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Common/InviteMetadata.cs b/src/Discord.Net.Rest/API/Common/InviteMetadata.cs index ca019b79b..f818de699 100644 --- a/src/Discord.Net.Rest/API/Common/InviteMetadata.cs +++ b/src/Discord.Net.Rest/API/Common/InviteMetadata.cs @@ -6,19 +6,15 @@ namespace Discord.API { internal class InviteMetadata : Invite { - [JsonProperty("inviter")] - public User Inviter { get; set; } [JsonProperty("uses")] - public Optional Uses { get; set; } + public int Uses { get; set; } [JsonProperty("max_uses")] - public Optional MaxUses { get; set; } + public int MaxUses { get; set; } [JsonProperty("max_age")] - public Optional MaxAge { get; set; } + public int MaxAge { get; set; } [JsonProperty("temporary")] public bool Temporary { get; set; } [JsonProperty("created_at")] - public Optional CreatedAt { get; set; } - [JsonProperty("revoked")] - public bool Revoked { get; set; } + public DateTimeOffset CreatedAt { 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 4ce8956eb..0474fec5b 100644 --- a/src/Discord.Net.Rest/API/Common/Message.cs +++ b/src/Discord.Net.Rest/API/Common/Message.cs @@ -60,5 +60,7 @@ namespace Discord.API public Optional ReferencedMessage { get; set; } [JsonProperty("components")] public Optional Components { get; set; } + [JsonProperty("stickers")] + public Optional Stickers { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Common/MessageFlags.cs b/src/Discord.Net.Rest/API/Common/MessageFlags.cs deleted file mode 100644 index ebe4e80ca..000000000 --- a/src/Discord.Net.Rest/API/Common/MessageFlags.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; - -namespace Discord.API -{ - [Flags] - internal enum MessageFlags : byte // probably safe to constrain this to 8 values, if not, it's internal so who cares - { - Suppressed = 0x04, - } -} 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 856a8695f..c655175da 100644 --- a/src/Discord.Net.Rest/API/Common/Role.cs +++ b/src/Discord.Net.Rest/API/Common/Role.cs @@ -1,4 +1,4 @@ -#pragma warning disable CS1591 +#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API @@ -18,8 +18,10 @@ 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")] + public Optional Tags { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Common/RoleTags.cs b/src/Discord.Net.Rest/API/Common/RoleTags.cs new file mode 100644 index 000000000..6446f2037 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/RoleTags.cs @@ -0,0 +1,15 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class RoleTags + { + [JsonProperty("bot_id")] + public Optional BotId { get; set; } + [JsonProperty("integration_id")] + public Optional IntegrationId { get; set; } + [JsonProperty("premium_subscriber")] + public Optional IsPremiumSubscriber { get; set; } + } +} 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/User.cs b/src/Discord.Net.Rest/API/Common/User.cs index 2eff3753d..d1f436afb 100644 --- a/src/Discord.Net.Rest/API/Common/User.cs +++ b/src/Discord.Net.Rest/API/Common/User.cs @@ -29,5 +29,7 @@ namespace Discord.API public Optional PremiumType { get; set; } [JsonProperty("locale")] public Optional Locale { get; set; } + [JsonProperty("public_flags")] + public Optional PublicFlags { 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/ModifyMessageParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyMessageParams.cs index 195525afc..69d962767 100644 --- a/src/Discord.Net.Rest/API/Rest/ModifyMessageParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyMessageParams.cs @@ -12,5 +12,9 @@ namespace Discord.API.Rest public Optional Embed { get; set; } [JsonProperty("components")] public Optional Components { get; set; } + [JsonProperty("flags")] + public Optional Flags { get; set; } + [JsonProperty("allowed_mentions")] + public Optional AllowedMentions { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Rest/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/API/Rest/UploadFileParams.cs b/src/Discord.Net.Rest/API/Rest/UploadFileParams.cs index 27336a11a..31a975aab 100644 --- a/src/Discord.Net.Rest/API/Rest/UploadFileParams.cs +++ b/src/Discord.Net.Rest/API/Rest/UploadFileParams.cs @@ -52,6 +52,8 @@ namespace Discord.API.Rest payload["components"] = MessageComponent.Value; if (IsSpoiler) payload["hasSpoiler"] = IsSpoiler.ToString(); + if (MessageReference.IsSpecified) + payload["message_reference"] = MessageReference.Value; var json = new StringBuilder(); using (var text = new StringWriter(json)) diff --git a/src/Discord.Net.Rest/ClientHelper.cs b/src/Discord.Net.Rest/ClientHelper.cs index f998b72f4..7bb358775 100644 --- a/src/Discord.Net.Rest/ClientHelper.cs +++ b/src/Discord.Net.Rest/ClientHelper.cs @@ -17,7 +17,7 @@ namespace Discord.Rest return RestApplication.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); @@ -45,13 +45,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) { @@ -60,7 +60,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) { @@ -69,14 +69,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) { @@ -85,7 +77,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( @@ -136,7 +128,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) { @@ -221,5 +213,11 @@ namespace Discord.Rest return response.Select(x => RestGuildCommand.Create(client, x, guildId)).ToImmutableArray(); } + + 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 9f2e90730..60f0bcc32 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -45,19 +45,17 @@ namespace Discord.API internal string AuthToken { get; private set; } 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) + JsonSerializer serializer = null, bool useSystemClock = true) { _restClientProvider = restClientProvider; UserAgent = userAgent; DefaultRetryMode = defaultRetryMode; _serializer = serializer ?? new JsonSerializer { ContractResolver = new DiscordContractResolver() }; - RateLimitPrecision = rateLimitPrecision; UseSystemClock = useSystemClock; RequestQueue = new RequestQueue(); @@ -74,14 +72,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)), @@ -522,6 +518,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) { @@ -607,16 +640,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)); @@ -1170,7 +1193,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) @@ -1183,32 +1206,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) @@ -1514,6 +1511,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 d62a83d2d..5059d437d 100644 --- a/src/Discord.Net.Rest/DiscordRestClient.cs +++ b/src/Discord.Net.Rest/DiscordRestClient.cs @@ -29,10 +29,7 @@ namespace Discord.Rest internal DiscordRestClient(DiscordRestConfig config, API.DiscordRestApiClient api) : base(config, api) { } private static API.DiscordRestApiClient CreateApiClient(DiscordRestConfig config) - => new API.DiscordRestApiClient(config.RestClientProvider, - DiscordRestConfig.UserAgent, - rateLimitPrecision: config.RateLimitPrecision, - useSystemClock: config.UseSystemClock); + => new API.DiscordRestApiClient(config.RestClientProvider, DiscordRestConfig.UserAgent, useSystemClock: config.UseSystemClock); internal override void Dispose(bool disposing) { @@ -80,9 +77,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) @@ -119,7 +113,19 @@ namespace Discord.Rest => ClientHelper.GetGlobalApplicationCommands(this, options); public Task> GetGuildApplicationCommands(ulong guildId, RequestOptions options = null) => ClientHelper.GetGuildApplicationCommands(this, guildId, 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/AuditLogs/DataTypes/ChannelInfo.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelInfo.cs index 0284b63f5..f50d9eeb3 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelInfo.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelInfo.cs @@ -5,13 +5,14 @@ namespace Discord.Rest /// public struct ChannelInfo { - internal ChannelInfo(string name, string topic, int? rateLimit, bool? nsfw, int? bitrate) + internal ChannelInfo(string name, string topic, int? rateLimit, bool? nsfw, int? bitrate, ChannelType? type) { Name = name; Topic = topic; SlowModeInterval = rateLimit; IsNsfw = nsfw; Bitrate = bitrate; + ChannelType = type; } /// @@ -53,5 +54,12 @@ namespace Discord.Rest /// null if this is not mentioned in this entry. /// public int? Bitrate { get; } + /// + /// Gets the type of this channel. + /// + /// + /// The channel type of this channel; null if not applicable. + /// + public ChannelType? ChannelType { get; } } } diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelUpdateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelUpdateAuditLogData.cs index fa5233145..b2294f183 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelUpdateAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelUpdateAuditLogData.cs @@ -26,6 +26,7 @@ namespace Discord.Rest var rateLimitPerUserModel = changes.FirstOrDefault(x => x.ChangedProperty == "rate_limit_per_user"); var nsfwModel = changes.FirstOrDefault(x => x.ChangedProperty == "nsfw"); var bitrateModel = changes.FirstOrDefault(x => x.ChangedProperty == "bitrate"); + var typeModel = changes.FirstOrDefault(x => x.ChangedProperty == "type"); string oldName = nameModel?.OldValue?.ToObject(discord.ApiClient.Serializer), newName = nameModel?.NewValue?.ToObject(discord.ApiClient.Serializer); @@ -37,9 +38,11 @@ namespace Discord.Rest newNsfw = nsfwModel?.NewValue?.ToObject(discord.ApiClient.Serializer); int? oldBitrate = bitrateModel?.OldValue?.ToObject(discord.ApiClient.Serializer), newBitrate = bitrateModel?.NewValue?.ToObject(discord.ApiClient.Serializer); + ChannelType? oldType = typeModel?.OldValue?.ToObject(discord.ApiClient.Serializer), + newType = typeModel?.NewValue?.ToObject(discord.ApiClient.Serializer); - var before = new ChannelInfo(oldName, oldTopic, oldRateLimitPerUser, oldNsfw, oldBitrate); - var after = new ChannelInfo(newName, newTopic, newRateLimitPerUser, newNsfw, newBitrate); + var before = new ChannelInfo(oldName, oldTopic, oldRateLimitPerUser, oldNsfw, oldBitrate, oldType); + var after = new ChannelInfo(newName, newTopic, newRateLimitPerUser, newNsfw, newBitrate, newType); return new ChannelUpdateAuditLogData(entry.TargetId.Value, before, after); } diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessagePinAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessagePinAuditLogData.cs index 020171152..be66ac846 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessagePinAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessagePinAuditLogData.cs @@ -19,8 +19,14 @@ namespace Discord.Rest internal static MessagePinAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) { - var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId); - return new MessagePinAuditLogData(entry.Options.MessageId.Value, entry.Options.ChannelId.Value, RestUser.Create(discord, userInfo)); + RestUser user = null; + if (entry.TargetId.HasValue) + { + var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId); + user = RestUser.Create(discord, userInfo); + } + + return new MessagePinAuditLogData(entry.Options.MessageId.Value, entry.Options.ChannelId.Value, user); } /// @@ -38,10 +44,10 @@ namespace Discord.Rest /// public ulong ChannelId { get; } /// - /// Gets the user of the message that was pinned. + /// Gets the user of the message that was pinned if available. /// /// - /// A user object representing the user that created the pinned message. + /// A user object representing the user that created the pinned message or . /// public IUser Target { get; } } diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageUnpinAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageUnpinAuditLogData.cs index 1b3ff96f3..b4fa389cc 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageUnpinAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageUnpinAuditLogData.cs @@ -19,8 +19,14 @@ namespace Discord.Rest internal static MessageUnpinAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) { - var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId); - return new MessageUnpinAuditLogData(entry.Options.MessageId.Value, entry.Options.ChannelId.Value, RestUser.Create(discord, userInfo)); + RestUser user = null; + if (entry.TargetId.HasValue) + { + var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId); + user = RestUser.Create(discord, userInfo); + } + + return new MessageUnpinAuditLogData(entry.Options.MessageId.Value, entry.Options.ChannelId.Value, user); } /// @@ -38,10 +44,10 @@ namespace Discord.Rest /// public ulong ChannelId { get; } /// - /// Gets the user of the message that was unpinned. + /// Gets the user of the message that was unpinned if available. /// /// - /// A user object representing the user that created the unpinned message. + /// A user object representing the user that created the unpinned message or . /// public IUser Target { get; } } diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/RestAuditLogEntry.cs b/src/Discord.Net.Rest/Entities/AuditLogs/RestAuditLogEntry.cs index d604077f4..2176eab71 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/RestAuditLogEntry.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/RestAuditLogEntry.cs @@ -22,7 +22,7 @@ namespace Discord.Rest internal static RestAuditLogEntry Create(BaseDiscordClient discord, Model fullLog, EntryModel model) { - var userInfo = fullLog.Users.FirstOrDefault(x => x.Id == model.UserId); + var userInfo = model.UserId != null ? fullLog.Users.FirstOrDefault(x => x.Id == model.UserId) : null; IUser user = null; if (userInfo != null) user = RestUser.Create(discord, userInfo); diff --git a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs index 894b820c8..f52174675 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 1d12ee944..7bfd73ee6 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 0f814a2d5..fb7a7a215 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, MessageComponent component = null) diff --git a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs index cd2230a61..800ee1d8d 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/RestInvite.cs b/src/Discord.Net.Rest/Entities/Invites/RestInvite.cs index 153eb6c41..95b454c20 100644 --- a/src/Discord.Net.Rest/Entities/Invites/RestInvite.cs +++ b/src/Discord.Net.Rest/Entities/Invites/RestInvite.cs @@ -21,6 +21,12 @@ namespace Discord.Rest public ulong ChannelId { get; private set; } /// public ulong? GuildId { get; private set; } + /// + public IUser Inviter { get; private set; } + /// + public IUser TargetUser { get; private set; } + /// + public TargetUserType TargetUserType { get; private set; } internal IChannel Channel { get; } internal IGuild Guild { get; } @@ -50,6 +56,9 @@ namespace Discord.Rest MemberCount = model.MemberCount.IsSpecified ? model.MemberCount.Value : null; PresenceCount = model.PresenceCount.IsSpecified ? model.PresenceCount.Value : null; ChannelType = (ChannelType)model.Channel.Type; + Inviter = model.Inviter.IsSpecified ? RestUser.Create(Discord, model.Inviter.Value) : null; + TargetUser = model.TargetUser.IsSpecified ? RestUser.Create(Discord, model.TargetUser.Value) : null; + TargetUserType = model.TargetUserType.IsSpecified ? model.TargetUserType.Value : TargetUserType.Undefined; } /// diff --git a/src/Discord.Net.Rest/Entities/Invites/RestInviteMetadata.cs b/src/Discord.Net.Rest/Entities/Invites/RestInviteMetadata.cs index 55acd5f45..a0ed9ec81 100644 --- a/src/Discord.Net.Rest/Entities/Invites/RestInviteMetadata.cs +++ b/src/Discord.Net.Rest/Entities/Invites/RestInviteMetadata.cs @@ -6,10 +6,8 @@ namespace Discord.Rest /// Represents additional information regarding the REST-based invite object. public class RestInviteMetadata : RestInvite, IInviteMetadata { - private long? _createdAtTicks; + private long _createdAtTicks; - /// - public bool IsRevoked { get; private set; } /// public bool IsTemporary { get; private set; } /// @@ -18,10 +16,6 @@ namespace Discord.Rest public int? MaxUses { get; private set; } /// public int? Uses { get; private set; } - /// - /// Gets the user that created this invite. - /// - public RestUser Inviter { get; private set; } /// public DateTimeOffset? CreatedAt => DateTimeUtils.FromTicks(_createdAtTicks); @@ -39,16 +33,11 @@ namespace Discord.Rest internal void Update(Model model) { base.Update(model); - Inviter = model.Inviter != null ? RestUser.Create(Discord, model.Inviter) : null; - IsRevoked = model.Revoked; IsTemporary = model.Temporary; - MaxAge = model.MaxAge.IsSpecified ? model.MaxAge.Value : (int?)null; - MaxUses = model.MaxUses.IsSpecified ? model.MaxUses.Value : (int?)null; - Uses = model.Uses.IsSpecified ? model.Uses.Value : (int?)null; - _createdAtTicks = model.CreatedAt.IsSpecified ? model.CreatedAt.Value.UtcTicks : (long?)null; + MaxAge = model.MaxAge; + MaxUses = model.MaxUses; + Uses = model.Uses; + _createdAtTicks = model.CreatedAt.UtcTicks; } - - /// - IUser IInviteMetadata.Inviter => Inviter; } } diff --git a/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs b/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs index 85b040f43..b24db3960 100644 --- a/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs +++ b/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs @@ -27,26 +27,93 @@ namespace Discord.Rest public static async Task ModifyAsync(IMessage msg, BaseDiscordClient client, Action func, RequestOptions options) { - if (msg.Author.Id != client.CurrentUser.Id) - throw new InvalidOperationException("Only the author of a message may modify the message."); - var args = new MessageProperties(); func(args); + if (msg.Author.Id != client.CurrentUser.Id && (args.Content.IsSpecified || args.Embed.IsSpecified || args.AllowedMentions.IsSpecified)) + throw new InvalidOperationException("Only the author of a message may modify the message content, embed, or allowed mentions."); + bool hasText = args.Content.IsSpecified ? !string.IsNullOrEmpty(args.Content.Value) : !string.IsNullOrEmpty(msg.Content); bool hasEmbed = args.Embed.IsSpecified ? args.Embed.Value != null : msg.Embeds.Any(); if (!hasText && !hasEmbed) Preconditions.NotNullOrEmpty(args.Content.IsSpecified ? args.Content.Value : string.Empty, nameof(args.Content)); + 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(), Components = args.Components.IsSpecified ? args.Components.Value?.Components.Select(x => new API.ActionRowComponent(x)).ToArray() : Optional.Unspecified + Flags = args.Flags.IsSpecified ? args.Flags.Value : Optional.Create(), + AllowedMentions = args.AllowedMentions.IsSpecified ? args.AllowedMentions.Value.ToModel() : Optional.Create(), }; 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); @@ -56,13 +123,14 @@ 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(ulong channelId, ulong messageId, IEmote emote, BaseDiscordClient client, RequestOptions options) + { + 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) @@ -70,16 +138,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 90bdc8636..9f62c45be 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); @@ -67,6 +69,8 @@ namespace Discord.Rest public MessageApplication Application { get; private set; } /// public MessageReference Reference { get; private set; } + /// + public MessageFlags? Flags { get; private set; } /// public IReadOnlyCollection Components { get; private set; } @@ -143,6 +147,9 @@ namespace Discord.Rest else Components = new List(); + if (model.Flags.IsSpecified) + Flags = model.Flags.Value; + if (model.Reactions.IsSpecified) { var value = model.Reactions.Value; @@ -187,8 +194,13 @@ namespace Discord.Rest IReadOnlyCollection IMessage.Embeds => Embeds; /// IReadOnlyCollection IMessage.MentionedUserIds => MentionedUsers.Select(x => x.Id).ToImmutableArray(); + /// IReadOnlyCollection IMessage.Components => Components; + + /// + 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/RestUserMessage.cs b/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs index 2c76a4253..aa6b44da6 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs @@ -13,7 +13,7 @@ namespace Discord.Rest [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class RestUserMessage : RestMessage, IUserMessage { - private bool _isMentioningEveryone, _isTTS, _isPinned, _isSuppressed; + private bool _isMentioningEveryone, _isTTS, _isPinned; private long? _editedTimestampTicks; private IUserMessage _referencedMessage; private ImmutableArray _attachments = ImmutableArray.Create(); @@ -21,13 +21,14 @@ 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; /// public override bool IsPinned => _isPinned; /// - public override bool IsSuppressed => _isSuppressed; + public override bool IsSuppressed => Flags.HasValue && Flags.Value.HasFlag(MessageFlags.SuppressEmbeds); /// public override DateTimeOffset? EditedTimestamp => DateTimeUtils.FromTicks(_editedTimestampTicks); /// @@ -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) @@ -70,10 +73,6 @@ namespace Discord.Rest _editedTimestampTicks = model.EditedTimestamp.Value?.UtcTicks; if (model.MentionEveryone.IsSpecified) _isMentioningEveryone = model.MentionEveryone.Value; - if (model.Flags.IsSpecified) - { - _isSuppressed = model.Flags.Value.HasFlag(API.MessageFlags.Suppressed); - } if (model.RoleMentions.IsSpecified) _roleMentionIds = model.RoleMentions.Value.ToImmutableArray(); @@ -136,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(); + } } /// @@ -151,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/RestRole.cs b/src/Discord.Net.Rest/Entities/Roles/RestRole.cs index 7c1a3aaa2..aa33ae7e5 100644 --- a/src/Discord.Net.Rest/Entities/Roles/RestRole.cs +++ b/src/Discord.Net.Rest/Entities/Roles/RestRole.cs @@ -26,6 +26,8 @@ namespace Discord.Rest public GuildPermissions Permissions { get; private set; } /// public int Position { get; private set; } + /// + public RoleTags Tags { get; private set; } /// public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); @@ -56,6 +58,8 @@ namespace Discord.Rest Position = model.Position; Color = new Color(model.Color); Permissions = new GuildPermissions(model.Permissions); + if (model.Tags.IsSpecified) + Tags = model.Tags.Value.ToEntity(); } /// 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/Users/RestGuildUser.cs b/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs index d6a8c2eda..6e6bbe09c 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs @@ -29,6 +29,8 @@ namespace Discord.Rest public DateTimeOffset? PremiumSince => DateTimeUtils.FromTicks(_premiumSinceTicks); /// public ulong GuildId => Guild.Id; + /// + public bool? IsPending { get; private set; } /// /// Resolving permissions requires the parent guild to be downloaded. @@ -73,6 +75,8 @@ namespace Discord.Rest UpdateRoles(model.Roles.Value); if (model.PremiumSince.IsSpecified) _premiumSinceTicks = model.PremiumSince.Value?.UtcTicks; + if (model.Pending.IsSpecified) + IsPending = model.Pending.Value; } private void UpdateRoles(ulong[] roleIds) { @@ -108,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 f5becd3ff..7bc1447fe 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestUser.cs @@ -21,6 +21,8 @@ namespace Discord.Rest public ushort DiscriminatorValue { get; private set; } /// public string AvatarId { get; private set; } + /// + public UserProperties? PublicFlags { get; private set; } /// public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); @@ -65,6 +67,8 @@ namespace Discord.Rest IsBot = model.Bot.Value; if (model.Username.IsSpecified) Username = model.Username.Value; + if (model.PublicFlags.IsSpecified) + PublicFlags = model.PublicFlags.Value; } /// @@ -75,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); /// @@ -103,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 8462cb8d4..2131fec93 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestWebhookUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestWebhookUser.cs @@ -52,32 +52,42 @@ namespace Discord.Rest /// string IGuildUser.Nickname => null; /// + bool? IGuildUser.IsPending => null; + /// GuildPermissions IGuildUser.GuildPermissions => GuildPermissions.Webhook; /// 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/Entities/Webhooks/RestWebhook.cs b/src/Discord.Net.Rest/Entities/Webhooks/RestWebhook.cs index 1fdc95a63..9baddf003 100644 --- a/src/Discord.Net.Rest/Entities/Webhooks/RestWebhook.cs +++ b/src/Discord.Net.Rest/Entities/Webhooks/RestWebhook.cs @@ -11,11 +11,11 @@ namespace Discord.Rest internal IGuild Guild { get; private set; } internal ITextChannel Channel { get; private set; } - /// - public ulong ChannelId { get; } /// public string Token { get; } + /// + public ulong ChannelId { get; private set; } /// public string Name { get; private set; } /// @@ -56,6 +56,8 @@ namespace Discord.Rest internal void Update(Model model) { + if (ChannelId != model.ChannelId) + ChannelId = model.ChannelId; if (model.Avatar.IsSpecified) AvatarId = model.Avatar.Value; if (model.Creator.IsSpecified) diff --git a/src/Discord.Net.Rest/Extensions/EntityExtensions.cs b/src/Discord.Net.Rest/Extensions/EntityExtensions.cs index 8e1b9c6d8..f8676c783 100644 --- a/src/Discord.Net.Rest/Extensions/EntityExtensions.cs +++ b/src/Discord.Net.Rest/Extensions/EntityExtensions.cs @@ -34,6 +34,13 @@ namespace Discord.Rest model.Thumbnail.IsSpecified ? model.Thumbnail.Value.ToEntity() : (EmbedThumbnail?)null, model.Fields.IsSpecified ? model.Fields.Value.Select(x => x.ToEntity()).ToImmutableArray() : ImmutableArray.Create()); } + public static RoleTags ToEntity(this API.RoleTags model) + { + return new RoleTags( + model.BotId.IsSpecified ? model.BotId.Value : null, + model.IntegrationId.IsSpecified ? model.IntegrationId.Value : null, + model.IsPremiumSubscriber.IsSpecified ? true : false); + } public static API.Embed ToModel(this Embed entity) { if (entity == null) return null; 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/InviteCreatedEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/InviteCreatedEvent.cs new file mode 100644 index 000000000..8f8002029 --- /dev/null +++ b/src/Discord.Net.WebSocket/API/Gateway/InviteCreatedEvent.cs @@ -0,0 +1,32 @@ +using Discord.API; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.API.Gateway +{ + internal class InviteCreatedEvent + { + [JsonProperty("channel_id")] + public ulong ChannelID { get; set; } + [JsonProperty("code")] + public string InviteCode { get; set; } + [JsonProperty("timestamp")] + public Optional RawTimestamp { get; set; } + [JsonProperty("guild_id")] + public ulong? GuildID { get; set; } + [JsonProperty("inviter")] + public Optional inviter { get; set; } + [JsonProperty("max_age")] + public int RawAge { get; set; } + [JsonProperty("max_uses")] + public int MaxUsers { get; set; } + [JsonProperty("temporary")] + public bool TempInvite { get; set; } + [JsonProperty("uses")] + public int Uses { get; set; } + } +} diff --git a/src/Discord.Net.WebSocket/API/Gateway/InviteDeletedEvent.cs b/src/Discord.Net.WebSocket/API/Gateway/InviteDeletedEvent.cs new file mode 100644 index 000000000..6bdd337f5 --- /dev/null +++ b/src/Discord.Net.WebSocket/API/Gateway/InviteDeletedEvent.cs @@ -0,0 +1,19 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.WebSocket +{ + internal class InviteDeletedEvent + { + [JsonProperty("channel_id")] + public ulong ChannelID { get; set; } + [JsonProperty("guild_id")] + public Optional GuildID { get; set; } + [JsonProperty("code")] + public string Code { 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 af1a9f147..3b6b3ec5a 100644 --- a/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs +++ b/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs @@ -127,12 +127,12 @@ 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. /// /// @@ -159,12 +159,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. /// /// @@ -222,26 +222,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. /// @@ -258,12 +255,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. @@ -369,12 +366,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 { @@ -397,12 +393,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 { diff --git a/src/Discord.Net.WebSocket/BaseSocketClient.cs b/src/Discord.Net.WebSocket/BaseSocketClient.cs index 548bb75bf..1cfe6c8bf 100644 --- a/src/Discord.Net.WebSocket/BaseSocketClient.cs +++ b/src/Discord.Net.WebSocket/BaseSocketClient.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.IO; using System.Threading.Tasks; @@ -46,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. /// @@ -69,19 +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. - /// - 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); /// @@ -162,14 +155,23 @@ namespace Discord.WebSocket /// public abstract SocketGuild GetGuild(ulong id); /// + /// Gets all voice regions. + /// + /// The options to be used when sending the request. + /// + /// A task that contains a read-only collection of REST-based voice regions. + /// + public abstract ValueTask> GetVoiceRegionsAsync(RequestOptions options = null); + /// /// Gets a voice region. /// /// The identifier of the voice region (e.g. eu-central ). + /// The options to be used when sending the request. /// - /// A REST-based voice region associated with the identifier; null if the voice region is not - /// found. + /// A task that contains a REST-based voice region associated with the identifier; null if the + /// voice region is not found. /// - public abstract RestVoiceRegion GetVoiceRegion(string id); + public abstract ValueTask GetVoiceRegionAsync(string id, RequestOptions options = null); /// public abstract Task StartAsync(); /// @@ -188,6 +190,12 @@ namespace Discord.WebSocket /// The name of the game. /// If streaming, the URL of the stream. Must be a valid Twitch URL. /// The type of the game. + /// + /// + /// Bot accounts cannot set as their activity + /// type and it will have no effect. + /// + /// /// /// A task that represents the asynchronous set operation. /// @@ -201,6 +209,10 @@ namespace Discord.WebSocket /// Discord will only accept setting of name and the type of activity. /// /// + /// Bot accounts cannot set as their activity + /// type and it will have no effect. + /// + /// /// Rich Presence cannot be set via this method or client. Rich Presence is strictly limited to RPC /// clients only. /// @@ -296,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/DiscordShardedClient.cs b/src/Discord.Net.WebSocket/DiscordShardedClient.cs index 7d62596f7..386f9f7e5 100644 --- a/src/Discord.Net.WebSocket/DiscordShardedClient.cs +++ b/src/Discord.Net.WebSocket/DiscordShardedClient.cs @@ -36,13 +36,13 @@ namespace Discord.WebSocket /// public override IReadOnlyCollection PrivateChannels => GetPrivateChannels().ToReadOnlyCollection(GetPrivateChannelCount); public IReadOnlyCollection Shards => _shards; - /// - 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()) { } @@ -90,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,8 +263,16 @@ namespace Discord.WebSocket } /// - public override RestVoiceRegion GetVoiceRegion(string id) - => _shards[0].GetVoiceRegion(id); + public override async ValueTask> GetVoiceRegionsAsync(RequestOptions options = null) + { + return await _shards[0].GetVoiceRegionsAsync().ConfigureAwait(false); + } + + /// + public override async ValueTask GetVoiceRegionAsync(string id, RequestOptions options = null) + { + return await _shards[0].GetVoiceRegionAsync(id, options).ConfigureAwait(false); + } /// /// is @@ -325,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); @@ -420,11 +419,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..d1407da01 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) @@ -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 76c17a2ec..c0f4263fb 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 bool AlwaysAcknowledgeInteractions { get; private set; } internal new DiscordSocketApiClient ApiClient => base.ApiClient as DiscordSocketApiClient; @@ -110,8 +108,6 @@ namespace Discord.WebSocket /// public IReadOnlyCollection GroupChannels => State.PrivateChannels.OfType().ToImmutableArray(); - /// - public override IReadOnlyCollection VoiceRegions => _voiceRegions.ToReadOnlyCollection(); /// /// Initializes a new REST/WebSocket-based Discord client. @@ -137,11 +133,9 @@ namespace Discord.WebSocket AlwaysDownloadUsers = config.AlwaysDownloadUsers; AlwaysAcknowledgeInteractions = config.AlwaysAcknowledgeInteractions; 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); @@ -180,12 +174,10 @@ namespace Discord.WebSocket return Task.Delay(0); }; - _voiceRegions = ImmutableDictionary.Create(); _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) { @@ -203,24 +195,12 @@ namespace Discord.WebSocket base.Dispose(disposing); } - /// - internal override async Task OnLoginAsync(TokenType tokenType, string token) - { - if (_parentClient == null) - { - var voiceRegions = await ApiClient.GetVoiceRegionsAsync(new RequestOptions { IgnoreState = true, RetryMode = RetryMode.AlwaysRetry }).ConfigureAwait(false); - _voiceRegions = voiceRegions.Select(x => RestVoiceRegion.Create(this, x)).ToImmutableDictionary(x => x.Id); - } - else - _voiceRegions = _parentClient._voiceRegions; - await Rest.OnLoginAsync(tokenType, token); - } /// internal override async Task OnLogoutAsync() { await StopAsync().ConfigureAwait(false); _applicationInfo = null; - _voiceRegions = ImmutableDictionary.Create(); + _voiceRegions = null; await Rest.OnLogoutAsync(); } @@ -252,17 +232,17 @@ 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); } - - //Wait for READY - await _connection.WaitAsync().ConfigureAwait(false); } finally { if (locked) _shardedClient.ReleaseIdentifyLock(); } + + //Wait for READY + await _connection.WaitAsync().ConfigureAwait(false); } private async Task OnDisconnectingAsync(Exception ex) { @@ -311,13 +291,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) @@ -331,12 +349,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) { @@ -344,7 +361,7 @@ 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; }); } @@ -352,11 +369,34 @@ namespace Discord.WebSocket => State.RemoveUser(id); /// - public override RestVoiceRegion GetVoiceRegion(string id) + public override async ValueTask> GetVoiceRegionsAsync(RequestOptions options = null) { - if (_voiceRegions.TryGetValue(id, out RestVoiceRegion region)) - return region; - return null; + if (_parentClient == null) + { + if (_voiceRegions == null) + { + options = RequestOptions.CreateOrClone(options); + options.IgnoreState = true; + var voiceRegions = await ApiClient.GetVoiceRegionsAsync(options).ConfigureAwait(false); + _voiceRegions = voiceRegions.Select(x => RestVoiceRegion.Create(this, x)).ToImmutableDictionary(x => x.Id); + } + return _voiceRegions.ToReadOnlyCollection(); + } + return await _parentClient.GetVoiceRegionsAsync().ConfigureAwait(false); + } + + /// + public override async ValueTask GetVoiceRegionAsync(string id, RequestOptions options = null) + { + if (_parentClient == null) + { + if (_voiceRegions == null) + await GetVoiceRegionsAsync().ConfigureAwait(false); + if (_voiceRegions.TryGetValue(id, out RestVoiceRegion region)) + return region; + return null; + } + return await _parentClient.GetVoiceRegionAsync(id, options).ConfigureAwait(false); } /// @@ -450,7 +490,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); @@ -545,7 +586,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 { @@ -553,7 +594,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: @@ -576,7 +617,8 @@ 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; int unavailableGuilds = 0; for (int i = 0; i < data.Guilds.Length; i++) @@ -730,7 +772,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) @@ -747,7 +790,7 @@ namespace Discord.WebSocket { await UnknownGuildAsync(type, data.Id).ConfigureAwait(false); return; - } + }*/ } break; case "GUILD_DELETE": @@ -947,15 +990,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 @@ -1218,56 +1261,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": @@ -1275,52 +1325,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": @@ -1328,26 +1411,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 channel = GetChannel(data.ChannelId) as ISocketMessageChannel; - 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)); - - 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": @@ -1355,32 +1434,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 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); - 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": @@ -1388,32 +1478,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": @@ -1421,21 +1514,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": @@ -1443,70 +1535,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; @@ -1552,7 +1629,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 { @@ -1575,24 +1653,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; @@ -1664,7 +1744,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); @@ -2015,24 +2095,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) { @@ -2188,8 +2277,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); @@ -2219,18 +2308,18 @@ 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)); /// - Task> IDiscordClient.GetVoiceRegionsAsync(RequestOptions options) - => Task.FromResult>(VoiceRegions); + async Task> IDiscordClient.GetVoiceRegionsAsync(RequestOptions options) + => await GetVoiceRegionsAsync(options).ConfigureAwait(false); /// - Task IDiscordClient.GetVoiceRegionAsync(string id, RequestOptions options) - => Task.FromResult(GetVoiceRegion(id)); + async Task IDiscordClient.GetVoiceRegionAsync(string id, RequestOptions options) + => await GetVoiceRegionAsync(id, options).ConfigureAwait(false); /// async Task IDiscordClient.StartAsync() diff --git a/src/Discord.Net.WebSocket/DiscordSocketConfig.cs b/src/Discord.Net.WebSocket/DiscordSocketConfig.cs index e234240ef..61c625bdf 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketConfig.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketConfig.cs @@ -134,28 +134,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. /// @@ -195,14 +173,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 cfc93e139..fd3fd1002 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 93f2fde10..e559bdd4d 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 c085f65da..c0badd557 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 35797dc78..bd2945a71 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) @@ -1020,6 +989,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); /// @@ -1241,10 +1213,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; @@ -1299,10 +1267,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/ISocketInvite.cs b/src/Discord.Net.WebSocket/Entities/Invites/ISocketInvite.cs new file mode 100644 index 000000000..8fe82b089 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Invites/ISocketInvite.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.WebSocket +{ + public interface ISocketInvite + { + /// + /// Gets the unique identifier for this invite. + /// + /// + /// A string containing the invite code (e.g. FTqNnyS). + /// + string Code { get; } + /// + /// Gets the URL used to accept this invite + /// + /// + /// A string containing the full invite URL (e.g. https://discord.gg/FTqNnyS). + /// + string Url { get; } + + /// + /// Gets the channel this invite is linked to. + /// + /// + /// A generic channel that the invite points to. + /// + SocketGuildChannel Channel { get; } + + /// + /// Gets the guild this invite is linked to. + /// + /// + /// A guild object representing the guild that the invite points to. + /// + SocketGuild Guild { get; } + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Invites/InviteCache.cs b/src/Discord.Net.WebSocket/Entities/Invites/InviteCache.cs new file mode 100644 index 000000000..a203b780f --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Invites/InviteCache.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.WebSocket +{ + internal class InviteCache + { + private readonly ConcurrentDictionary _invites; + private readonly ConcurrentQueue _queue; + private static int _size; + + public InviteCache(DiscordSocketClient client) + { + //NOTE: + //This should be an option in the client config. default for now is 20 invites per guild + _size = client.Guilds.Count * 20; + + _invites = new ConcurrentDictionary(); + _queue = new ConcurrentQueue(); + } + public void Add(SocketGuildInvite invite) + { + if(_invites.TryAdd(invite.Code, invite)) + { + _queue.Enqueue(invite.Code); + + while (_queue.Count > _size && _queue.TryDequeue(out string invCode)) + _invites.TryRemove(invCode, out _); + } + } + public SocketGuildInvite Remove(string inviteCode) + { + _invites.TryRemove(inviteCode, out SocketGuildInvite inv); + return inv; + } + public SocketGuildInvite Get(string inviteCode) + { + if(_invites.TryGetValue(inviteCode, out SocketGuildInvite inv)) + return inv; + return null; + } + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Invites/SocketGuildInvite.cs b/src/Discord.Net.WebSocket/Entities/Invites/SocketGuildInvite.cs new file mode 100644 index 000000000..35fdf237c --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Invites/SocketGuildInvite.cs @@ -0,0 +1,112 @@ +using Discord.Rest; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization.Formatters; +using System.Text; +using System.Threading.Tasks; +using InviteUpdate = Discord.API.Gateway.InviteCreatedEvent; + +namespace Discord.WebSocket +{ + /// + /// Represents a guild invite + /// + public class SocketGuildInvite : SocketEntity, ISocketInvite + { + public string Code { get; private set; } + public string Url => $"{DiscordConfig.InviteUrl}{Code}"; + public SocketGuildChannel Channel { get; private set; } + public SocketGuild Guild { get; private set; } + /// + /// Gets the unique invite code + /// + /// Returns the unique invite code + /// + /// + public string Id => Code; + /// + /// Gets the user who created the invite + /// + /// Returns the user who created the invite + /// + /// + public SocketGuildUser Inviter { get; private set; } + /// + /// Gets the maximum number of times the invite can be used, if there is no limit then the value will be 0 + /// + /// Returns the maximum number of times the invite can be used, if there is no limit then the value will be 0 + /// + /// + public int? MaxUses { get; private set; } + /// + /// Gets whether or not the invite is temporary (invited users will be kicked on disconnect unless they're assigned a role) + /// + /// Returns whether or not the invite is temporary (invited users will be kicked on disconnect unless they're assigned a role) + /// + /// + public bool Temporary { get; private set; } + /// + /// Gets the time at which the invite was created + /// + /// Returns the time at which the invite was created + /// + /// + public DateTimeOffset? CreatedAt { get; private set; } + /// + /// Gets how long the invite is valid for + /// + /// Returns how long the invite is valid for (in seconds) + /// + /// + public TimeSpan? MaxAge { get; private set; } + + internal SocketGuildInvite(DiscordSocketClient _client, SocketGuild guild, SocketGuildChannel channel, string inviteCode, RestInviteMetadata rest) : base(_client, inviteCode) + { + Code = inviteCode; + Guild = guild; + Channel = channel; + CreatedAt = rest.CreatedAt; + Temporary = rest.IsTemporary; + MaxUses = rest.MaxUses; + Inviter = guild.GetUser(rest.Inviter.Id); + if (rest.MaxAge.HasValue) + MaxAge = TimeSpan.FromSeconds(rest.MaxAge.Value); + } + internal SocketGuildInvite(DiscordSocketClient _client, SocketGuild guild, SocketGuildChannel channel, string inviteCode, InviteUpdate Update) : base(_client, inviteCode) + { + Code = inviteCode; + Guild = guild; + Channel = channel; + + if (Update.RawTimestamp.IsSpecified) + CreatedAt = Update.RawTimestamp.Value; + else + CreatedAt = DateTimeOffset.Now; + + if (Update.inviter.IsSpecified) + Inviter = guild.GetUser(Update.inviter.Value.Id); + + Temporary = Update.TempInvite; + MaxUses = Update.MaxUsers; + MaxAge = TimeSpan.FromSeconds(Update.RawAge); + } + internal static SocketGuildInvite Create(DiscordSocketClient _client, SocketGuild guild, SocketGuildChannel channel, string inviteCode, InviteUpdate Update) + { + var invite = new SocketGuildInvite(_client, guild, channel, inviteCode, Update); + return invite; + } + internal static SocketGuildInvite CreateFromRest(DiscordSocketClient _client, SocketGuild guild, SocketGuildChannel channel, string inviteCode, RestInviteMetadata rest) + { + var invite = new SocketGuildInvite(_client, guild, channel, inviteCode, rest); + return invite; + } + /// + /// Deletes the invite + /// + /// + /// + public Task DeleteAsync(RequestOptions options = null) + => SocketInviteHelper.DeleteAsync(this, Discord, options); + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Invites/SocketInvite.cs b/src/Discord.Net.WebSocket/Entities/Invites/SocketInvite.cs index 902f13935..845b48b8b 100644 --- a/src/Discord.Net.WebSocket/Entities/Invites/SocketInvite.cs +++ b/src/Discord.Net.WebSocket/Entities/Invites/SocketInvite.cs @@ -49,8 +49,6 @@ namespace Discord.WebSocket /// int? IInvite.MemberCount => throw new NotImplementedException(); /// - bool IInviteMetadata.IsRevoked => throw new NotImplementedException(); - /// public bool IsTemporary { get; private set; } /// int? IInviteMetadata.MaxAge { get => MaxAge; } @@ -138,6 +136,8 @@ namespace Discord.WebSocket /// IChannel IInvite.Channel => Channel; /// - IUser IInviteMetadata.Inviter => Inviter; + IUser IInvite.Inviter => Inviter; + /// + IUser IInvite.TargetUser => TargetUser; } } diff --git a/src/Discord.Net.WebSocket/Entities/Invites/SocketInviteHelper.cs b/src/Discord.Net.WebSocket/Entities/Invites/SocketInviteHelper.cs new file mode 100644 index 000000000..3781739a9 --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Invites/SocketInviteHelper.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.WebSocket +{ + internal class SocketInviteHelper + { + public static async Task DeleteAsync(ISocketInvite invite, BaseSocketClient client, + RequestOptions options) + { + await client.ApiClient.DeleteInviteAsync(invite.Code, options).ConfigureAwait(false); + } + } +} diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs index e201d3e97..d0b9443a4 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 IReadOnlyCollection Components { get; private set; } + /// + public MessageFlags? Flags { 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) }); /// @@ -175,6 +180,9 @@ namespace Discord.WebSocket } else Components = new List(); + + if (model.Flags.IsSpecified) + Flags = model.Flags.Value; } /// @@ -207,8 +215,13 @@ namespace Discord.WebSocket IReadOnlyCollection IMessage.MentionedRoleIds => MentionedRoles.Select(x => x.Id).ToImmutableArray(); /// IReadOnlyCollection IMessage.MentionedUserIds => MentionedUsers.Select(x => x.Id).ToImmutableArray(); + /// IReadOnlyCollection IMessage.Components => Components; + + /// + IReadOnlyCollection IMessage.Stickers => Stickers; + internal void AddReaction(SocketReaction reaction) { _reactions.Add(reaction); diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs index bea0f45eb..597544f4d 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs @@ -15,7 +15,7 @@ namespace Discord.WebSocket [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class SocketUserMessage : SocketMessage, IUserMessage { - private bool _isMentioningEveryone, _isTTS, _isPinned, _isSuppressed; + private bool _isMentioningEveryone, _isTTS, _isPinned; private long? _editedTimestampTicks; private IUserMessage _referencedMessage; private ImmutableArray _attachments = ImmutableArray.Create(); @@ -23,13 +23,14 @@ 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; /// public override bool IsPinned => _isPinned; /// - public override bool IsSuppressed => _isSuppressed; + public override bool IsSuppressed => Flags.HasValue && Flags.Value.HasFlag(MessageFlags.SuppressEmbeds); /// public override DateTimeOffset? EditedTimestamp => DateTimeUtils.FromTicks(_editedTimestampTicks); /// @@ -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) @@ -74,10 +77,6 @@ namespace Discord.WebSocket _editedTimestampTicks = model.EditedTimestamp.Value?.UtcTicks; if (model.MentionEveryone.IsSpecified) _isMentioningEveryone = model.MentionEveryone.Value; - if (model.Flags.IsSpecified) - { - _isSuppressed = model.Flags.Value.HasFlag(API.MessageFlags.Suppressed); - } if (model.RoleMentions.IsSpecified) _roleMentions = model.RoleMentions.Value.Select(x => guild.GetRole(x)).ToImmutableArray(); @@ -162,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(); + } } /// @@ -176,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/Roles/SocketRole.cs b/src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs index b5e26ad78..e6aac2c04 100644 --- a/src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs +++ b/src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs @@ -36,6 +36,8 @@ namespace Discord.WebSocket public GuildPermissions Permissions { get; private set; } /// public int Position { get; private set; } + /// + public RoleTags Tags { get; private set; } /// public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); @@ -71,6 +73,8 @@ namespace Discord.WebSocket Position = model.Position; Color = new Color(model.Color); Permissions = new GuildPermissions(model.Permissions); + if (model.Tags.IsSpecified) + Tags = model.Tags.Value.ToEntity(); } /// 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 a506a5d7f..9263fe642 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs @@ -57,11 +57,13 @@ namespace Discord.WebSocket /// public bool IsStreaming => VoiceState?.IsStreaming ?? false; /// + public bool? IsPending { get; private set; } + /// public DateTimeOffset? JoinedAt => DateTimeUtils.FromTicks(_joinedAtTicks); /// /// 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. @@ -142,6 +144,8 @@ namespace Discord.WebSocket UpdateRoles(model.Roles.Value); if (model.PremiumSince.IsSpecified) _premiumSinceTicks = model.PremiumSince.Value?.UtcTicks; + if (model.Pending.IsSpecified) + IsPending = model.Pending.Value; } internal void Update(ClientState state, PresenceModel model, bool updatePresence) { @@ -173,17 +177,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 7d3c2d23b..025daf29a 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs @@ -26,6 +26,8 @@ namespace Discord.WebSocket public abstract string AvatarId { get; internal set; } /// public abstract bool IsWebhook { get; } + /// + public UserProperties? PublicFlags { get; private set; } internal abstract SocketGlobalUser GlobalUser { get; } internal abstract SocketPresence Presence { get; set; } @@ -36,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; @@ -83,12 +83,17 @@ namespace Discord.WebSocket Username = model.Username.Value; hasChanges = true; } + if (model.PublicFlags.IsSpecified && model.PublicFlags.Value != PublicFlags) + { + PublicFlags = model.PublicFlags.Value; + hasChanges = true; + } return hasChanges; } /// - 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 d400e1ae7..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) @@ -65,38 +65,60 @@ namespace Discord.WebSocket /// DateTimeOffset? IGuildUser.PremiumSince => null; /// + bool? IGuildUser.IsPending => null; + /// GuildPermissions IGuildUser.GuildPermissions => GuildPermissions.Webhook; /// 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.AddRoleAsync(IRole role, RequestOptions options) => + throw new NotSupportedException("Roles are not supported on webhook users."); + + /// + /// Roles are not supported on webhook users. + 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.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.AddRolesAsync(IEnumerable roles, RequestOptions options) => + 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.RemoveRoleAsync(IRole role, RequestOptions options) => + 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) => + Task IGuildUser.RemoveRolesAsync(IEnumerable roles, RequestOptions options) => throw new NotSupportedException("Roles are not supported on webhook users."); //IVoiceState 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/Entities/Webhooks/RestInternalWebhook.cs b/src/Discord.Net.Webhook/Entities/Webhooks/RestInternalWebhook.cs index 60cb89ee2..bbb160fcd 100644 --- a/src/Discord.Net.Webhook/Entities/Webhooks/RestInternalWebhook.cs +++ b/src/Discord.Net.Webhook/Entities/Webhooks/RestInternalWebhook.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Diagnostics; using System.Threading.Tasks; using Model = Discord.API.Webhook; @@ -11,9 +11,9 @@ namespace Discord.Webhook private DiscordWebhookClient _client; public ulong Id { get; } - public ulong ChannelId { get; } public string Token { get; } + public ulong ChannelId { get; private set; } public string Name { get; private set; } public string AvatarId { get; private set; } public ulong? GuildId { get; private set; } @@ -36,6 +36,8 @@ namespace Discord.Webhook internal void Update(Model model) { + if (ChannelId != model.ChannelId) + ChannelId = model.ChannelId; if (model.Avatar.IsSpecified) AvatarId = model.Avatar.Value; if (model.GuildId.IsSpecified) 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/test/Discord.Net.Tests.Unit/EmbedBuilderTests.cs b/test/Discord.Net.Tests.Unit/EmbedBuilderTests.cs index 12ec1a0bd..6cfdc83b2 100644 --- a/test/Discord.Net.Tests.Unit/EmbedBuilderTests.cs +++ b/test/Discord.Net.Tests.Unit/EmbedBuilderTests.cs @@ -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 f0d393af3..2cc46d3d3 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 4650a6708..e124bc923 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 c8c328d66..b01fac3b0 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)]