From f86c39de6a0eafbe5a3cd7952a3c7ab48e62415b Mon Sep 17 00:00:00 2001 From: Neuheit Date: Sat, 9 Nov 2019 13:12:29 -0500 Subject: [PATCH 01/28] feature: Implemented Message Reference Property (#1413) * Added support for Message References * Removed unused usings, added debugger display, updated ToString override * Changed snowflakes to be wrapped in an optional instead of a nullable. --- .../Entities/Messages/IMessage.cs | 12 +++++++ .../Entities/Messages/MessageReference.cs | 33 +++++++++++++++++++ src/Discord.Net.Rest/API/Common/Message.cs | 2 ++ .../API/Common/MessageReference.cs | 16 +++++++++ .../Entities/Messages/RestMessage.cs | 13 ++++++++ .../Entities/Messages/SocketMessage.cs | 14 ++++++++ 6 files changed, 90 insertions(+) create mode 100644 src/Discord.Net.Core/Entities/Messages/MessageReference.cs create mode 100644 src/Discord.Net.Rest/API/Common/MessageReference.cs diff --git a/src/Discord.Net.Core/Entities/Messages/IMessage.cs b/src/Discord.Net.Core/Entities/Messages/IMessage.cs index 1eba1e076..05f505269 100644 --- a/src/Discord.Net.Core/Entities/Messages/IMessage.cs +++ b/src/Discord.Net.Core/Entities/Messages/IMessage.cs @@ -140,6 +140,18 @@ namespace Discord /// MessageApplication Application { get; } + /// + /// Gets the reference to the original message if it was crossposted. + /// + /// + /// Sent with Cross-posted messages, meaning they were published from news channels + /// and received by subscriber channels. + /// + /// + /// A message's reference, if any is associated. + /// + MessageReference Reference { get; } + /// /// Gets all reactions included in this message. /// diff --git a/src/Discord.Net.Core/Entities/Messages/MessageReference.cs b/src/Discord.Net.Core/Entities/Messages/MessageReference.cs new file mode 100644 index 000000000..57a508a7c --- /dev/null +++ b/src/Discord.Net.Core/Entities/Messages/MessageReference.cs @@ -0,0 +1,33 @@ +using System.Diagnostics; + +namespace Discord +{ + /// + /// Contains the IDs sent from a crossposted message. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class MessageReference + { + /// + /// Gets the Message ID of the original message. + /// + public Optional MessageId { get; internal set; } + + /// + /// Gets the Channel ID of the original message. + /// + public ulong ChannelId { get; internal set; } + + /// + /// Gets the Guild ID of the original message. + /// + public Optional GuildId { get; internal set; } + + private string DebuggerDisplay + => $"Channel ID: ({ChannelId}){(GuildId.IsSpecified ? $", Guild ID: ({GuildId.Value})" : "")}" + + $"{(MessageId.IsSpecified ? $", Message ID: ({MessageId.Value})" : "")}"; + + public override string ToString() + => DebuggerDisplay; + } +} diff --git a/src/Discord.Net.Rest/API/Common/Message.cs b/src/Discord.Net.Rest/API/Common/Message.cs index a5016f923..f20035685 100644 --- a/src/Discord.Net.Rest/API/Common/Message.cs +++ b/src/Discord.Net.Rest/API/Common/Message.cs @@ -50,6 +50,8 @@ namespace Discord.API // sent with Rich Presence-related chat embeds [JsonProperty("application")] public Optional Application { get; set; } + [JsonProperty("message_reference")] + public Optional Reference { get; set; } [JsonProperty("flags")] public Optional Flags { get; set; } } diff --git a/src/Discord.Net.Rest/API/Common/MessageReference.cs b/src/Discord.Net.Rest/API/Common/MessageReference.cs new file mode 100644 index 000000000..8c0f8fe14 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/MessageReference.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class MessageReference + { + [JsonProperty("message_id")] + public Optional MessageId { get; set; } + + [JsonProperty("channel_id")] + public ulong ChannelId { get; set; } + + [JsonProperty("guild_id")] + public Optional GuildId { get; set; } + } +} diff --git a/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs b/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs index 29a9c9bd2..f457f4f7a 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs @@ -62,6 +62,8 @@ namespace Discord.Rest public MessageActivity Activity { get; private set; } /// public MessageApplication Application { get; private set; } + /// + public MessageReference Reference { get; private set; } internal RestMessage(BaseDiscordClient discord, ulong id, IMessageChannel channel, IUser author, MessageSource source) : base(discord, id) @@ -108,6 +110,17 @@ namespace Discord.Rest }; } + if(model.Reference.IsSpecified) + { + // Creates a new Reference from the API model + Reference = new MessageReference + { + GuildId = model.Reference.Value.GuildId, + ChannelId = model.Reference.Value.ChannelId, + MessageId = model.Reference.Value.MessageId + }; + } + if (model.Reactions.IsSpecified) { var value = model.Reactions.Value; diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs index ae42d9d61..7900b7ee7 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs @@ -53,6 +53,9 @@ namespace Discord.WebSocket /// public MessageApplication Application { get; private set; } + /// + public MessageReference Reference { get; private set; } + /// /// Returns all attachments included in this message. /// @@ -140,6 +143,17 @@ namespace Discord.WebSocket PartyId = model.Activity.Value.PartyId.Value }; } + + if (model.Reference.IsSpecified) + { + // Creates a new Reference from the API model + Reference = new MessageReference + { + GuildId = model.Reference.Value.GuildId, + ChannelId = model.Reference.Value.ChannelId, + MessageId = model.Reference.Value.MessageId + }; + } } /// From 5439cbad5af1f04f7d4fe6103953059eb2d000cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Saulius=20=C5=A0altenis?= <31516441+LtLi0n@users.noreply.github.com> Date: Sat, 9 Nov 2019 20:38:25 +0200 Subject: [PATCH 02/28] fix: GetUsersAsync to use MaxUsersPerBatch const as limit instead of MaxMessagesPerBatch. (#1412) Requests are now returning up to 1000 guild user entities instead of the previous 100. --- src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs index 05a520547..7730a9cc3 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs @@ -349,7 +349,7 @@ namespace Discord.Rest ulong? fromUserId, int? limit, RequestOptions options) { return new PagedAsyncEnumerable( - DiscordConfig.MaxMessagesPerBatch, + DiscordConfig.MaxUsersPerBatch, async (info, ct) => { var args = new GetGuildMembersParams @@ -363,7 +363,7 @@ namespace Discord.Rest }, nextPage: (info, lastPage) => { - if (lastPage.Count != DiscordConfig.MaxMessagesPerBatch) + if (lastPage.Count != DiscordConfig.MaxUsersPerBatch) return false; info.Position = lastPage.Max(x => x.Id); return true; From 79a0ea9de30f3b539060d61c800c8764735f8d17 Mon Sep 17 00:00:00 2001 From: Chris Johnston Date: Sat, 9 Nov 2019 10:41:10 -0800 Subject: [PATCH 03/28] Feature: CustomStatusGame Activity (#1406) * Implement CustomStatusGame activity Adds the CustomStatusGame class, which is the activity corresponding to the custom status feature. * Remove unused import from Game.cs --- .../Entities/Activities/ActivityType.cs | 6 ++- .../Entities/Activities/CustomStatusGame.cs | 40 +++++++++++++++++++ src/Discord.Net.Rest/API/Common/Game.cs | 6 +++ .../Extensions/EntityExtensions.cs | 7 ++++ .../Extensions/EntityExtensions.cs | 15 +++++++ 5 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 src/Discord.Net.Core/Entities/Activities/CustomStatusGame.cs diff --git a/src/Discord.Net.Core/Entities/Activities/ActivityType.cs b/src/Discord.Net.Core/Entities/Activities/ActivityType.cs index b603e27a4..8c44f49e3 100644 --- a/src/Discord.Net.Core/Entities/Activities/ActivityType.cs +++ b/src/Discord.Net.Core/Entities/Activities/ActivityType.cs @@ -20,6 +20,10 @@ namespace Discord /// /// The user is watching some form of media. /// - Watching = 3 + Watching = 3, + /// + /// The user has set a custom status. + /// + CustomStatus = 4, } } diff --git a/src/Discord.Net.Core/Entities/Activities/CustomStatusGame.cs b/src/Discord.Net.Core/Entities/Activities/CustomStatusGame.cs new file mode 100644 index 000000000..7bd2664a2 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Activities/CustomStatusGame.cs @@ -0,0 +1,40 @@ +using System; +using System.Diagnostics; + +namespace Discord +{ + /// + /// A user's activity for their custom status. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class CustomStatusGame : Game + { + internal CustomStatusGame() { } + + /// + /// Gets the emote, if it is set. + /// + /// + /// An containing the or set by the user. + /// + public IEmote Emote { get; internal set; } + + /// + /// Gets the timestamp of when this status was created. + /// + /// + /// A containing the time when this status was created. + /// + public DateTimeOffset CreatedAt { get; internal set; } + + /// + /// Gets the state of the status. + /// + public string State { get; internal set; } + + public override string ToString() + => $"{Emote} {State}"; + + private string DebuggerDisplay => $"{Name}"; + } +} diff --git a/src/Discord.Net.Rest/API/Common/Game.cs b/src/Discord.Net.Rest/API/Common/Game.cs index 2ec1e3846..d3a618697 100644 --- a/src/Discord.Net.Rest/API/Common/Game.cs +++ b/src/Discord.Net.Rest/API/Common/Game.cs @@ -35,6 +35,12 @@ namespace Discord.API public Optional SessionId { get; set; } [JsonProperty("Flags")] public Optional Flags { get; set; } + [JsonProperty("id")] + public Optional Id { get; set; } + [JsonProperty("emoji")] + public Optional Emoji { get; set; } + [JsonProperty("created_at")] + public Optional CreatedAt { get; set; } [OnError] internal void OnError(StreamingContext context, ErrorContext errorContext) diff --git a/src/Discord.Net.Rest/Extensions/EntityExtensions.cs b/src/Discord.Net.Rest/Extensions/EntityExtensions.cs index 4d164df96..e265f991f 100644 --- a/src/Discord.Net.Rest/Extensions/EntityExtensions.cs +++ b/src/Discord.Net.Rest/Extensions/EntityExtensions.cs @@ -5,6 +5,13 @@ namespace Discord.Rest { internal static class EntityExtensions { + public static IEmote ToIEmote(this API.Emoji model) + { + if (model.Id.HasValue) + return model.ToEntity(); + return new Emoji(model.Name); + } + public static GuildEmote ToEntity(this API.Emoji model) => new GuildEmote(model.Id.Value, model.Name, diff --git a/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs b/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs index fd91ba987..bad72aaea 100644 --- a/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs +++ b/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs @@ -1,3 +1,5 @@ +using Discord.Rest; +using System; using System.Collections.Immutable; using System.Linq; @@ -7,6 +9,19 @@ namespace Discord.WebSocket { public static IActivity ToEntity(this API.Game model) { + // Custom Status Game + if (model.Id.IsSpecified) + { + return new CustomStatusGame() + { + Type = ActivityType.CustomStatus, + Name = model.Name, + State = model.State.IsSpecified ? model.State.Value : null, + Emote = model.Emoji.IsSpecified ? model.Emoji.Value.ToIEmote() : null, + CreatedAt = DateTimeOffset.FromUnixTimeMilliseconds(model.CreatedAt.Value), + }; + } + // Spotify Game if (model.SyncId.IsSpecified) { From 99d7135d09c89519e0d418a927b14aa8fe8271a3 Mon Sep 17 00:00:00 2001 From: Joe4evr Date: Sat, 9 Nov 2019 20:58:49 +0100 Subject: [PATCH 04/28] nit: Utilize ValueTuples (#1393) * Utilize ValueTuples (with polyfill) * Rebase and remove polyfill --- src/Discord.Net.Commands/CommandService.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index 3a7da3862..d5c060fe4 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -49,7 +49,7 @@ namespace Discord.Commands private readonly ConcurrentDictionary _typedModuleDefs; private readonly ConcurrentDictionary> _typeReaders; private readonly ConcurrentDictionary _defaultTypeReaders; - private readonly ImmutableList> _entityTypeReaders; //TODO: Candidate for C#7 Tuple + private readonly ImmutableList<(Type EntityType, Type TypeReaderType)> _entityTypeReaders; private readonly HashSet _moduleDefs; private readonly CommandMap _map; @@ -124,11 +124,11 @@ namespace Discord.Commands _defaultTypeReaders[typeof(string)] = new PrimitiveTypeReader((string x, out string y) => { y = x; return true; }, 0); - var entityTypeReaders = ImmutableList.CreateBuilder>(); - entityTypeReaders.Add(new Tuple(typeof(IMessage), typeof(MessageTypeReader<>))); - entityTypeReaders.Add(new Tuple(typeof(IChannel), typeof(ChannelTypeReader<>))); - entityTypeReaders.Add(new Tuple(typeof(IRole), typeof(RoleTypeReader<>))); - entityTypeReaders.Add(new Tuple(typeof(IUser), typeof(UserTypeReader<>))); + var entityTypeReaders = ImmutableList.CreateBuilder<(Type, Type)>(); + entityTypeReaders.Add((typeof(IMessage), typeof(MessageTypeReader<>))); + entityTypeReaders.Add((typeof(IChannel), typeof(ChannelTypeReader<>))); + entityTypeReaders.Add((typeof(IRole), typeof(RoleTypeReader<>))); + entityTypeReaders.Add((typeof(IUser), typeof(UserTypeReader<>))); _entityTypeReaders = entityTypeReaders.ToImmutable(); } @@ -408,7 +408,7 @@ namespace Discord.Commands var typeInfo = type.GetTypeInfo(); if (typeInfo.IsEnum) return true; - return _entityTypeReaders.Any(x => type == x.Item1 || typeInfo.ImplementedInterfaces.Contains(x.Item2)); + return _entityTypeReaders.Any(x => type == x.EntityType || typeInfo.ImplementedInterfaces.Contains(x.TypeReaderType)); } internal void AddNullableTypeReader(Type valueType, TypeReader valueTypeReader) { @@ -439,9 +439,9 @@ namespace Discord.Commands //Is this an entity? for (int i = 0; i < _entityTypeReaders.Count; i++) { - if (type == _entityTypeReaders[i].Item1 || typeInfo.ImplementedInterfaces.Contains(_entityTypeReaders[i].Item1)) + if (type == _entityTypeReaders[i].EntityType || typeInfo.ImplementedInterfaces.Contains(_entityTypeReaders[i].EntityType)) { - reader = Activator.CreateInstance(_entityTypeReaders[i].Item2.MakeGenericType(type)) as TypeReader; + reader = Activator.CreateInstance(_entityTypeReaders[i].TypeReaderType.MakeGenericType(type)) as TypeReader; _defaultTypeReaders[type] = reader; return reader; } From 911523d56fcec59532c91ab9a2b3c71a6ecf81ef Mon Sep 17 00:00:00 2001 From: Joe4evr Date: Sat, 23 Nov 2019 18:57:16 +0100 Subject: [PATCH 05/28] docs: Fix the Comparer descriptions not linking the type (#1424) --- .../Common/DiscordComparers.Overwrites.md | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 docs/_overwrites/Common/DiscordComparers.Overwrites.md diff --git a/docs/_overwrites/Common/DiscordComparers.Overwrites.md b/docs/_overwrites/Common/DiscordComparers.Overwrites.md new file mode 100644 index 000000000..cbff7cf74 --- /dev/null +++ b/docs/_overwrites/Common/DiscordComparers.Overwrites.md @@ -0,0 +1,29 @@ +--- +uid: Discord.DiscordComparers.ChannelComparer +summary: *content +--- +Gets an [IEqualityComparer](xref:System.Collections.Generic.IEqualityComparer`1)<> to be used to compare channels. + +--- +uid: Discord.DiscordComparers.GuildComparer +summary: *content +--- +Gets an [IEqualityComparer](xref:System.Collections.Generic.IEqualityComparer`1)<> to be used to compare guilds. + +--- +uid: Discord.DiscordComparers.MessageComparer +summary: *content +--- +Gets an [IEqualityComparer](xref:System.Collections.Generic.IEqualityComparer`1)<> to be used to compare messages. + +--- +uid: Discord.DiscordComparers.RoleComparer +summary: *content +--- +Gets an [IEqualityComparer](xref:System.Collections.Generic.IEqualityComparer`1)<> to be used to compare roles. + +--- +uid: Discord.DiscordComparers.UserComparer +summary: *content +--- +Gets an [IEqualityComparer](xref:System.Collections.Generic.IEqualityComparer`1)<> to be used to compare users. From 3ff4e3d5066da6db2734be704433983071f6ffa6 Mon Sep 17 00:00:00 2001 From: Chris Johnston Date: Sat, 23 Nov 2019 09:58:13 -0800 Subject: [PATCH 06/28] fix: #1421 (in a better way) Return empty set when ActiveClients is null (#1422) This change ensures that SocketUser.ActiveClients will not return null, but instead an empty set by default. This can happen if the client has not recieved a presence update for a user, or if the user is not cached. --- src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs index d4798bedd..09c4165f4 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs @@ -40,7 +40,7 @@ namespace Discord.WebSocket /// public UserStatus Status => Presence.Status; /// - public IImmutableSet ActiveClients => Presence.ActiveClients; + public IImmutableSet ActiveClients => Presence.ActiveClients ?? ImmutableHashSet.Empty; /// /// Gets mutual guilds shared with this user. /// From 9ede6b905f17ff08538e51670ac86bfb37442bcc Mon Sep 17 00:00:00 2001 From: Mark Date: Fri, 29 Nov 2019 18:58:04 -0800 Subject: [PATCH 07/28] docs: Fix incorrect and missing colour values for Color fields (#1426) --- src/Discord.Net.Core/Entities/Roles/Color.cs | 39 ++++++++++---------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Roles/Color.cs b/src/Discord.Net.Core/Entities/Roles/Color.cs index b522ae47e..7c2d152a4 100644 --- a/src/Discord.Net.Core/Entities/Roles/Color.cs +++ b/src/Discord.Net.Core/Entities/Roles/Color.cs @@ -16,60 +16,61 @@ namespace Discord /// A color struct with the hex value of 1ABC9C. public static readonly Color Teal = new Color(0x1ABC9C); /// Gets the dark teal color value. + /// A color struct with the hex value of 11806A. public static readonly Color DarkTeal = new Color(0x11806A); /// Gets the green color value. - /// A color struct with the hex value of 11806A. + /// A color struct with the hex value of 2ECC71. public static readonly Color Green = new Color(0x2ECC71); /// Gets the dark green color value. - /// A color struct with the hex value of 2ECC71. + /// A color struct with the hex value of 1F8B4C. public static readonly Color DarkGreen = new Color(0x1F8B4C); /// Gets the blue color value. - /// A color struct with the hex value of 1F8B4C. + /// A color struct with the hex value of 3498DB. public static readonly Color Blue = new Color(0x3498DB); /// Gets the dark blue color value. - /// A color struct with the hex value of 3498DB. + /// A color struct with the hex value of 206694. public static readonly Color DarkBlue = new Color(0x206694); /// Gets the purple color value. - /// A color struct with the hex value of 206694. + /// A color struct with the hex value of 9B59B6. public static readonly Color Purple = new Color(0x9B59B6); /// Gets the dark purple color value. - /// A color struct with the hex value of 9B59B6. + /// A color struct with the hex value of 71368A. public static readonly Color DarkPurple = new Color(0x71368A); /// Gets the magenta color value. - /// A color struct with the hex value of 71368A. + /// A color struct with the hex value of E91E63. public static readonly Color Magenta = new Color(0xE91E63); /// Gets the dark magenta color value. - /// A color struct with the hex value of E91E63. + /// A color struct with the hex value of AD1457. public static readonly Color DarkMagenta = new Color(0xAD1457); /// Gets the gold color value. - /// A color struct with the hex value of AD1457. + /// A color struct with the hex value of F1C40F. public static readonly Color Gold = new Color(0xF1C40F); /// Gets the light orange color value. - /// A color struct with the hex value of F1C40F. + /// A color struct with the hex value of C27C0E. public static readonly Color LightOrange = new Color(0xC27C0E); /// Gets the orange color value. - /// A color struct with the hex value of C27C0E. + /// A color struct with the hex value of E67E22. public static readonly Color Orange = new Color(0xE67E22); /// Gets the dark orange color value. - /// A color struct with the hex value of E67E22. + /// A color struct with the hex value of A84300. public static readonly Color DarkOrange = new Color(0xA84300); /// Gets the red color value. - /// A color struct with the hex value of A84300. + /// A color struct with the hex value of E74C3C. public static readonly Color Red = new Color(0xE74C3C); /// Gets the dark red color value. - /// A color struct with the hex value of E74C3C. - public static readonly Color DarkRed = new Color(0x992D22); - /// Gets the light grey color value. /// A color struct with the hex value of 992D22. + public static readonly Color DarkRed = new Color(0x992D22); + /// Gets the light grey color value. + /// A color struct with the hex value of 979C9F. public static readonly Color LightGrey = new Color(0x979C9F); /// Gets the lighter grey color value. - /// A color struct with the hex value of 979C9F. + /// A color struct with the hex value of 95A5A6. public static readonly Color LighterGrey = new Color(0x95A5A6); /// Gets the dark grey color value. - /// A color struct with the hex value of 95A5A6. + /// A color struct with the hex value of 607D8B. public static readonly Color DarkGrey = new Color(0x607D8B); /// Gets the darker grey color value. - /// A color struct with the hex value of 607D8B. + /// A color struct with the hex value of 546E7A. public static readonly Color DarkerGrey = new Color(0x546E7A); /// Gets the encoded value for this color. From 1c63fd479dfd0cacf204101a51284c4d2fc1ae85 Mon Sep 17 00:00:00 2001 From: Chris Johnston Date: Fri, 29 Nov 2019 18:59:11 -0800 Subject: [PATCH 08/28] (ifcbrk)fix: #1335 Add isMentionable parameter to CreateRoleAsync in non-breaking manner (#1418) * Fix #1335 Add isMentionable parameter to CreateRoleAsync in non-breaking manner This PR adds the isMentionable parameter to the CreateRoleAsync method in a way that prevents it from being interface-breaking. This has been done by adding it as an optional parameter at the end of publicly-exposed methods. This parameter determines if the newly created role can be mentioned as it is created. * Overload CreateRoleAsync methods --- src/Discord.Net.Core/Entities/Guilds/IGuild.cs | 15 +++++++++++++++ .../Entities/Guilds/GuildHelper.cs | 3 ++- src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs | 15 ++++++++++++--- .../Entities/Guilds/SocketGuild.cs | 14 +++++++++++--- 4 files changed, 40 insertions(+), 7 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs index 213091ad4..a18e91b69 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs @@ -598,6 +598,21 @@ namespace Discord /// role. /// Task CreateRoleAsync(string name, GuildPermissions? permissions = null, Color? color = null, bool isHoisted = false, RequestOptions options = null); + // TODO remove CreateRoleAsync overload that does not have isMentionable when breaking change is acceptable + /// + /// Creates a new role with the provided name. + /// + /// The new name for the role. + /// The guild permission that the role should possess. + /// The color of the role. + /// Whether the role is separated from others on the sidebar. + /// Whether the role can be mentioned. + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous creation operation. The task result contains the newly created + /// role. + /// + Task CreateRoleAsync(string name, GuildPermissions? permissions = null, Color? color = null, bool isHoisted = false, bool isMentionable = false, RequestOptions options = null); /// /// Adds a user to this guild. diff --git a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs index 7730a9cc3..664c57a1c 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs @@ -257,7 +257,7 @@ namespace Discord.Rest //Roles /// is null. public static async Task CreateRoleAsync(IGuild guild, BaseDiscordClient client, - string name, GuildPermissions? permissions, Color? color, bool isHoisted, RequestOptions options) + string name, GuildPermissions? permissions, Color? color, bool isHoisted, bool isMentionable, RequestOptions options) { if (name == null) throw new ArgumentNullException(paramName: nameof(name)); @@ -270,6 +270,7 @@ namespace Discord.Rest x.Permissions = (permissions ?? role.Permissions); x.Color = (color ?? Color.Default); x.Hoist = isHoisted; + x.Mentionable = isMentionable; }, options).ConfigureAwait(false); return role; diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs index e9e4d3290..900f5045e 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -530,6 +530,11 @@ namespace Discord.Rest return null; } + /// + public Task CreateRoleAsync(string name, GuildPermissions? permissions = default(GuildPermissions?), Color? color = default(Color?), + bool isHoisted = false, RequestOptions options = null) + => CreateRoleAsync(name, permissions, color, isHoisted, false, options); + /// /// Creates a new role with the provided name. /// @@ -538,14 +543,15 @@ namespace Discord.Rest /// The color of the role. /// Whether the role is separated from others on the sidebar. /// The options to be used when sending the request. + /// Whether the role can be mentioned. /// /// A task that represents the asynchronous creation operation. The task result contains the newly created /// role. /// public async Task CreateRoleAsync(string name, GuildPermissions? permissions = default(GuildPermissions?), Color? color = default(Color?), - bool isHoisted = false, RequestOptions options = null) + bool isHoisted = false, bool isMentionable = false, RequestOptions options = null) { - var role = await GuildHelper.CreateRoleAsync(this, Discord, name, permissions, color, isHoisted, options).ConfigureAwait(false); + var role = await GuildHelper.CreateRoleAsync(this, Discord, name, permissions, color, isHoisted, isMentionable, options).ConfigureAwait(false); _roles = _roles.Add(role.Id, role); return role; } @@ -833,7 +839,10 @@ namespace Discord.Rest => GetRole(id); /// async Task IGuild.CreateRoleAsync(string name, GuildPermissions? permissions, Color? color, bool isHoisted, RequestOptions options) - => await CreateRoleAsync(name, permissions, color, isHoisted, options).ConfigureAwait(false); + => await CreateRoleAsync(name, permissions, color, isHoisted, false, options).ConfigureAwait(false); + /// + async Task IGuild.CreateRoleAsync(string name, GuildPermissions? permissions, Color? color, bool isHoisted, bool isMentionable, RequestOptions options) + => await CreateRoleAsync(name, permissions, color, isHoisted, isMentionable, options).ConfigureAwait(false); /// async Task IGuild.AddGuildUserAsync(ulong userId, string accessToken, Action func, RequestOptions options) diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index 054348ef1..da9a316eb 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -679,6 +679,10 @@ namespace Discord.WebSocket return null; } + /// + public Task CreateRoleAsync(string name, GuildPermissions? permissions = default(GuildPermissions?), Color? color = default(Color?), + bool isHoisted = false, RequestOptions options = null) + => GuildHelper.CreateRoleAsync(this, Discord, name, permissions, color, isHoisted, false, options); /// /// Creates a new role with the provided name. /// @@ -686,6 +690,7 @@ namespace Discord.WebSocket /// The guild permission that the role should possess. /// The color of the role. /// Whether the role is separated from others on the sidebar. + /// Whether the role can be mentioned. /// The options to be used when sending the request. /// is null. /// @@ -693,8 +698,8 @@ namespace Discord.WebSocket /// role. /// public Task CreateRoleAsync(string name, GuildPermissions? permissions = default(GuildPermissions?), Color? color = default(Color?), - bool isHoisted = false, RequestOptions options = null) - => GuildHelper.CreateRoleAsync(this, Discord, name, permissions, color, isHoisted, options); + bool isHoisted = false, bool isMentionable = false, RequestOptions options = null) + => GuildHelper.CreateRoleAsync(this, Discord, name, permissions, color, isHoisted, isMentionable, options); internal SocketRole AddRole(RoleModel model) { var role = SocketRole.Create(this, Discord.State, model); @@ -1151,7 +1156,10 @@ namespace Discord.WebSocket => GetRole(id); /// async Task IGuild.CreateRoleAsync(string name, GuildPermissions? permissions, Color? color, bool isHoisted, RequestOptions options) - => await CreateRoleAsync(name, permissions, color, isHoisted, options).ConfigureAwait(false); + => await CreateRoleAsync(name, permissions, color, isHoisted, false, options).ConfigureAwait(false); + /// + async Task IGuild.CreateRoleAsync(string name, GuildPermissions? permissions, Color? color, bool isHoisted, bool isMentionable, RequestOptions options) + => await CreateRoleAsync(name, permissions, color, isHoisted, isMentionable, options).ConfigureAwait(false); /// Task> IGuild.GetUsersAsync(CacheMode mode, RequestOptions options) From e627f0780a74e9f2c2c1b497aa5bcf31d2ab79f6 Mon Sep 17 00:00:00 2001 From: Chris Johnston Date: Fri, 29 Nov 2019 19:07:43 -0800 Subject: [PATCH 09/28] change: fix #1415 Re-add support for overwrite permissions for news channels (#1417) This change updates the NewsChannel classes so that the overwrite permission-related properties no longer throw an Exception when they are used. These properties were not initially supported by News/Announcement channels when the feature was first released, but now they are. --- .../Entities/Channels/RestNewsChannel.cs | 24 --------- .../Entities/Channels/SocketNewsChannel.cs | 50 +------------------ 2 files changed, 1 insertion(+), 73 deletions(-) diff --git a/src/Discord.Net.Rest/Entities/Channels/RestNewsChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestNewsChannel.cs index f4984a0d2..8a334fae5 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestNewsChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestNewsChannel.cs @@ -25,29 +25,5 @@ namespace Discord.Rest return entity; } public override int SlowModeInterval => throw new NotSupportedException("News channels do not support Slow Mode."); - public override Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options = null) - { - throw new NotSupportedException("News channels do not support Overwrite Permissions."); - } - public override Task AddPermissionOverwriteAsync(IUser user, OverwritePermissions permissions, RequestOptions options = null) - { - throw new NotSupportedException("News channels do not support Overwrite Permissions."); - } - public override OverwritePermissions? GetPermissionOverwrite(IRole role) - { - throw new NotSupportedException("News channels do not support Overwrite Permissions."); - } - public override OverwritePermissions? GetPermissionOverwrite(IUser user) - { - throw new NotSupportedException("News channels do not support Overwrite Permissions."); - } - public override Task RemovePermissionOverwriteAsync(IRole role, RequestOptions options = null) - { - throw new NotSupportedException("News channels do not support Overwrite Permissions."); - } - public override Task RemovePermissionOverwriteAsync(IUser user, RequestOptions options = null) - { - throw new NotSupportedException("News channels do not support Overwrite Permissions."); - } } } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketNewsChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketNewsChannel.cs index f84c35cae..815a99ce7 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketNewsChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketNewsChannel.cs @@ -11,7 +11,7 @@ namespace Discord.WebSocket /// /// /// - /// Most of the properties and methods featured may not be supported due to the nature of the channel. + /// The property is not supported for news channels. /// /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] @@ -35,53 +35,5 @@ namespace Discord.WebSocket /// public override int SlowModeInterval => throw new NotSupportedException("News channels do not support Slow Mode."); - /// - /// - /// - /// This method is not supported by this type. Attempting to use this method will result in a . - /// - /// - public override Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options = null) - => throw new NotSupportedException("News channels do not support Overwrite Permissions."); - /// - /// - /// - /// This method is not supported by this type. Attempting to use this method will result in a . - /// - /// - public override Task AddPermissionOverwriteAsync(IUser user, OverwritePermissions permissions, RequestOptions options = null) - => throw new NotSupportedException("News channels do not support Overwrite Permissions."); - /// - /// - /// - /// This property is not supported by this type. Attempting to use this property will result in a . - /// - /// - public override IReadOnlyCollection PermissionOverwrites - => throw new NotSupportedException("News channels do not support Overwrite Permissions."); - /// - /// - /// - /// This method is not supported by this type. Attempting to use this method will result in a . - /// - /// - public override Task SyncPermissionsAsync(RequestOptions options = null) - => throw new NotSupportedException("News channels do not support Overwrite Permissions."); - /// - /// - /// - /// This method is not supported by this type. Attempting to use this method will result in a . - /// - /// - public override Task RemovePermissionOverwriteAsync(IRole role, RequestOptions options = null) - => throw new NotSupportedException("News channels do not support Overwrite Permissions."); - /// - /// - /// - /// This method is not supported by this type. Attempting to use this method will result in a . - /// - /// - public override Task RemovePermissionOverwriteAsync(IUser user, RequestOptions options = null) - => throw new NotSupportedException("News channels do not support Overwrite Permissions."); } } From 2bba324143063f39b46bad92563bec068a4b35cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Saulius=20=C5=A0altenis?= <31516441+LtLi0n@users.noreply.github.com> Date: Sat, 30 Nov 2019 05:11:48 +0200 Subject: [PATCH 10/28] feature: add StartedAt, EndsAt, Elapsed and Remaining to SpotifyGame. (#1414) * Fixed GetUsersAsync to use MaxUsersPerBatch const as limit instead of MaxMessagesPerBatch. Requests are now returning up to 1000 guild user entities instead of the previous 100. * Added StartedAt and EndsAt timespans for SpotifyGame. They make it possible to expose track's Elapsed and Remaining properties. * Moved Duration to be initialized within model construction. * Updated StartedAt and EndsAt comments. --- .../Entities/Activities/SpotifyGame.cs | 33 +++++++++++++++++++ .../Extensions/EntityExtensions.cs | 2 ++ 2 files changed, 35 insertions(+) diff --git a/src/Discord.Net.Core/Entities/Activities/SpotifyGame.cs b/src/Discord.Net.Core/Entities/Activities/SpotifyGame.cs index 23f88687d..4eab34fa2 100644 --- a/src/Discord.Net.Core/Entities/Activities/SpotifyGame.cs +++ b/src/Discord.Net.Core/Entities/Activities/SpotifyGame.cs @@ -31,6 +31,23 @@ namespace Discord /// A string containing the name of the song (e.g. Lonely Together (feat. Rita Ora)). /// public string TrackTitle { get; internal set; } + + /// + /// Gets the date when the track started playing. + /// + /// + /// A containing the start timestamp of the song. + /// + public DateTimeOffset? StartedAt { get; internal set; } + + /// + /// Gets the date when the track ends. + /// + /// + /// A containing the finish timestamp of the song. + /// + public DateTimeOffset? EndsAt { get; internal set; } + /// /// Gets the duration of the song. /// @@ -39,6 +56,22 @@ namespace Discord /// public TimeSpan? Duration { get; internal set; } + /// + /// Gets the elapsed duration of the song. + /// + /// + /// A containing the elapsed duration of the song. + /// + public TimeSpan? Elapsed => DateTimeOffset.UtcNow - StartedAt; + + /// + /// Gets the remaining duration of the song. + /// + /// + /// A containing the remaining duration of the song. + /// + public TimeSpan? Remaining => EndsAt - DateTimeOffset.UtcNow; + /// /// Gets the track ID of the song. /// diff --git a/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs b/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs index bad72aaea..5149eea5b 100644 --- a/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs +++ b/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs @@ -38,6 +38,8 @@ namespace Discord.WebSocket AlbumTitle = albumText, TrackTitle = model.Details.GetValueOrDefault(), Artists = model.State.GetValueOrDefault()?.Split(';').Select(x=>x?.Trim()).ToImmutableArray(), + StartedAt = timestamps?.Start, + EndsAt = timestamps?.End, Duration = timestamps?.End - timestamps?.Start, AlbumArtUrl = albumArtId != null ? CDN.GetSpotifyAlbumArtUrl(albumArtId) : null, Type = ActivityType.Listening, From a4846516fbce6cc3f80c04be43667f34048e23c4 Mon Sep 17 00:00:00 2001 From: Chris Johnston Date: Fri, 29 Nov 2019 19:13:08 -0800 Subject: [PATCH 11/28] fix: false-positive detection of CustomStatusGame based on Id property (#1416) This change fixes a bug that was introduced in PR #1406. Games were falsely detected to be CustomStatusGames, based on the Id property being included in the model payload. This fixes the false detection of Games as CustomStatusGame. An activity will only be considered a CustomStatusGame if the Id has a value of "custom". This change has been tested by listening to the GuildMemberUpdated event, and opening/closing games with a custom status set. --- src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs b/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs index 5149eea5b..cbe575075 100644 --- a/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs +++ b/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs @@ -10,7 +10,7 @@ namespace Discord.WebSocket public static IActivity ToEntity(this API.Game model) { // Custom Status Game - if (model.Id.IsSpecified) + if (model.Id.IsSpecified && model.Id.Value == "custom") { return new CustomStatusGame() { From d734ce0a11ca5d0b9d823f1303f90c5af3de32fe Mon Sep 17 00:00:00 2001 From: NovusTheory <3434404+NovusTheory@users.noreply.github.com> Date: Thu, 26 Dec 2019 17:44:01 -0600 Subject: [PATCH 12/28] feature: Add ability to modify the banner for guilds (#1432) --- src/Discord.Net.Core/Entities/Guilds/GuildProperties.cs | 4 ++++ src/Discord.Net.Rest/API/Rest/ModifyGuildParams.cs | 2 ++ src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs | 3 +++ 3 files changed, 9 insertions(+) diff --git a/src/Discord.Net.Core/Entities/Guilds/GuildProperties.cs b/src/Discord.Net.Core/Entities/Guilds/GuildProperties.cs index ec31019af..981e1198c 100644 --- a/src/Discord.Net.Core/Entities/Guilds/GuildProperties.cs +++ b/src/Discord.Net.Core/Entities/Guilds/GuildProperties.cs @@ -38,6 +38,10 @@ namespace Discord /// public Optional Icon { get; set; } /// + /// Gets or sets the banner of the guild. + /// + public Optional Banner { get; set; } + /// /// Gets or sets the guild's splash image. /// /// diff --git a/src/Discord.Net.Rest/API/Rest/ModifyGuildParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyGuildParams.cs index 6341b63b6..cfb107bcd 100644 --- a/src/Discord.Net.Rest/API/Rest/ModifyGuildParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyGuildParams.cs @@ -22,6 +22,8 @@ namespace Discord.API.Rest public Optional SystemChannelId { get; set; } [JsonProperty("icon")] public Optional Icon { get; set; } + [JsonProperty("banner")] + public Optional Banner { get; set; } [JsonProperty("splash")] public Optional Splash { get; set; } [JsonProperty("afk_channel_id")] diff --git a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs index 664c57a1c..790b1e5c3 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs @@ -32,6 +32,7 @@ namespace Discord.Rest Icon = args.Icon.IsSpecified ? args.Icon.Value?.ToModel() : Optional.Create(), Name = args.Name, Splash = args.Splash.IsSpecified ? args.Splash.Value?.ToModel() : Optional.Create(), + Banner = args.Banner.IsSpecified ? args.Banner.Value?.ToModel() : Optional.Create(), VerificationLevel = args.VerificationLevel, ExplicitContentFilter = args.ExplicitContentFilter, SystemChannelFlags = args.SystemChannelFlags @@ -57,6 +58,8 @@ namespace Discord.Rest else if (args.RegionId.IsSpecified) apiArgs.RegionId = args.RegionId.Value; + if (!apiArgs.Banner.IsSpecified && guild.BannerId != null) + apiArgs.Banner = new ImageModel(guild.BannerId); if (!apiArgs.Splash.IsSpecified && guild.SplashId != null) apiArgs.Splash = new ImageModel(guild.SplashId); if (!apiArgs.Icon.IsSpecified && guild.IconId != null) From adf823ca9a4fe8d6d812d952630023c9e7bfdcb9 Mon Sep 17 00:00:00 2001 From: Braedon Smith <11038206+Tycharis@users.noreply.github.com> Date: Tue, 31 Mar 2020 09:03:11 -0500 Subject: [PATCH 13/28] Added System.Linq reference (#1470) Use of IReadOnlyCollection#Any requires a reference to System.Linq. It's a small thing, but better copy-paste-ability is never a bad thing. --- docs/guides/commands/samples/preconditions/require_role.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/guides/commands/samples/preconditions/require_role.cs b/docs/guides/commands/samples/preconditions/require_role.cs index 77d09b525..d9a393ace 100644 --- a/docs/guides/commands/samples/preconditions/require_role.cs +++ b/docs/guides/commands/samples/preconditions/require_role.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Threading.Tasks; using Discord.Commands; using Discord.WebSocket; From 91aec9ff93f810286ed08c8a6467d051df5f45a8 Mon Sep 17 00:00:00 2001 From: Christopher Felegy Date: Tue, 14 Apr 2020 20:47:36 -0400 Subject: [PATCH 14/28] add idn debugger where is my foxboat --- Discord.Net.sln | 15 +++++ samples/idn/Inspector.cs | 74 +++++++++++++++++++++ samples/idn/Program.cs | 135 +++++++++++++++++++++++++++++++++++++++ samples/idn/idn.csproj | 16 +++++ samples/idn/logview.ps1 | 1 + 5 files changed, 241 insertions(+) create mode 100644 samples/idn/Inspector.cs create mode 100644 samples/idn/Program.cs create mode 100644 samples/idn/idn.csproj create mode 100644 samples/idn/logview.ps1 diff --git a/Discord.Net.sln b/Discord.Net.sln index 54a788f7d..1a32f1270 100644 --- a/Discord.Net.sln +++ b/Discord.Net.sln @@ -40,6 +40,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Analyzers.Tests EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Examples", "src\Discord.Net.Examples\Discord.Net.Examples.csproj", "{47820065-3CFB-401C-ACEA-862BD564A404}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "idn", "samples\idn\idn.csproj", "{4A03840B-9EBE-47E3-89AB-E0914DF21AFB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -218,6 +220,18 @@ Global {47820065-3CFB-401C-ACEA-862BD564A404}.Release|x64.Build.0 = Release|Any CPU {47820065-3CFB-401C-ACEA-862BD564A404}.Release|x86.ActiveCfg = Release|Any CPU {47820065-3CFB-401C-ACEA-862BD564A404}.Release|x86.Build.0 = Release|Any CPU + {4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Debug|x64.ActiveCfg = Debug|Any CPU + {4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Debug|x64.Build.0 = Debug|Any CPU + {4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Debug|x86.ActiveCfg = Debug|Any CPU + {4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Debug|x86.Build.0 = Debug|Any CPU + {4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Release|Any CPU.Build.0 = Release|Any CPU + {4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Release|x64.ActiveCfg = Release|Any CPU + {4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Release|x64.Build.0 = Release|Any CPU + {4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Release|x86.ActiveCfg = Release|Any CPU + {4A03840B-9EBE-47E3-89AB-E0914DF21AFB}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -236,6 +250,7 @@ Global {E169E15A-E82C-45BF-8C24-C2CADB7093AA} = {C7CF5621-7D36-433B-B337-5A2E3C101A71} {FC67057C-E92F-4E1C-98BE-46F839C8AD71} = {C7CF5621-7D36-433B-B337-5A2E3C101A71} {47820065-3CFB-401C-ACEA-862BD564A404} = {BB59D5B5-E7B0-4BF4-8F82-D14431B2799B} + {4A03840B-9EBE-47E3-89AB-E0914DF21AFB} = {BB59D5B5-E7B0-4BF4-8F82-D14431B2799B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D2404771-EEC8-45F2-9D71-F3373F6C1495} diff --git a/samples/idn/Inspector.cs b/samples/idn/Inspector.cs new file mode 100644 index 000000000..3806e0e79 --- /dev/null +++ b/samples/idn/Inspector.cs @@ -0,0 +1,74 @@ +using System.Collections; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace idn +{ + public static class Inspector + { + public static string Inspect(object value) + { + var builder = new StringBuilder(); + if (value != null) + { + var type = value.GetType().GetTypeInfo(); + builder.AppendLine($"[{type.Namespace}.{type.Name}]"); + builder.AppendLine($"{InspectProperty(value)}"); + + if (value is IEnumerable) + { + var items = (value as IEnumerable).Cast().ToArray(); + if (items.Length > 0) + { + builder.AppendLine(); + foreach (var item in items) + builder.AppendLine($"- {InspectProperty(item)}"); + } + } + else + { + var groups = type.GetProperties(BindingFlags.Instance | BindingFlags.Public) + .Where(x => x.GetIndexParameters().Length == 0) + .GroupBy(x => x.Name) + .OrderBy(x => x.Key) + .ToArray(); + if (groups.Length > 0) + { + builder.AppendLine(); + int pad = groups.Max(x => x.Key.Length) + 1; + foreach (var group in groups) + builder.AppendLine($"{group.Key.PadRight(pad, ' ')}{InspectProperty(group.First().GetValue(value))}"); + } + } + } + else + builder.AppendLine("null"); + return builder.ToString(); + } + + private static string InspectProperty(object obj) + { + if (obj == null) + return "null"; + + var type = obj.GetType(); + + var debuggerDisplay = type.GetProperty("DebuggerDisplay", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + if (debuggerDisplay != null) + return debuggerDisplay.GetValue(obj).ToString(); + + var toString = type.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) + .Where(x => x.Name == "ToString" && x.DeclaringType != typeof(object)) + .FirstOrDefault(); + if (toString != null) + return obj.ToString(); + + var count = type.GetProperty("Count", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + if (count != null) + return $"[{count.GetValue(obj)} Items]"; + + return obj.ToString(); + } + } +} diff --git a/samples/idn/Program.cs b/samples/idn/Program.cs new file mode 100644 index 000000000..60af7faf4 --- /dev/null +++ b/samples/idn/Program.cs @@ -0,0 +1,135 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CSharp.Scripting; +using Microsoft.CodeAnalysis.Scripting; +using Discord; +using Discord.WebSocket; +using System.Collections.Concurrent; +using System.Threading; +using System.Text; +using System.Diagnostics; + +namespace idn +{ + public class Program + { + public static readonly string[] Imports = + { + "System", + "System.Collections.Generic", + "System.Linq", + "System.Threading.Tasks", + "System.Diagnostics", + "System.IO", + "Discord", + "Discord.Rest", + "Discord.WebSocket", + "idn" + }; + + static async Task Main(string[] args) + { + var token = File.ReadAllText("token.ignore"); + var client = new DiscordSocketClient(new DiscordSocketConfig { LogLevel = LogSeverity.Debug }); + var logQueue = new ConcurrentQueue(); + var logCancelToken = new CancellationTokenSource(); + + client.Log += msg => + { + logQueue.Enqueue(msg); + return Task.CompletedTask; + }; + + var logTask = Task.Run(async () => + { + var fs = new FileStream("idn.log", FileMode.Append); + //var f = File.Open("idn.log", FileMode.Append); + StringBuilder logStringBuilder = new StringBuilder(200); + string logString = ""; + + byte[] helloBytes = Encoding.UTF8.GetBytes($"### new log session: {DateTime.Now} ###\n\n"); + await fs.WriteAsync(helloBytes); + + while (!logCancelToken.IsCancellationRequested) + { + if (logQueue.TryDequeue(out var msg)) + { + _ = msg.ToString(builder: logStringBuilder); + logStringBuilder.AppendLine(); + logString = logStringBuilder.ToString(); + + Debug.Write(logString, "DNET"); + await fs.WriteAsync(Encoding.UTF8.GetBytes(logString), logCancelToken.Token); + } + await fs.FlushAsync(); + await Task.Delay(100, logCancelToken.Token); + } + + byte[] goodbyeBytes = Encoding.UTF8.GetBytes($"#!! end log session: {DateTime.Now} !!#\n\n\n"); + await fs.WriteAsync(goodbyeBytes); + await fs.DisposeAsync(); + }); + + await client.LoginAsync(TokenType.Bot, token); + await client.StartAsync(); + + var options = ScriptOptions.Default + .AddReferences(GetAssemblies().ToArray()) + .AddImports(Imports); + + var globals = new ScriptGlobals + { + Client = client, + }; + + while (true) + { + Console.Write("> "); + string input = Console.ReadLine(); + + if (input == "quit!") + { + break; + } + + object eval; + try + { + eval = await CSharpScript.EvaluateAsync(input, options, globals); + } + catch (Exception e) + { + eval = e; + } + Console.WriteLine(Inspector.Inspect(eval)); + } + + await client.StopAsync(); + client.Dispose(); + logCancelToken.Cancel(); + try + { await logTask; } + finally { Console.WriteLine("goodbye!"); } + } + + static IEnumerable GetAssemblies() + { + var Assemblies = Assembly.GetEntryAssembly().GetReferencedAssemblies(); + foreach (var a in Assemblies) + { + var asm = Assembly.Load(a); + yield return asm; + } + yield return Assembly.GetEntryAssembly(); + } + + public class ScriptGlobals + { + public DiscordSocketClient Client { get; set; } + } + } +} diff --git a/samples/idn/idn.csproj b/samples/idn/idn.csproj new file mode 100644 index 000000000..984c86383 --- /dev/null +++ b/samples/idn/idn.csproj @@ -0,0 +1,16 @@ + + + + Exe + netcoreapp3.1 + + + + + + + + + + + diff --git a/samples/idn/logview.ps1 b/samples/idn/logview.ps1 new file mode 100644 index 000000000..0857475f5 --- /dev/null +++ b/samples/idn/logview.ps1 @@ -0,0 +1 @@ +Get-Content .\bin\Debug\netcoreapp3.1\idn.log -Tail 3 -Wait \ No newline at end of file From 6ed311bc73e7b50e157fc4d79eabae145330a65c Mon Sep 17 00:00:00 2001 From: Christopher Felegy Date: Tue, 14 Apr 2020 20:50:55 -0400 Subject: [PATCH 15/28] meta: 2.2.0 --- Discord.Net.targets | 2 +- src/Discord.Net/Discord.Net.nuspec | 32 +++++++++++++++--------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/Discord.Net.targets b/Discord.Net.targets index e7b45e52f..3a348e786 100644 --- a/Discord.Net.targets +++ b/Discord.Net.targets @@ -1,7 +1,7 @@ 2.2.0 - dev + RogueException discord;discordapp https://github.com/RogueException/Discord.Net diff --git a/src/Discord.Net/Discord.Net.nuspec b/src/Discord.Net/Discord.Net.nuspec index 4b7717e58..b73d3bf24 100644 --- a/src/Discord.Net/Discord.Net.nuspec +++ b/src/Discord.Net/Discord.Net.nuspec @@ -2,7 +2,7 @@ Discord.Net - 2.2.0-dev$suffix$ + 2.2.0$suffix$ Discord.Net Discord.Net Contributors RogueException @@ -14,25 +14,25 @@ https://github.com/RogueException/Discord.Net/raw/dev/docs/marketing/logo/PackageLogo.png - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + From 4b602b4517779221514c7f46b9858d1c36bcbb57 Mon Sep 17 00:00:00 2001 From: Christopher Felegy Date: Tue, 14 Apr 2020 20:50:55 -0400 Subject: [PATCH 16/28] meta: 2.2.0 free chick fil a for whomever writes the changelog --- CHANGELOG.md | 3 +++ Discord.Net.targets | 2 +- src/Discord.Net/Discord.Net.nuspec | 32 +++++++++++++++--------------- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac159d86f..fcebf0465 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## [2.2.0] - 2020-04-14 +TODO: write changelog + ## [2.1.1] - 2019-06-08 ### Fixed - #994: Remainder parameters now ignore character escaping, as there is no reason to escape characters here (2e95c49) diff --git a/Discord.Net.targets b/Discord.Net.targets index e7b45e52f..3a348e786 100644 --- a/Discord.Net.targets +++ b/Discord.Net.targets @@ -1,7 +1,7 @@ 2.2.0 - dev + RogueException discord;discordapp https://github.com/RogueException/Discord.Net diff --git a/src/Discord.Net/Discord.Net.nuspec b/src/Discord.Net/Discord.Net.nuspec index 4b7717e58..b73d3bf24 100644 --- a/src/Discord.Net/Discord.Net.nuspec +++ b/src/Discord.Net/Discord.Net.nuspec @@ -2,7 +2,7 @@ Discord.Net - 2.2.0-dev$suffix$ + 2.2.0$suffix$ Discord.Net Discord.Net Contributors RogueException @@ -14,25 +14,25 @@ https://github.com/RogueException/Discord.Net/raw/dev/docs/marketing/logo/PackageLogo.png - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + From 8d9e9714f7bdccd634a5fe89ec29de6a9aefed76 Mon Sep 17 00:00:00 2001 From: Christopher Felegy Date: Thu, 16 Apr 2020 21:44:53 -0400 Subject: [PATCH 17/28] meta: bump version to 2.3.0-dev --- CHANGELOG.md | 74 ++++++++++++++++++++++++++++++ Discord.Net.targets | 6 +-- samples/idn/Program.cs | 25 ++++++++-- src/Discord.Net/Discord.Net.nuspec | 34 +++++++------- 4 files changed, 115 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac159d86f..886754052 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,79 @@ # Changelog +## [2.2.0] - 2020-04-16 +### Added +- #1247 Implement Client Status Support (9da11b4) +- #1310 id overload for RemoveReactionAsync (c88b1da) +- #1319 BOOST (faf23de) +- #1326 Added a Rest property to DiscordShardedClient (9fede34) +- #1348 Add Quote Formatting (265da99) +- #1354 Add support for setting X-RateLimit-Precision (9482204) +- #1355 Provide ParameterInfo with error ParseResult (3755a02) +- #1357 add the "Stream" permission. (b00da3d) +- #1358 Add ChannelFollowAdd MessageType (794eba5) +- #1369 Add SelfStream voice state property (9bb08c9) +- #1372 support X-RateLimit-Reset-After (7b9029d) +- #1373 update audit log models (c54867f) +- #1377 Support filtering audit log entries on user, action type, and before entry id (68eb71c) +- #1386 support guild subscription opt-out (0d54207) +- #1387 #1381 Guild PreferredLocale support (a61adb0) +- #1406 CustomStatusGame Activity (79a0ea9) +- #1413 Implemented Message Reference Property (f86c39d) +- #1414 add StartedAt, EndsAt, Elapsed and Remaining to SpotifyGame. (2bba324) +- #1432 Add ability to modify the banner for guilds (d734ce0) +- suppress messages (cd28892) + +### Fixed +- #1318 #1314 Don't parse tags within code blocks (c977f2e) +- #1333 Remove null coalescing on ToEmbedBuilder Color (120c0f7) +- #1337 Fixed attempting to access a non-present optional value (4edda5b) +- #1346 CommandExecuted event will fire when a parameter precondition fails like what happens when standard precondition fails. (e8cb031) +- #1371 Fix keys of guild update audit (b0a595b) +- #1375 Use double precision for X-Reset-After, set CultureInfo when parsing numeric types (606dac3) +- #1392 patch todo in NamedTypeReader (0bda8a4) +- #1405 add .NET Standard 2.1 support for Color (7f0c0c9) +- #1412 GetUsersAsync to use MaxUsersPerBatch const as limit instead of MaxMessagesPerBatch. (5439cba) +- #1416 false-positive detection of CustomStatusGame based on Id property (a484651) +- #1418 #1335 Add isMentionable parameter to CreateRoleAsync in non-breaking manner (1c63fd4) +- #1421 (3ff4e3d) +- include MessageFlags and SuppressEmbedParams (d6d4429) + +### Changed +- #1368 Update ISystemMessage interface to allow reactions (07f4d5f) +- #1417 fix #1415 Re-add support for overwrite permissions for news channels (e627f07) +- use millisecond precision by default (bcb3534) + +### Misc +- #1290 Split Unit and Integration tests into separate projects (a797be9) +- #1328 Fix #1327 Color.ToString returns wrong value (1e8aa08) +- #1329 Fix invalid cref values in docs (363d1c6) +- #1330 Fix spelling mistake in ExclusiveBulkDelete warning (c864f48) +- #1331 Change token explanation (0484fe8) +- #1349 Fixed a spelling error. (af79ed5) +- #1353 [ci skip] Removed duplicate "any" from the readme (15b2a36) +- #1359 Fixing GatewayEncoding comment (52565ed) +- #1379 September 2019 Documentation Update (fd3810e) +- #1382 Fix .NET Core 3.0 compatibility + Drop NS1.3 (d199d93) +- #1388 fix coercion error with DateTime/Offset (3d39704) +- #1393 Utilize ValueTuples (99d7135) +- #1400 Fix #1394 Misworded doc for command params args (1c6ee72) +- #1401 Fix package publishing in azure pipelines (a08d529) +- #1402 Fix packaging (65223a6) +- #1403 Cache regex instances in MessageHelper (007b011) +- #1424 Fix the Comparer descriptions not linking the type (911523d) +- #1426 Fix incorrect and missing colour values for Color fields (9ede6b9) +- #1470 Added System.Linq reference (adf823c) +- temporary sanity checking in SocketGuild (c870e67) +- build and deploy docs automatically (2981d6b) +- 2.2.0 (4b602b4) +- target the Process env-var scope (3c6b376) +- fix metapackage build (1794f95) +- copy only _site to docs-static (a8cdadc) +- do not exit on failed robocopy (fd204ee) +- add idn debugger (91aec9f) +- rename IsStream to IsStreaming (dcd9cdd) +- feature (40844b9) + ## [2.1.1] - 2019-06-08 ### Fixed - #994: Remainder parameters now ignore character escaping, as there is no reason to escape characters here (2e95c49) diff --git a/Discord.Net.targets b/Discord.Net.targets index 3a348e786..a5ab756d9 100644 --- a/Discord.Net.targets +++ b/Discord.Net.targets @@ -1,8 +1,8 @@ - 2.2.0 - - RogueException + 2.3.0 + dev + Discord.Net Contributors discord;discordapp https://github.com/RogueException/Discord.Net http://opensource.org/licenses/MIT diff --git a/samples/idn/Program.cs b/samples/idn/Program.cs index 60af7faf4..ffd8fd1af 100644 --- a/samples/idn/Program.cs +++ b/samples/idn/Program.cs @@ -37,18 +37,22 @@ namespace idn var client = new DiscordSocketClient(new DiscordSocketConfig { LogLevel = LogSeverity.Debug }); var logQueue = new ConcurrentQueue(); var logCancelToken = new CancellationTokenSource(); + int presenceUpdates = 0; client.Log += msg => { logQueue.Enqueue(msg); return Task.CompletedTask; }; + Console.CancelKeyPress += (_ev, _s) => + { + logCancelToken.Cancel(); + }; var logTask = Task.Run(async () => { var fs = new FileStream("idn.log", FileMode.Append); - //var f = File.Open("idn.log", FileMode.Append); - StringBuilder logStringBuilder = new StringBuilder(200); + var logStringBuilder = new StringBuilder(200); string logString = ""; byte[] helloBytes = Encoding.UTF8.GetBytes($"### new log session: {DateTime.Now} ###\n\n"); @@ -58,15 +62,25 @@ namespace idn { if (logQueue.TryDequeue(out var msg)) { + if (msg.Message?.IndexOf("PRESENCE_UPDATE)") > 0) + { + presenceUpdates++; + continue; + } + _ = msg.ToString(builder: logStringBuilder); logStringBuilder.AppendLine(); logString = logStringBuilder.ToString(); Debug.Write(logString, "DNET"); - await fs.WriteAsync(Encoding.UTF8.GetBytes(logString), logCancelToken.Token); + await fs.WriteAsync(Encoding.UTF8.GetBytes(logString)); } await fs.FlushAsync(); - await Task.Delay(100, logCancelToken.Token); + try + { + await Task.Delay(100, logCancelToken.Token); + } + finally { } } byte[] goodbyeBytes = Encoding.UTF8.GetBytes($"#!! end log session: {DateTime.Now} !!#\n\n\n"); @@ -84,6 +98,7 @@ namespace idn var globals = new ScriptGlobals { Client = client, + PUCount = -1, }; while (true) @@ -99,6 +114,7 @@ namespace idn object eval; try { + globals.PUCount = presenceUpdates; eval = await CSharpScript.EvaluateAsync(input, options, globals); } catch (Exception e) @@ -130,6 +146,7 @@ namespace idn public class ScriptGlobals { public DiscordSocketClient Client { get; set; } + public int PUCount { get; set; } } } } diff --git a/src/Discord.Net/Discord.Net.nuspec b/src/Discord.Net/Discord.Net.nuspec index b73d3bf24..5c5ea4072 100644 --- a/src/Discord.Net/Discord.Net.nuspec +++ b/src/Discord.Net/Discord.Net.nuspec @@ -2,10 +2,10 @@ Discord.Net - 2.2.0$suffix$ + 2.3.0-dev$suffix$ Discord.Net Discord.Net Contributors - RogueException + foxbot An asynchronous API wrapper for Discord. This metapackage includes all of the optional Discord.Net components. discord;discordapp https://github.com/RogueException/Discord.Net @@ -14,25 +14,25 @@ https://github.com/RogueException/Discord.Net/raw/dev/docs/marketing/logo/PackageLogo.png - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + From 1f01a2d1fad0fe8d48fab267c116467514af0c5a Mon Sep 17 00:00:00 2001 From: Min Date: Wed, 22 Apr 2020 16:02:22 +1000 Subject: [PATCH 18/28] fix: nullcheck _shards before iterating (#1493) --- src/Discord.Net.WebSocket/DiscordShardedClient.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net.WebSocket/DiscordShardedClient.cs b/src/Discord.Net.WebSocket/DiscordShardedClient.cs index 0877abfd9..8359ca048 100644 --- a/src/Discord.Net.WebSocket/DiscordShardedClient.cs +++ b/src/Discord.Net.WebSocket/DiscordShardedClient.cs @@ -389,8 +389,11 @@ namespace Discord.WebSocket { if (disposing) { - foreach (var client in _shards) - client?.Dispose(); + if (_shards != null) + { + foreach (var client in _shards) + client?.Dispose(); + } _connectionGroupLock?.Dispose(); } From 106f346ddb8ada70ad2227d12e13be58d1234a17 Mon Sep 17 00:00:00 2001 From: Still Hsu <5843208+Still34@users.noreply.github.com> Date: Wed, 22 Apr 2020 14:04:10 +0800 Subject: [PATCH 19/28] docs: 2020 April Documentation Maintenance (#1484) * Add doc page for Named Arguments * Implement minor stylistic changes * Update docfx.json to support NS2.0 Signed-off-by: Still Hsu <5843208+Still34@users.noreply.github.com> * Fix broken xref in basic-operations * Fix broken crefs * Fix wordings in named argument * Fix misleading warning about long-running code * Fix misleading CommandService summary Signed-off-by: Still Hsu <5843208+Still34@users.noreply.github.com> * Update copyright year and version Signed-off-by: Still Hsu <5843208+Still34@users.noreply.github.com> * Escape example captions * Add warning regarding FlattenAsync for GetReactionUsersAsync * Fix a minor grammar mistake Co-authored-by: Joe4evr --- docs/docfx.json | 4 +- docs/faq/basics/basic-operations.md | 2 +- docs/guides/commands/intro.md | 8 +- docs/guides/commands/namedarguments.md | 79 +++++++++++++++++++ docs/guides/toc.yml | 2 + src/Discord.Net.Commands/CommandService.cs | 2 +- .../Entities/Channels/INestedChannel.cs | 8 +- .../Entities/Channels/ITextChannel.cs | 4 +- .../Entities/Guilds/IGuild.cs | 2 +- .../Entities/Messages/IMessage.cs | 29 +++++-- .../Entities/Messages/IUserMessage.cs | 2 +- .../Entities/Users/IGuildUser.cs | 4 +- src/Discord.Net.Core/Entities/Users/IUser.cs | 8 +- .../Extensions/MessageExtensions.cs | 6 +- .../Extensions/UserExtensions.cs | 4 +- src/Discord.Net.Core/IDiscordClient.cs | 2 +- src/Discord.Net.Core/RequestOptions.cs | 3 +- .../DataTypes/ChannelCreateAuditLogData.cs | 4 +- .../DataTypes/ChannelDeleteAuditLogData.cs | 4 +- .../AuditLogs/DataTypes/ChannelInfo.cs | 4 +- .../DiscordWebhookClient.cs | 2 +- 21 files changed, 139 insertions(+), 44 deletions(-) create mode 100644 docs/guides/commands/namedarguments.md diff --git a/docs/docfx.json b/docs/docfx.json index 17cf72388..759dc174f 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -9,7 +9,7 @@ "dest": "api", "filter": "filterConfig.yml", "properties": { - "TargetFramework": "netstandard1.3" + "TargetFramework": "netstandard2.0" } }], "build": { @@ -51,7 +51,7 @@ "overwrite": "_overwrites/**/**.md", "globalMetadata": { "_appTitle": "Discord.Net Documentation", - "_appFooter": "Discord.Net (c) 2015-2019 2.1.1", + "_appFooter": "Discord.Net (c) 2015-2020 2.2.0", "_enableSearch": true, "_appLogoPath": "marketing/logo/SVG/Logomark Purple.svg", "_appFaviconPath": "favicon.ico" diff --git a/docs/faq/basics/basic-operations.md b/docs/faq/basics/basic-operations.md index 93811977f..35c71709f 100644 --- a/docs/faq/basics/basic-operations.md +++ b/docs/faq/basics/basic-operations.md @@ -88,7 +88,7 @@ implement [IEmote] and are valid options. *** -[AddReactionAsync]: xref:Discord.IUserMessage.AddReactionAsync* +[AddReactionAsync]: xref:Discord.IMessage.AddReactionAsync* ## What is a "preemptive rate limit?" diff --git a/docs/guides/commands/intro.md b/docs/guides/commands/intro.md index f8ff1b596..abe7065c1 100644 --- a/docs/guides/commands/intro.md +++ b/docs/guides/commands/intro.md @@ -71,11 +71,11 @@ By now, your module should look like this: > [!WARNING] > **Avoid using long-running code** in your modules wherever possible. -> You should **not** be implementing very much logic into your -> modules, instead, outsource to a service for that. +> Long-running code, by default, within a command module +> can cause gateway thread to be blocked; therefore, interrupting +> the bot's connection to Discord. > -> If you are unfamiliar with Inversion of Control, it is recommended -> to read the MSDN article on [IoC] and [Dependency Injection]. +> You may read more about it in @FAQ.Commands.General . The next step to creating commands is actually creating the commands. diff --git a/docs/guides/commands/namedarguments.md b/docs/guides/commands/namedarguments.md new file mode 100644 index 000000000..890a8463f --- /dev/null +++ b/docs/guides/commands/namedarguments.md @@ -0,0 +1,79 @@ +--- +uid: Guides.Commands.NamedArguments +title: Named Arguments +--- + +# Named Arguments + +By default, arguments for commands are parsed positionally, meaning +that the order matters. But sometimes you may want to define a command +with many optional parameters, and it'd be easier for end-users +to only specify what they want to set, instead of needing them +to specify everything by hand. + +## Setting up Named Arguments + +In order to be able to specify different arguments by name, you have +to create a new class that contains all of the optional values that +the command will use, and apply an instance of +[NamedArgumentTypeAttribute] on it. + +### Example - Creating a Named Arguments Type + +```cs +[NamedArgumentType] +public class NamableArguments +{ + public string First { get; set; } + public string Second { get; set; } + public string Third { get; set; } + public string Fourth { get; set; } +} +``` + +## Usage in a Command + +The command where you want to use these values can be declared like so: +```cs +[Command("act")] +public async Task Act(int requiredArg, NamableArguments namedArgs) +``` + +The command can now be invoked as +`.act 42 first: Hello fourth: "A string with spaces must be wrapped in quotes" second: World`. + +A TypeReader for the named arguments container type is +automatically registered. +It's important that any other arguments that would be required +are placed before the container type. + +> [!IMPORTANT] +> A single command can have only __one__ parameter of a +> type annotated with [NamedArgumentTypeAttribute], and it +> **MUST** be the last parameter in the list. +> A command parameter of such an annotated type +> is automatically treated as if that parameter +> has [RemainderAttribute](xref:Discord.Commands.RemainderAttribute) +> applied. + +## Complex Types + +The TypeReader for Named Argument Types will look for a TypeReader +of every property type, meaning any other command parameter type +will work just the same. + +You can also read multiple values into a single property +by making that property an `IEnumerable`. So for example, if your +Named Argument Type has the following field, +```cs +public IEnumerable Numbers { get; set; } +``` +then the command can be invoked as +`.cmd numbers: "1, 2, 4, 8, 16, 32"` + +## Additional Notes + +The use of [`[OverrideTypeReader]`](xref:Discord.Commands.OverrideTypeReaderAttribute) +is also supported on the properties of a Named Argument Type. + +[NamedArgumentTypeAttribute]: xref:Discord.Commands.NamedArgumentTypeAttribute diff --git a/docs/guides/toc.yml b/docs/guides/toc.yml index 01c245301..a6c38768f 100644 --- a/docs/guides/toc.yml +++ b/docs/guides/toc.yml @@ -27,6 +27,8 @@ topicUid: Guides.Commands.Intro - name: TypeReaders topicUid: Guides.Commands.TypeReaders + - name: Named Arguments + topicUid: Guides.Commands.NamedArguments - name: Preconditions topicUid: Guides.Commands.Preconditions - name: Dependency Injection diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index d5c060fe4..1d4b0e15a 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -36,7 +36,7 @@ namespace Discord.Commands internal readonly AsyncEvent> _logEvent = new AsyncEvent>(); /// - /// Occurs when a command is successfully executed without any error. + /// Occurs when a command is executed. /// /// /// This event is fired when a command has been executed, successfully or not. When a command fails to diff --git a/src/Discord.Net.Core/Entities/Channels/INestedChannel.cs b/src/Discord.Net.Core/Entities/Channels/INestedChannel.cs index e38e1db41..2c9503db1 100644 --- a/src/Discord.Net.Core/Entities/Channels/INestedChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/INestedChannel.cs @@ -40,8 +40,8 @@ namespace Discord /// Creates a new invite to this channel. /// /// - /// The following example creates a new invite to this channel; the invite lasts for 12 hours and can only - /// be used 3 times throughout its lifespan. + /// The following example creates a new invite to this channel; the invite lasts for 12 hours and can only + /// be used 3 times throughout its lifespan. /// /// await guildChannel.CreateInviteAsync(maxAge: 43200, maxUses: 3); /// @@ -60,8 +60,8 @@ namespace Discord /// Gets a collection of all invites to this channel. /// B /// - /// The following example gets all of the invites that have been created in this channel and selects the - /// most used invite. + /// The following example gets all of the invites that have been created in this channel and selects the + /// most used invite. /// /// var invites = await channel.GetInvitesAsync(); /// if (invites.Count == 0) return; diff --git a/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs b/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs index 29c764e3f..a2baf6990 100644 --- a/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs @@ -30,7 +30,7 @@ namespace Discord /// Gets the current slow-mode delay for this channel. /// /// - /// An representing the time in seconds required before the user can send another + /// An representing the time in seconds required before the user can send another /// message; 0 if disabled. /// int SlowModeInterval { get; } @@ -39,7 +39,7 @@ namespace Discord /// Bulk-deletes multiple messages. /// /// - /// The following example gets 250 messages from the channel and deletes them. + /// The following example gets 250 messages from the channel and deletes them. /// /// var messages = await textChannel.GetMessagesAsync(250).FlattenAsync(); /// await textChannel.DeleteMessagesAsync(messages); diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs index a18e91b69..b39a49776 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs @@ -510,7 +510,7 @@ namespace Discord /// Creates a new text channel in this guild. /// /// - /// The following example creates a new text channel under an existing category named Wumpus with a set topic. + /// The following example creates a new text channel under an existing category named Wumpus with a set topic. /// /// diff --git a/src/Discord.Net.Core/Entities/Messages/IMessage.cs b/src/Discord.Net.Core/Entities/Messages/IMessage.cs index 05f505269..aac526831 100644 --- a/src/Discord.Net.Core/Entities/Messages/IMessage.cs +++ b/src/Discord.Net.Core/Entities/Messages/IMessage.cs @@ -161,7 +161,7 @@ namespace Discord /// Adds a reaction to this message. /// /// - /// The following example adds the reaction, 💕, to the message. + /// The following example adds the reaction, 💕, to the message. /// /// await msg.AddReactionAsync(new Emoji("\U0001f495")); /// @@ -177,7 +177,7 @@ namespace Discord /// Removes a reaction from message. /// /// - /// The following example removes the reaction, 💕, added by the message author from the message. + /// The following example removes the reaction, 💕, added by the message author from the message. /// /// await msg.RemoveReactionAsync(new Emoji("\U0001f495"), msg.Author); /// @@ -194,7 +194,7 @@ namespace Discord /// Removes a reaction from message. /// /// - /// The following example removes the reaction, 💕, added by the user with ID 84291986575613952 from the message. + /// The following example removes the reaction, 💕, added by the user with ID 84291986575613952 from the message. /// /// await msg.RemoveReactionAsync(new Emoji("\U0001f495"), 84291986575613952); /// @@ -219,8 +219,25 @@ namespace Discord /// /// Gets all users that reacted to a message with a given emote. /// + /// + /// + /// The returned collection is an asynchronous enumerable object; one must call + /// to access the users as a + /// collection. + /// + /// + /// Do not fetch too many users at once! This may cause unwanted preemptive rate limit or even actual + /// rate limit, causing your bot to freeze! + /// + /// This method will attempt to fetch the number of reactions specified under . + /// The library will attempt to split up the requests according to your and + /// . In other words, should the user request 500 reactions, + /// and the constant is 100, the request will + /// be split into 5 individual requests; thus returning 5 individual asynchronous responses, hence the need + /// of flattening. + /// /// - /// The following example gets the users that have reacted with the emoji 💕 to the message. + /// The following example gets the users that have reacted with the emoji 💕 to the message. /// /// var emoji = new Emoji("\U0001f495"); /// var reactedUsers = await message.GetReactionUsersAsync(emoji, 100).FlattenAsync(); @@ -230,9 +247,7 @@ namespace Discord /// The number of users to request. /// The options to be used when sending the request. /// - /// A paged collection containing a read-only collection of users that has reacted to this message. - /// Flattening the paginated response into a collection of users with - /// is required if you wish to access the users. + /// Paged collection of users. /// IAsyncEnumerable> GetReactionUsersAsync(IEmote emoji, int limit, RequestOptions options = null); } diff --git a/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs b/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs index be2523b21..bc52dd01c 100644 --- a/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs +++ b/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs @@ -17,7 +17,7 @@ namespace Discord /// method and what properties are available, please refer to . /// /// - /// The following example replaces the content of the message with Hello World!. + /// The following example replaces the content of the message with Hello World!. /// /// await msg.ModifyAsync(x => x.Content = "Hello World!"); /// diff --git a/src/Discord.Net.Core/Entities/Users/IGuildUser.cs b/src/Discord.Net.Core/Entities/Users/IGuildUser.cs index ae682afd5..60fa06cbd 100644 --- a/src/Discord.Net.Core/Entities/Users/IGuildUser.cs +++ b/src/Discord.Net.Core/Entities/Users/IGuildUser.cs @@ -72,8 +72,8 @@ namespace Discord /// Gets the level permissions granted to this user to a given channel. /// /// - /// The following example checks if the current user has the ability to send a message with attachment in - /// this channel; if so, uploads a file via . + /// The following example checks if the current user has the ability to send a message with attachment in + /// this channel; if so, uploads a file via . /// /// if (currentUser?.GetPermissions(targetChannel)?.AttachFiles) /// await targetChannel.SendFileAsync("fortnite.png"); diff --git a/src/Discord.Net.Core/Entities/Users/IUser.cs b/src/Discord.Net.Core/Entities/Users/IUser.cs index c59a75de1..c36fb2326 100644 --- a/src/Discord.Net.Core/Entities/Users/IUser.cs +++ b/src/Discord.Net.Core/Entities/Users/IUser.cs @@ -21,8 +21,8 @@ namespace Discord /// example). /// /// - /// The following example attempts to retrieve the user's current avatar and send it to a channel; if one is - /// not set, a default avatar for this user will be returned instead. + /// The following example attempts to retrieve the user's current avatar and send it to a channel; if one is + /// not set, a default avatar for this user will be returned instead. /// /// @@ -90,8 +90,8 @@ namespace Discord /// /// /// - /// The following example attempts to send a direct message to the target user and logs the incident should - /// it fail. + /// The following example attempts to send a direct message to the target user and logs the incident should + /// it fail. /// /// diff --git a/src/Discord.Net.Core/Extensions/MessageExtensions.cs b/src/Discord.Net.Core/Extensions/MessageExtensions.cs index 90ebea92f..64a1d89ab 100644 --- a/src/Discord.Net.Core/Extensions/MessageExtensions.cs +++ b/src/Discord.Net.Core/Extensions/MessageExtensions.cs @@ -39,7 +39,7 @@ namespace Discord /// /// A task that represents the asynchronous operation for adding a reaction to this message. /// - /// + /// /// public static async Task AddReactionsAsync(this IUserMessage msg, IEmote[] reactions, RequestOptions options = null) { @@ -51,7 +51,7 @@ namespace Discord /// /// /// This method does not bulk remove reactions! If you want to clear reactions from a message, - /// + /// /// /// /// @@ -64,7 +64,7 @@ namespace Discord /// /// A task that represents the asynchronous operation for removing a reaction to this message. /// - /// + /// /// public static async Task RemoveReactionsAsync(this IUserMessage msg, IUser user, IEmote[] reactions, RequestOptions options = null) { diff --git a/src/Discord.Net.Core/Extensions/UserExtensions.cs b/src/Discord.Net.Core/Extensions/UserExtensions.cs index f98bf7227..58c762090 100644 --- a/src/Discord.Net.Core/Extensions/UserExtensions.cs +++ b/src/Discord.Net.Core/Extensions/UserExtensions.cs @@ -44,8 +44,8 @@ namespace Discord /// Sends a file to this message channel with an optional caption. /// /// - /// The following example uploads a streamed image that will be called b1nzy.jpg embedded inside a - /// rich embed to the channel. + /// The following example uploads a streamed image that will be called b1nzy.jpg embedded inside a + /// rich embed to the channel. /// /// await channel.SendFileAsync(b1nzyStream, "b1nzy.jpg", /// embed: new EmbedBuilder {ImageUrl = "attachment://b1nzy.jpg"}.Build()); diff --git a/src/Discord.Net.Core/IDiscordClient.cs b/src/Discord.Net.Core/IDiscordClient.cs index e1c900680..f972cd71d 100644 --- a/src/Discord.Net.Core/IDiscordClient.cs +++ b/src/Discord.Net.Core/IDiscordClient.cs @@ -270,7 +270,7 @@ namespace Discord /// /// The options to be used when sending the request. /// - /// A task that represents the asynchronous get operation. The task result contains an + /// A task that represents the asynchronous get operation. The task result contains an /// that represents the number of shards that should be used with this account. /// Task GetRecommendedShardCountAsync(RequestOptions options = null); diff --git a/src/Discord.Net.Core/RequestOptions.cs b/src/Discord.Net.Core/RequestOptions.cs index 6aa0eea12..1b05df2a3 100644 --- a/src/Discord.Net.Core/RequestOptions.cs +++ b/src/Discord.Net.Core/RequestOptions.cs @@ -49,8 +49,7 @@ namespace Discord /// clock for rate-limiting. Defaults to true. /// /// - /// This property can also be set in . - /// + /// This property can also be set in . /// On a per-request basis, the system clock should only be disabled /// when millisecond precision is especially important, and the /// hosting system is known to have a desynced clock. diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelCreateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelCreateAuditLogData.cs index f432b4ca5..7a015d6bc 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelCreateAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelCreateAuditLogData.cs @@ -78,7 +78,7 @@ namespace Discord.Rest /// Gets the current slow-mode delay of the created channel. /// /// - /// An representing the time in seconds required before the user can send another + /// An representing the time in seconds required before the user can send another /// message; 0 if disabled. /// null if this is not mentioned in this entry. /// @@ -95,7 +95,7 @@ namespace Discord.Rest /// Gets the bit-rate that the clients in the created voice channel are requested to use. /// /// - /// An representing the bit-rate (bps) that the created voice channel defines and requests the + /// An representing the bit-rate (bps) that the created voice channel defines and requests the /// client(s) to use. /// null if this is not mentioned in this entry. /// diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelDeleteAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelDeleteAuditLogData.cs index 390749929..81ae7155b 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelDeleteAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelDeleteAuditLogData.cs @@ -71,7 +71,7 @@ namespace Discord.Rest /// Gets the slow-mode delay of the deleted channel. /// /// - /// An representing the time in seconds required before the user can send another + /// An representing the time in seconds required before the user can send another /// message; 0 if disabled. /// null if this is not mentioned in this entry. /// @@ -88,7 +88,7 @@ namespace Discord.Rest /// Gets the bit-rate of this channel if applicable. /// /// - /// An representing the bit-rate set of the voice channel. + /// An representing the bit-rate set of the voice channel. /// null if this is not mentioned in this entry. /// public int? Bitrate { get; } diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelInfo.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelInfo.cs index d6d2fb4b3..0284b63f5 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelInfo.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelInfo.cs @@ -32,7 +32,7 @@ namespace Discord.Rest /// Gets the current slow-mode delay of this channel. /// /// - /// An representing the time in seconds required before the user can send another + /// An representing the time in seconds required before the user can send another /// message; 0 if disabled. /// null if this is not mentioned in this entry. /// @@ -49,7 +49,7 @@ namespace Discord.Rest /// Gets the bit-rate of this channel if applicable. /// /// - /// An representing the bit-rate set for the voice channel; + /// An representing the bit-rate set for the voice channel; /// null if this is not mentioned in this entry. /// public int? Bitrate { get; } diff --git a/src/Discord.Net.Webhook/DiscordWebhookClient.cs b/src/Discord.Net.Webhook/DiscordWebhookClient.cs index 542ec7997..9c90df565 100644 --- a/src/Discord.Net.Webhook/DiscordWebhookClient.cs +++ b/src/Discord.Net.Webhook/DiscordWebhookClient.cs @@ -85,7 +85,7 @@ namespace Discord.Webhook } private static API.DiscordRestApiClient CreateApiClient(DiscordRestConfig config) => new API.DiscordRestApiClient(config.RestClientProvider, DiscordRestConfig.UserAgent); - /// Sends a message using to the channel for this webhook. + /// Sends a message to the channel for this webhook. /// Returns the ID of the created message. public Task SendMessageAsync(string text = null, bool isTTS = false, IEnumerable embeds = null, string username = null, string avatarUrl = null, RequestOptions options = null) From 6d8e2165451cc0e08190ca7c0ffaf848f3de9335 Mon Sep 17 00:00:00 2001 From: Yamboy1 Date: Wed, 22 Apr 2020 18:10:38 +1200 Subject: [PATCH 20/28] docs: Small typo in documentation (#1460) --- src/Discord.Net.Commands/CommandServiceConfig.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.Commands/CommandServiceConfig.cs b/src/Discord.Net.Commands/CommandServiceConfig.cs index 2dedceaa5..3c62063c8 100644 --- a/src/Discord.Net.Commands/CommandServiceConfig.cs +++ b/src/Discord.Net.Commands/CommandServiceConfig.cs @@ -44,7 +44,7 @@ namespace Discord.Commands /// /// /// - /// QuotationMarkAliasMap = new Dictionary<char, char%gt;() + /// QuotationMarkAliasMap = new Dictionary<char, char>() /// { /// {'\"', '\"' }, /// {'“', '”' }, From ed869bd78b8ae152805b449b759714839b429ce5 Mon Sep 17 00:00:00 2001 From: Monica S Date: Sat, 25 Apr 2020 12:12:57 +0100 Subject: [PATCH 21/28] [apibrk] change: Specify WebSocket close code (#1500) * API breaking change: Specify WebSocket close code Should fix #1479 and help overall with resuming sessions. * Also try to resume on missed heartbeats --- .../Net/WebSockets/IWebSocketClient.cs | 2 +- .../WS4NetClient.cs | 10 ++++----- src/Discord.Net.Rest/DiscordRestApiClient.cs | 8 +++---- .../DiscordSocketApiClient.cs | 18 +++++---------- .../DiscordSocketClient.cs | 6 ++--- .../GatewayReconnectException.cs | 22 +++++++++++++++++++ .../Net/DefaultWebSocketClient.cs | 13 ++++++----- 7 files changed, 48 insertions(+), 31 deletions(-) create mode 100644 src/Discord.Net.WebSocket/GatewayReconnectException.cs diff --git a/src/Discord.Net.Core/Net/WebSockets/IWebSocketClient.cs b/src/Discord.Net.Core/Net/WebSockets/IWebSocketClient.cs index 14b41cce1..6791af354 100644 --- a/src/Discord.Net.Core/Net/WebSockets/IWebSocketClient.cs +++ b/src/Discord.Net.Core/Net/WebSockets/IWebSocketClient.cs @@ -14,7 +14,7 @@ namespace Discord.Net.WebSockets void SetCancelToken(CancellationToken cancelToken); Task ConnectAsync(string host); - Task DisconnectAsync(); + Task DisconnectAsync(int closeCode = 1000); Task SendAsync(byte[] data, int index, int count, bool isText); } diff --git a/src/Discord.Net.Providers.WS4Net/WS4NetClient.cs b/src/Discord.Net.Providers.WS4Net/WS4NetClient.cs index ef99c8045..50f19b778 100644 --- a/src/Discord.Net.Providers.WS4Net/WS4NetClient.cs +++ b/src/Discord.Net.Providers.WS4Net/WS4NetClient.cs @@ -40,7 +40,7 @@ namespace Discord.Net.Providers.WS4Net { if (disposing) { - DisconnectInternalAsync(true).GetAwaiter().GetResult(); + DisconnectInternalAsync(isDisposing: true).GetAwaiter().GetResult(); _lock?.Dispose(); _cancelTokenSource?.Dispose(); } @@ -92,19 +92,19 @@ namespace Discord.Net.Providers.WS4Net _waitUntilConnect.Wait(_cancelToken); } - public async Task DisconnectAsync() + public async Task DisconnectAsync(int closeCode = 1000) { await _lock.WaitAsync().ConfigureAwait(false); try { - await DisconnectInternalAsync().ConfigureAwait(false); + await DisconnectInternalAsync(closeCode: closeCode).ConfigureAwait(false); } finally { _lock.Release(); } } - private Task DisconnectInternalAsync(bool isDisposing = false) + private Task DisconnectInternalAsync(int closeCode = 1000, bool isDisposing = false) { _disconnectCancelTokenSource.Cancel(); if (_client == null) @@ -112,7 +112,7 @@ namespace Discord.Net.Providers.WS4Net if (_client.State == WebSocketState.Open) { - try { _client.Close(1000, ""); } + try { _client.Close(closeCode, ""); } catch { } } diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index ff6d17240..3a46dcf21 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -47,7 +47,7 @@ namespace Discord.API internal ulong? CurrentUserId { get; set; } public RateLimitPrecision RateLimitPrecision { get; private set; } internal bool UseSystemClock { get; set; } - + internal JsonSerializer Serializer => _serializer; /// Unknown OAuth token type. @@ -164,7 +164,7 @@ namespace Discord.API try { _loginCancelToken?.Cancel(false); } catch { } - await DisconnectInternalAsync().ConfigureAwait(false); + await DisconnectInternalAsync(null).ConfigureAwait(false); await RequestQueue.ClearAsync().ConfigureAwait(false); await RequestQueue.SetCancelTokenAsync(CancellationToken.None).ConfigureAwait(false); @@ -175,7 +175,7 @@ namespace Discord.API } internal virtual Task ConnectInternalAsync() => Task.Delay(0); - internal virtual Task DisconnectInternalAsync() => Task.Delay(0); + internal virtual Task DisconnectInternalAsync(Exception ex = null) => Task.Delay(0); //Core internal Task SendAsync(string method, Expression> endpointExpr, BucketIds ids, @@ -1062,7 +1062,7 @@ namespace Discord.API { foreach (var roleId in args.RoleIds.Value) Preconditions.NotEqual(roleId, 0, nameof(roleId)); - } + } options = RequestOptions.CreateOrClone(options); diff --git a/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs b/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs index 9313f0711..ef97615e2 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs @@ -164,26 +164,17 @@ namespace Discord.API } } - public async Task DisconnectAsync() + public async Task DisconnectAsync(Exception ex = null) { await _stateLock.WaitAsync().ConfigureAwait(false); try { - await DisconnectInternalAsync().ConfigureAwait(false); - } - finally { _stateLock.Release(); } - } - public async Task DisconnectAsync(Exception ex) - { - await _stateLock.WaitAsync().ConfigureAwait(false); - try - { - await DisconnectInternalAsync().ConfigureAwait(false); + await DisconnectInternalAsync(ex).ConfigureAwait(false); } finally { _stateLock.Release(); } } /// This client is not configured with WebSocket support. - internal override async Task DisconnectInternalAsync() + internal override async Task DisconnectInternalAsync(Exception ex = null) { if (WebSocketClient == null) throw new NotSupportedException("This client is not configured with WebSocket support."); @@ -194,6 +185,9 @@ namespace Discord.API try { _connectCancelToken?.Cancel(false); } catch { } + if (ex is GatewayReconnectException) + await WebSocketClient.DisconnectAsync(4000); + else await WebSocketClient.DisconnectAsync().ConfigureAwait(false); ConnectionState = ConnectionState.Disconnected; diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index ed142d001..1819966b3 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -264,7 +264,7 @@ namespace Discord.WebSocket { await _gatewayLogger.DebugAsync("Disconnecting ApiClient").ConfigureAwait(false); - await ApiClient.DisconnectAsync().ConfigureAwait(false); + await ApiClient.DisconnectAsync(ex).ConfigureAwait(false); //Wait for tasks to complete await _gatewayLogger.DebugAsync("Waiting for heartbeater").ConfigureAwait(false); @@ -511,7 +511,7 @@ namespace Discord.WebSocket case GatewayOpCode.Reconnect: { await _gatewayLogger.DebugAsync("Received Reconnect").ConfigureAwait(false); - _connection.Error(new Exception("Server requested a reconnect")); + _connection.Error(new GatewayReconnectException("Server requested a reconnect")); } break; case GatewayOpCode.Dispatch: @@ -1689,7 +1689,7 @@ namespace Discord.WebSocket { if (ConnectionState == ConnectionState.Connected && (_guildDownloadTask?.IsCompleted ?? true)) { - _connection.Error(new Exception("Server missed last heartbeat")); + _connection.Error(new GatewayReconnectException("Server missed last heartbeat")); return; } } diff --git a/src/Discord.Net.WebSocket/GatewayReconnectException.cs b/src/Discord.Net.WebSocket/GatewayReconnectException.cs new file mode 100644 index 000000000..1a8024558 --- /dev/null +++ b/src/Discord.Net.WebSocket/GatewayReconnectException.cs @@ -0,0 +1,22 @@ +using System; + +namespace Discord.WebSocket +{ + /// + /// An exception thrown when the gateway client has been requested to + /// reconnect. + /// + public class GatewayReconnectException : Exception + { + /// + /// Creates a new instance of the + /// type. + /// + /// + /// The reason why the gateway has been requested to reconnect. + /// + public GatewayReconnectException(string message) + : base(message) + { } + } +} diff --git a/src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs b/src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs index 36a6fea4f..4723ae57a 100644 --- a/src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs +++ b/src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs @@ -44,7 +44,7 @@ namespace Discord.Net.WebSockets { if (disposing) { - DisconnectInternalAsync(true).GetAwaiter().GetResult(); + DisconnectInternalAsync(isDisposing: true).GetAwaiter().GetResult(); _disconnectTokenSource?.Dispose(); _cancelTokenSource?.Dispose(); _lock?.Dispose(); @@ -94,19 +94,19 @@ namespace Discord.Net.WebSockets _task = RunAsync(_cancelToken); } - public async Task DisconnectAsync() + public async Task DisconnectAsync(int closeCode = 1000) { await _lock.WaitAsync().ConfigureAwait(false); try { - await DisconnectInternalAsync().ConfigureAwait(false); + await DisconnectInternalAsync(closeCode: closeCode).ConfigureAwait(false); } finally { _lock.Release(); } } - private async Task DisconnectInternalAsync(bool isDisposing = false) + private async Task DisconnectInternalAsync(int closeCode = 1000, bool isDisposing = false) { try { _disconnectTokenSource.Cancel(false); } catch { } @@ -117,7 +117,8 @@ namespace Discord.Net.WebSockets { if (!isDisposing) { - try { await _client.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "", new CancellationToken()); } + var status = (WebSocketCloseStatus)closeCode; + try { await _client.CloseOutputAsync(status, "", new CancellationToken()); } catch { } } try { _client.Dispose(); } @@ -141,7 +142,7 @@ namespace Discord.Net.WebSockets await _lock.WaitAsync().ConfigureAwait(false); try { - await DisconnectInternalAsync(false); + await DisconnectInternalAsync(isDisposing: false); } finally { From 479e28358e488ef791788e2b74f0840ca5d1f043 Mon Sep 17 00:00:00 2001 From: NeKz Date: Thu, 7 May 2020 15:04:33 +0200 Subject: [PATCH 22/28] feature: Implement missing audit log types (#1458) * Implement missing audit log types * Use IUser properties --- .../Entities/AuditLogs/ActionType.cs | 26 +++++++++- .../API/Common/AuditLogOptions.cs | 7 +-- .../Entities/AuditLogs/AuditLogHelper.cs | 6 +++ .../AuditLogs/DataTypes/BotAddAuditLogData.cs | 32 +++++++++++++ .../DataTypes/MemberDisconnectAuditLogData.cs | 29 +++++++++++ .../DataTypes/MemberMoveAuditLogData.cs | 37 ++++++++++++++ .../MessageBulkDeleteAuditLogData.cs | 38 +++++++++++++++ .../DataTypes/MessageDeleteAuditLogData.cs | 15 +++--- .../DataTypes/MessagePinAuditLogData.cs | 48 +++++++++++++++++++ .../DataTypes/MessageUnpinAuditLogData.cs | 48 +++++++++++++++++++ 10 files changed, 276 insertions(+), 10 deletions(-) create mode 100644 src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BotAddAuditLogData.cs create mode 100644 src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberDisconnectAuditLogData.cs create mode 100644 src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberMoveAuditLogData.cs create mode 100644 src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageBulkDeleteAuditLogData.cs create mode 100644 src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessagePinAuditLogData.cs create mode 100644 src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageUnpinAuditLogData.cs diff --git a/src/Discord.Net.Core/Entities/AuditLogs/ActionType.cs b/src/Discord.Net.Core/Entities/AuditLogs/ActionType.cs index 2561a0970..1728b2021 100644 --- a/src/Discord.Net.Core/Entities/AuditLogs/ActionType.cs +++ b/src/Discord.Net.Core/Entities/AuditLogs/ActionType.cs @@ -61,6 +61,18 @@ namespace Discord /// A guild member's role collection was updated. /// MemberRoleUpdated = 25, + /// + /// A guild member moved to a voice channel. + /// + MemberMoved = 26, + /// + /// A guild member disconnected from a voice channel. + /// + MemberDisconnected = 27, + /// + /// A bot was added to this guild. + /// + BotAdded = 28, /// /// A role was created in this guild. @@ -117,6 +129,18 @@ namespace Discord /// /// A message was deleted from this guild. /// - MessageDeleted = 72 + MessageDeleted = 72, + /// + /// Multiple messages were deleted from this guild. + /// + MessageBulkDeleted = 73, + /// + /// A message was pinned from this guild. + /// + MessagePinned = 74, + /// + /// A message was unpinned from this guild. + /// + MessageUnpinned = 75, } } diff --git a/src/Discord.Net.Rest/API/Common/AuditLogOptions.cs b/src/Discord.Net.Rest/API/Common/AuditLogOptions.cs index 24141d90c..b666215e2 100644 --- a/src/Discord.Net.Rest/API/Common/AuditLogOptions.cs +++ b/src/Discord.Net.Rest/API/Common/AuditLogOptions.cs @@ -4,11 +4,12 @@ namespace Discord.API { internal class AuditLogOptions { - //Message delete [JsonProperty("count")] - public int? MessageDeleteCount { get; set; } + public int? Count { get; set; } [JsonProperty("channel_id")] - public ulong? MessageDeleteChannelId { get; set; } + public ulong? ChannelId { get; set; } + [JsonProperty("message_id")] + public ulong? MessageId { get; set; } //Prune [JsonProperty("delete_member_days")] diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/AuditLogHelper.cs b/src/Discord.Net.Rest/Entities/AuditLogs/AuditLogHelper.cs index 7936343f3..696917203 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/AuditLogHelper.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/AuditLogHelper.cs @@ -27,6 +27,9 @@ namespace Discord.Rest [ActionType.Unban] = UnbanAuditLogData.Create, [ActionType.MemberUpdated] = MemberUpdateAuditLogData.Create, [ActionType.MemberRoleUpdated] = MemberRoleAuditLogData.Create, + [ActionType.MemberMoved] = MemberMoveAuditLogData.Create, + [ActionType.MemberDisconnected] = MemberDisconnectAuditLogData.Create, + [ActionType.BotAdded] = BotAddAuditLogData.Create, [ActionType.RoleCreated] = RoleCreateAuditLogData.Create, [ActionType.RoleUpdated] = RoleUpdateAuditLogData.Create, @@ -45,6 +48,9 @@ namespace Discord.Rest [ActionType.EmojiDeleted] = EmoteDeleteAuditLogData.Create, [ActionType.MessageDeleted] = MessageDeleteAuditLogData.Create, + [ActionType.MessageBulkDeleted] = MessageBulkDeleteAuditLogData.Create, + [ActionType.MessagePinned] = MessagePinAuditLogData.Create, + [ActionType.MessageUnpinned] = MessageUnpinAuditLogData.Create, }; public static IAuditLogData CreateData(BaseDiscordClient discord, Model log, EntryModel entry) diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BotAddAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BotAddAuditLogData.cs new file mode 100644 index 000000000..0d12e4609 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BotAddAuditLogData.cs @@ -0,0 +1,32 @@ +using System.Linq; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + /// + /// Contains a piece of audit log data related to a adding a bot to a guild. + /// + public class BotAddAuditLogData : IAuditLogData + { + private BotAddAuditLogData(IUser bot) + { + Target = bot; + } + + internal static BotAddAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId); + return new BotAddAuditLogData(RestUser.Create(discord, userInfo)); + } + + /// + /// Gets the bot that was added. + /// + /// + /// A user object representing the bot. + /// + public IUser Target { get; } + } +} diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberDisconnectAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberDisconnectAuditLogData.cs new file mode 100644 index 000000000..b0374dc86 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberDisconnectAuditLogData.cs @@ -0,0 +1,29 @@ +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + /// + /// Contains a piece of audit log data related to disconnecting members from voice channels. + /// + public class MemberDisconnectAuditLogData : IAuditLogData + { + private MemberDisconnectAuditLogData(int count) + { + MemberCount = count; + } + + internal static MemberDisconnectAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + return new MemberDisconnectAuditLogData(entry.Options.Count.Value); + } + + /// + /// Gets the number of members that were disconnected. + /// + /// + /// An representing the number of members that were disconnected from a voice channel. + /// + public int MemberCount { get; } + } +} diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberMoveAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberMoveAuditLogData.cs new file mode 100644 index 000000000..f5373d34d --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberMoveAuditLogData.cs @@ -0,0 +1,37 @@ +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + /// + /// Contains a piece of audit log data related to moving members between voice channels. + /// + public class MemberMoveAuditLogData : IAuditLogData + { + private MemberMoveAuditLogData(ulong channelId, int count) + { + ChannelId = channelId; + MemberCount = count; + } + + internal static MemberMoveAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + return new MemberMoveAuditLogData(entry.Options.ChannelId.Value, entry.Options.Count.Value); + } + + /// + /// Gets the ID of the channel that the members were moved to. + /// + /// + /// A representing the snowflake identifier for the channel that the members were moved to. + /// + public ulong ChannelId { get; } + /// + /// Gets the number of members that were moved. + /// + /// + /// An representing the number of members that were moved to another voice channel. + /// + public int MemberCount { get; } + } +} diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageBulkDeleteAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageBulkDeleteAuditLogData.cs new file mode 100644 index 000000000..7a9846349 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageBulkDeleteAuditLogData.cs @@ -0,0 +1,38 @@ +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + /// + /// Contains a piece of audit log data related to message deletion(s). + /// + public class MessageBulkDeleteAuditLogData : IAuditLogData + { + private MessageBulkDeleteAuditLogData(ulong channelId, int count) + { + ChannelId = channelId; + MessageCount = count; + } + + internal static MessageBulkDeleteAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + return new MessageBulkDeleteAuditLogData(entry.TargetId.Value, entry.Options.Count.Value); + } + + /// + /// Gets the ID of the channel that the messages were deleted from. + /// + /// + /// A representing the snowflake identifier for the channel that the messages were + /// deleted from. + /// + public ulong ChannelId { get; } + /// + /// Gets the number of messages that were deleted. + /// + /// + /// An representing the number of messages that were deleted from the channel. + /// + public int MessageCount { get; } + } +} diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageDeleteAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageDeleteAuditLogData.cs index c6b2e1053..66b3f7d83 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageDeleteAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageDeleteAuditLogData.cs @@ -1,3 +1,5 @@ +using System.Linq; + using Model = Discord.API.AuditLog; using EntryModel = Discord.API.AuditLogEntry; @@ -8,16 +10,17 @@ namespace Discord.Rest /// public class MessageDeleteAuditLogData : IAuditLogData { - private MessageDeleteAuditLogData(ulong channelId, int count, ulong authorId) + private MessageDeleteAuditLogData(ulong channelId, int count, IUser user) { ChannelId = channelId; MessageCount = count; - AuthorId = authorId; + Target = user; } internal static MessageDeleteAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) { - return new MessageDeleteAuditLogData(entry.Options.MessageDeleteChannelId.Value, entry.Options.MessageDeleteCount.Value, entry.TargetId.Value); + var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId); + return new MessageDeleteAuditLogData(entry.Options.ChannelId.Value, entry.Options.Count.Value, RestUser.Create(discord, userInfo)); } /// @@ -36,11 +39,11 @@ namespace Discord.Rest /// public ulong ChannelId { get; } /// - /// Gets the author of the messages that were deleted. + /// Gets the user of the messages that were deleted. /// /// - /// A representing the snowflake identifier for the user that created the deleted messages. + /// A user object representing the user that created the deleted messages. /// - public ulong AuthorId { get; } + public IUser Target { get; } } } diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessagePinAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessagePinAuditLogData.cs new file mode 100644 index 000000000..020171152 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessagePinAuditLogData.cs @@ -0,0 +1,48 @@ +using System.Linq; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + /// + /// Contains a piece of audit log data related to a pinned message. + /// + public class MessagePinAuditLogData : IAuditLogData + { + private MessagePinAuditLogData(ulong messageId, ulong channelId, IUser user) + { + MessageId = messageId; + ChannelId = channelId; + Target = user; + } + + internal static MessagePinAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId); + return new MessagePinAuditLogData(entry.Options.MessageId.Value, entry.Options.ChannelId.Value, RestUser.Create(discord, userInfo)); + } + + /// + /// Gets the ID of the messages that was pinned. + /// + /// + /// A representing the snowflake identifier for the messages that was pinned. + /// + public ulong MessageId { get; } + /// + /// Gets the ID of the channel that the message was pinned from. + /// + /// + /// A representing the snowflake identifier for the channel that the message was pinned from. + /// + public ulong ChannelId { get; } + /// + /// Gets the user of the message that was pinned. + /// + /// + /// A user object representing the user that created the pinned message. + /// + public IUser Target { get; } + } +} diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageUnpinAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageUnpinAuditLogData.cs new file mode 100644 index 000000000..1b3ff96f3 --- /dev/null +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageUnpinAuditLogData.cs @@ -0,0 +1,48 @@ +using System.Linq; + +using Model = Discord.API.AuditLog; +using EntryModel = Discord.API.AuditLogEntry; + +namespace Discord.Rest +{ + /// + /// Contains a piece of audit log data related to an unpinned message. + /// + public class MessageUnpinAuditLogData : IAuditLogData + { + private MessageUnpinAuditLogData(ulong messageId, ulong channelId, IUser user) + { + MessageId = messageId; + ChannelId = channelId; + Target = user; + } + + internal static MessageUnpinAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) + { + var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId); + return new MessageUnpinAuditLogData(entry.Options.MessageId.Value, entry.Options.ChannelId.Value, RestUser.Create(discord, userInfo)); + } + + /// + /// Gets the ID of the messages that was unpinned. + /// + /// + /// A representing the snowflake identifier for the messages that was unpinned. + /// + public ulong MessageId { get; } + /// + /// Gets the ID of the channel that the message was unpinned from. + /// + /// + /// A representing the snowflake identifier for the channel that the message was unpinned from. + /// + public ulong ChannelId { get; } + /// + /// Gets the user of the message that was unpinned. + /// + /// + /// A user object representing the user that created the unpinned message. + /// + public IUser Target { get; } + } +} From 41543a80840e7af4356e720d13d3b9d87633f4f2 Mon Sep 17 00:00:00 2001 From: NeKz Date: Thu, 7 May 2020 15:09:46 +0200 Subject: [PATCH 23/28] fix: Fix Deserialization in Audit Log Data Types (#1509) * Get overwrite id and type from options * Create overwrites via API model --- .../DataTypes/ChannelCreateAuditLogData.cs | 17 +++++------------ .../DataTypes/OverwriteDeleteAuditLogData.cs | 11 ++++++----- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelCreateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelCreateAuditLogData.cs index 7a015d6bc..5c2f81ae6 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelCreateAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelCreateAuditLogData.cs @@ -25,7 +25,6 @@ namespace Discord.Rest internal static ChannelCreateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry) { var changes = entry.Changes; - var overwrites = new List(); var overwritesModel = changes.FirstOrDefault(x => x.ChangedProperty == "permission_overwrites"); var typeModel = changes.FirstOrDefault(x => x.ChangedProperty == "type"); @@ -34,23 +33,17 @@ namespace Discord.Rest var nsfwModel = changes.FirstOrDefault(x => x.ChangedProperty == "nsfw"); var bitrateModel = changes.FirstOrDefault(x => x.ChangedProperty == "bitrate"); + var overwrites = overwritesModel.NewValue.ToObject(discord.ApiClient.Serializer) + .Select(x => new Overwrite(x.TargetId, x.TargetType, new OverwritePermissions(x.Allow, x.Deny))) + .ToList(); var type = typeModel.NewValue.ToObject(discord.ApiClient.Serializer); var name = nameModel.NewValue.ToObject(discord.ApiClient.Serializer); int? rateLimitPerUser = rateLimitPerUserModel?.NewValue?.ToObject(discord.ApiClient.Serializer); bool? nsfw = nsfwModel?.NewValue?.ToObject(discord.ApiClient.Serializer); int? bitrate = bitrateModel?.NewValue?.ToObject(discord.ApiClient.Serializer); + var id = entry.TargetId.Value; - foreach (var overwrite in overwritesModel.NewValue) - { - var deny = overwrite["deny"].ToObject(discord.ApiClient.Serializer); - var permType = overwrite["type"].ToObject(discord.ApiClient.Serializer); - var id = overwrite["id"].ToObject(discord.ApiClient.Serializer); - var allow = overwrite["allow"].ToObject(discord.ApiClient.Serializer); - - overwrites.Add(new Overwrite(id, permType, new OverwritePermissions(allow, deny))); - } - - return new ChannelCreateAuditLogData(entry.TargetId.Value, name, type, rateLimitPerUser, nsfw, bitrate, overwrites.ToReadOnlyCollection()); + return new ChannelCreateAuditLogData(id, name, type, rateLimitPerUser, nsfw, bitrate, overwrites.ToReadOnlyCollection()); } /// diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteDeleteAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteDeleteAuditLogData.cs index a193e76ce..dc8948d37 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteDeleteAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteDeleteAuditLogData.cs @@ -21,16 +21,17 @@ namespace Discord.Rest var changes = entry.Changes; var denyModel = changes.FirstOrDefault(x => x.ChangedProperty == "deny"); - var typeModel = changes.FirstOrDefault(x => x.ChangedProperty == "type"); - var idModel = changes.FirstOrDefault(x => x.ChangedProperty == "id"); var allowModel = changes.FirstOrDefault(x => x.ChangedProperty == "allow"); var deny = denyModel.OldValue.ToObject(discord.ApiClient.Serializer); - var type = typeModel.OldValue.ToObject(discord.ApiClient.Serializer); - var id = idModel.OldValue.ToObject(discord.ApiClient.Serializer); var allow = allowModel.OldValue.ToObject(discord.ApiClient.Serializer); - return new OverwriteDeleteAuditLogData(entry.TargetId.Value, new Overwrite(id, type, new OverwritePermissions(allow, deny))); + var permissions = new OverwritePermissions(allow, deny); + + var id = entry.Options.OverwriteTargetId.Value; + var type = entry.Options.OverwriteType; + + return new OverwriteDeleteAuditLogData(entry.TargetId.Value, new Overwrite(id, type, permissions)); } /// From f8b2b5627eca485191362c0346dc448d9853b451 Mon Sep 17 00:00:00 2001 From: Paulo Date: Thu, 7 May 2020 10:12:08 -0300 Subject: [PATCH 24/28] fix: Move content check for ModifyMessage (#1503) good catch, thanks! --- src/Discord.Net.Rest/DiscordRestApiClient.cs | 9 ++------- src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs | 5 +++++ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index 3a46dcf21..a726ef75d 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -602,13 +602,8 @@ namespace Discord.API Preconditions.NotEqual(channelId, 0, nameof(channelId)); Preconditions.NotEqual(messageId, 0, nameof(messageId)); Preconditions.NotNull(args, nameof(args)); - if (args.Content.IsSpecified) - { - if (!args.Embed.IsSpecified) - Preconditions.NotNullOrEmpty(args.Content, nameof(args.Content)); - if (args.Content.Value?.Length > DiscordConfig.MaxMessageSize) - throw new ArgumentOutOfRangeException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content)); - } + if (args.Content.IsSpecified && args.Content.Value?.Length > DiscordConfig.MaxMessageSize) + throw new ArgumentOutOfRangeException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content)); options = RequestOptions.CreateOrClone(options); var ids = new BucketIds(channelId: channelId); diff --git a/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs b/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs index 75892defb..b29eca62e 100644 --- a/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs +++ b/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs @@ -32,6 +32,11 @@ namespace Discord.Rest var args = new MessageProperties(); func(args); + bool hasText = args.Content.IsSpecified ? !string.IsNullOrEmpty(args.Content.Value) : !string.IsNullOrEmpty(msg.Content); + bool hasEmbed = args.Embed.IsSpecified ? args.Embed.Value != null : msg.Embeds.Any(); + if (!hasText && !hasEmbed) + Preconditions.NotNullOrEmpty(args.Content.IsSpecified ? args.Content.Value : string.Empty, nameof(args.Content)); + var apiArgs = new API.Rest.ModifyMessageParams { Content = args.Content, From 89b6b7e1a5cc3ab6264d8606ee17908f4abdadd5 Mon Sep 17 00:00:00 2001 From: Chris Johnston <16418643+Chris-Johnston@users.noreply.github.com> Date: Thu, 7 May 2020 06:13:57 -0700 Subject: [PATCH 25/28] feature: Include allowed mentions payload on message creation (#1455) * Feature: Allowed mentions object on msg create (interface breaking) This change implements the AllowedMentions object for the payload of message creation. By default, the mentions behavior remains unchanged, the message content is parsed for all mentionable entities and they are all notified. If this payload is not null, it will use the content of the allowed_mentions field to determine if a role is notified, or just displayed. This change is interface breaking. This follows the conventions of keeping RequestOptions as the last argument, but could break some users who specify each of these arguments without using named arguments. * lint: remove commented-out code This change removes the commented-out code which was added during testing from the previous commit. * fix interface break: reorder allowedMentions arg so that it's after options This change modifies the order of the AllowedMentions argument of SendMessageAsync so that this addition shouldn't be interface breaking. The downside to this change is that it breaks the convention followed by other methods, where the RequestOptions argument is normally last. * docs: fix typo in allowedMentions arg doc * fix interface break arg from IRestMessageChannel * docs: update xmldoc for allowedMentions args * fix interface breaking arg order for ISocketMessageChannel * fix mocked classes that weren't updated * fix: RestDMChannel#SendMessageAsync bug, allowed mentions always null This change fixes a bug that was introduced while testing changes to the interface of the SendMessageAsync method to try and retain interface compatibility * docs: update xmldoc for AllowedMentions type * docs: reword xmldoc of AllowedMentionTypes type * docs: fix typo * fix: validate that User/Role flags and UserIds/RoleIds lists are mutually exclusive This change adds validation to SendMessageAsync which checks that the User flag is mutually exclusive with the list of UserIds, and that the Role flag is also mutually exclusive with the list of RoleIds * docs: reword summaries for AllowedMentions type * Add util properties for specifying all or no mentions Adds read only properties which specify that all mentions or no mentions will notify users. These settings might be more common than others, so this would make them easier to use. * docs: Resolve PR comments for documentation issues/typos --- src/Discord.Net.Commands/ModuleBase.cs | 8 ++- .../Entities/Channels/IMessageChannel.cs | 6 +- .../Entities/Messages/AllowedMentionTypes.cs | 24 +++++++ .../Entities/Messages/AllowedMentions.cs | 64 +++++++++++++++++++ .../Extensions/UserExtensions.cs | 9 ++- src/Discord.Net.Rest/API/Common/Message.cs | 2 + .../API/Rest/CreateMessageParams.cs | 4 +- .../Entities/Channels/ChannelHelper.cs | 23 ++++++- .../Entities/Channels/IRestMessageChannel.cs | 6 +- .../Entities/Channels/RestDMChannel.cs | 8 +-- .../Entities/Channels/RestGroupChannel.cs | 9 +-- .../Entities/Channels/RestTextChannel.cs | 8 +-- .../Entities/Messages/AllowedMentions.cs | 15 +++++ .../Extensions/EntityExtensions.cs | 19 ++++++ .../Channels/ISocketMessageChannel.cs | 6 +- .../Entities/Channels/SocketDMChannel.cs | 8 +-- .../Entities/Channels/SocketGroupChannel.cs | 8 +-- .../Entities/Channels/SocketTextChannel.cs | 8 +-- .../MockedEntities/MockedDMChannel.cs | 2 +- .../MockedEntities/MockedGroupChannel.cs | 2 +- .../MockedEntities/MockedTextChannel.cs | 2 +- 21 files changed, 204 insertions(+), 37 deletions(-) create mode 100644 src/Discord.Net.Core/Entities/Messages/AllowedMentionTypes.cs create mode 100644 src/Discord.Net.Core/Entities/Messages/AllowedMentions.cs create mode 100644 src/Discord.Net.Rest/Entities/Messages/AllowedMentions.cs diff --git a/src/Discord.Net.Commands/ModuleBase.cs b/src/Discord.Net.Commands/ModuleBase.cs index 9cd4ea15d..ec1722c70 100644 --- a/src/Discord.Net.Commands/ModuleBase.cs +++ b/src/Discord.Net.Commands/ModuleBase.cs @@ -31,9 +31,13 @@ namespace Discord.Commands /// /// Specifies if Discord should read this aloud using text-to-speech. /// An embed to be displayed alongside the . - protected virtual async Task ReplyAsync(string message = null, bool isTTS = false, Embed embed = null, RequestOptions options = null) + /// + /// Specifies if notifications are sent for mentioned users and roles in the . + /// If null, all mentioned roles and users will be notified. + /// + protected virtual async Task ReplyAsync(string message = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null) { - return await Context.Channel.SendMessageAsync(message, isTTS, embed, options).ConfigureAwait(false); + return await Context.Channel.SendMessageAsync(message, isTTS, embed, options, allowedMentions).ConfigureAwait(false); } /// /// The method to execute before executing the command. diff --git a/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs b/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs index b5aa69d55..f5b986295 100644 --- a/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs @@ -23,11 +23,15 @@ namespace Discord /// Determines whether the message should be read aloud by Discord or not. /// The to be sent. /// The options to be used when sending the request. + /// + /// Specifies if notifications are sent for mentioned users and roles in the message . + /// If null, all mentioned roles and users will be notified. + /// /// /// A task that represents an asynchronous send operation for delivering the message. The task result /// contains the sent message. /// - Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null); + Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null); /// /// Sends a file to this message channel with an optional caption. /// diff --git a/src/Discord.Net.Core/Entities/Messages/AllowedMentionTypes.cs b/src/Discord.Net.Core/Entities/Messages/AllowedMentionTypes.cs new file mode 100644 index 000000000..3ce6531b7 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Messages/AllowedMentionTypes.cs @@ -0,0 +1,24 @@ +using System; + +namespace Discord +{ + /// + /// Specifies the type of mentions that will be notified from the message content. + /// + [Flags] + public enum AllowedMentionTypes + { + /// + /// Controls role mentions. + /// + Roles, + /// + /// Controls user mentions. + /// + Users, + /// + /// Controls @everyone and @here mentions. + /// + Everyone, + } +} diff --git a/src/Discord.Net.Core/Entities/Messages/AllowedMentions.cs b/src/Discord.Net.Core/Entities/Messages/AllowedMentions.cs new file mode 100644 index 000000000..9b168bbd0 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Messages/AllowedMentions.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; + +namespace Discord +{ + /// + /// Defines which mentions and types of mentions that will notify users from the message content. + /// + public class AllowedMentions + { + private static readonly Lazy none = new Lazy(() => new AllowedMentions()); + private static readonly Lazy all = new Lazy(() => + new AllowedMentions(AllowedMentionTypes.Everyone | AllowedMentionTypes.Users | AllowedMentionTypes.Roles)); + + /// + /// Gets a value which indicates that no mentions in the message content should notify users. + /// + public static AllowedMentions None => none.Value; + + /// + /// Gets a value which indicates that all mentions in the message content should notify users. + /// + public static AllowedMentions All => all.Value; + + /// + /// Gets or sets the type of mentions that will be parsed from the message content. + /// + /// + /// The flag is mutually exclusive with the + /// property, and the flag is mutually exclusive with the + /// property. + /// If null, only the ids specified in and will be mentioned. + /// + public AllowedMentionTypes? AllowedTypes { get; set; } + + /// + /// Gets or sets the list of all role ids that will be mentioned. + /// This property is mutually exclusive with the + /// flag of the property. If the flag is set, the value of this property + /// must be null or empty. + /// + public List RoleIds { get; set; } + + /// + /// Gets or sets the list of all user ids that will be mentioned. + /// This property is mutually exclusive with the + /// flag of the property. If the flag is set, the value of this property + /// must be null or empty. + /// + public List UserIds { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The types of mentions to parse from the message content. + /// If null, only the ids specified in and will be mentioned. + /// + public AllowedMentions(AllowedMentionTypes? allowedTypes = null) + { + AllowedTypes = allowedTypes; + } + } +} diff --git a/src/Discord.Net.Core/Extensions/UserExtensions.cs b/src/Discord.Net.Core/Extensions/UserExtensions.cs index 58c762090..90f26828a 100644 --- a/src/Discord.Net.Core/Extensions/UserExtensions.cs +++ b/src/Discord.Net.Core/Extensions/UserExtensions.cs @@ -28,6 +28,10 @@ namespace Discord /// Whether the message should be read aloud by Discord or not. /// The to be sent. /// The options to be used when sending the request. + /// + /// Specifies if notifications are sent for mentioned users and roles in the message . + /// If null, all mentioned roles and users will be notified. + /// /// /// A task that represents the asynchronous send operation. The task result contains the sent message. /// @@ -35,9 +39,10 @@ namespace Discord string text = null, bool isTTS = false, Embed embed = null, - RequestOptions options = null) + RequestOptions options = null, + AllowedMentions allowedMentions = null) { - return await (await user.GetOrCreateDMChannelAsync().ConfigureAwait(false)).SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); + return await (await user.GetOrCreateDMChannelAsync().ConfigureAwait(false)).SendMessageAsync(text, isTTS, embed, options, allowedMentions).ConfigureAwait(false); } /// diff --git a/src/Discord.Net.Rest/API/Common/Message.cs b/src/Discord.Net.Rest/API/Common/Message.cs index f20035685..b4529d457 100644 --- a/src/Discord.Net.Rest/API/Common/Message.cs +++ b/src/Discord.Net.Rest/API/Common/Message.cs @@ -54,5 +54,7 @@ namespace Discord.API public Optional Reference { get; set; } [JsonProperty("flags")] public Optional Flags { get; set; } + [JsonProperty("allowed_mentions")] + public Optional AllowedMentions { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Rest/CreateMessageParams.cs b/src/Discord.Net.Rest/API/Rest/CreateMessageParams.cs index d77bff8ca..4b56658d6 100644 --- a/src/Discord.Net.Rest/API/Rest/CreateMessageParams.cs +++ b/src/Discord.Net.Rest/API/Rest/CreateMessageParams.cs @@ -1,4 +1,4 @@ -#pragma warning disable CS1591 +#pragma warning disable CS1591 using Newtonsoft.Json; namespace Discord.API.Rest @@ -15,6 +15,8 @@ namespace Discord.API.Rest public Optional IsTTS { get; set; } [JsonProperty("embed")] public Optional Embed { get; set; } + [JsonProperty("allowed_mentions")] + public Optional AllowedMentions { get; set; } public CreateMessageParams(string content) { diff --git a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs index 5fb150cda..aa90b2eee 100644 --- a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs +++ b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs @@ -167,9 +167,28 @@ namespace Discord.Rest /// Message content is too long, length must be less or equal to . public static async Task SendMessageAsync(IMessageChannel channel, BaseDiscordClient client, - string text, bool isTTS, Embed embed, RequestOptions options) + string text, bool isTTS, Embed embed, AllowedMentions allowedMentions, RequestOptions options) { - var args = new CreateMessageParams(text) { IsTTS = isTTS, Embed = embed?.ToModel() }; + Preconditions.AtMost(allowedMentions?.RoleIds?.Count ?? 0, 100, nameof(allowedMentions.RoleIds), "A max of 100 role Ids are allowed."); + Preconditions.AtMost(allowedMentions?.UserIds?.Count ?? 0, 100, nameof(allowedMentions.UserIds), "A max of 100 user Ids are allowed."); + + // check that user flag and user Id list are exclusive, same with role flag and role Id list + if (allowedMentions != null && allowedMentions.AllowedTypes.HasValue) + { + if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Users) && + allowedMentions.UserIds != null && allowedMentions.UserIds.Count > 0) + { + throw new ArgumentException("The Users flag is mutually exclusive with the list of User Ids.", nameof(allowedMentions)); + } + + if (allowedMentions.AllowedTypes.Value.HasFlag(AllowedMentionTypes.Roles) && + allowedMentions.RoleIds != null && allowedMentions.RoleIds.Count > 0) + { + throw new ArgumentException("The Roles flag is mutually exclusive with the list of Role Ids.", nameof(allowedMentions)); + } + } + + var args = new CreateMessageParams(text) { IsTTS = isTTS, Embed = embed?.ToModel(), AllowedMentions = allowedMentions?.ToModel() }; var model = await client.ApiClient.CreateMessageAsync(channel.Id, args, options).ConfigureAwait(false); return RestUserMessage.Create(client, channel, client.CurrentUser, model); } diff --git a/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs b/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs index a28170eed..195fa92df 100644 --- a/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs @@ -20,11 +20,15 @@ namespace Discord.Rest /// Determines whether the message should be read aloud by Discord or not. /// The to be sent. /// The options to be used when sending the request. + /// + /// Specifies if notifications are sent for mentioned users and roles in the message . + /// If null, all mentioned roles and users will be notified. + /// /// /// A task that represents an asynchronous send operation for delivering the message. The task result /// contains the sent message. /// - new Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null); + new Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null); /// /// Sends a file to this message channel with an optional caption. /// diff --git a/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs index 446410b70..732af2d81 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs @@ -93,8 +93,8 @@ namespace Discord.Rest /// /// Message content is too long, length must be less or equal to . - public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null) - => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, options); /// /// @@ -206,8 +206,8 @@ namespace Discord.Rest async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler) => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false); /// - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) - => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions) + => await SendMessageAsync(text, isTTS, embed, options, allowedMentions).ConfigureAwait(false); //IChannel /// diff --git a/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs index 5cfe03f15..3c21bd95f 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs @@ -95,8 +95,8 @@ namespace Discord.Rest /// /// Message content is too long, length must be less or equal to . - public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null) - => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, options); /// /// @@ -183,8 +183,9 @@ namespace Discord.Rest async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler) => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false); - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) - => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); + + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions) + => await SendMessageAsync(text, isTTS, embed, options, allowedMentions).ConfigureAwait(false); //IAudioChannel /// diff --git a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs index dc86327bd..cecd0a4d2 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs @@ -101,8 +101,8 @@ namespace Discord.Rest /// /// Message content is too long, length must be less or equal to . - public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null) - => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, options); /// /// @@ -273,8 +273,8 @@ namespace Discord.Rest async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler) => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false); /// - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) - => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions) + => await SendMessageAsync(text, isTTS, embed, options, allowedMentions).ConfigureAwait(false); //IGuildChannel /// diff --git a/src/Discord.Net.Rest/Entities/Messages/AllowedMentions.cs b/src/Discord.Net.Rest/Entities/Messages/AllowedMentions.cs new file mode 100644 index 000000000..5ab96032f --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Messages/AllowedMentions.cs @@ -0,0 +1,15 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + public class AllowedMentions + { + [JsonProperty("parse")] + public Optional Parse { get; set; } + // Roles and Users have a max size of 100 + [JsonProperty("roles")] + public Optional Roles { get; set; } + [JsonProperty("users")] + public Optional Users { get; set; } + } +} diff --git a/src/Discord.Net.Rest/Extensions/EntityExtensions.cs b/src/Discord.Net.Rest/Extensions/EntityExtensions.cs index e265f991f..abdfc9d4f 100644 --- a/src/Discord.Net.Rest/Extensions/EntityExtensions.cs +++ b/src/Discord.Net.Rest/Extensions/EntityExtensions.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; @@ -60,6 +61,24 @@ namespace Discord.Rest model.Video = entity.Video.Value.ToModel(); return model; } + public static API.AllowedMentions ToModel(this AllowedMentions entity) + { + return new API.AllowedMentions() + { + Parse = entity.AllowedTypes?.EnumerateMentionTypes().ToArray(), + Roles = entity.RoleIds?.ToArray(), + Users = entity.UserIds?.ToArray(), + }; + } + public static IEnumerable EnumerateMentionTypes(this AllowedMentionTypes mentionTypes) + { + if (mentionTypes.HasFlag(AllowedMentionTypes.Everyone)) + yield return "everyone"; + if (mentionTypes.HasFlag(AllowedMentionTypes.Roles)) + yield return "roles"; + if (mentionTypes.HasFlag(AllowedMentionTypes.Users)) + yield return "users"; + } public static EmbedAuthor ToEntity(this API.EmbedAuthor model) { return new EmbedAuthor(model.Name, model.Url, model.IconUrl, model.ProxyIconUrl); diff --git a/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs index b88d0106b..378478dcc 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs @@ -29,11 +29,15 @@ namespace Discord.WebSocket /// Determines whether the message should be read aloud by Discord or not. /// The to be sent. /// The options to be used when sending the request. + /// + /// Specifies if notifications are sent for mentioned users and roles in the message . + /// If null, all mentioned roles and users will be notified. + /// /// /// A task that represents an asynchronous send operation for delivering the message. The task result /// contains the sent message. /// - new Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null); + new Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null); /// /// Sends a file to this message channel with an optional caption. /// diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs index 838fb8ef2..11259a31e 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs @@ -135,8 +135,8 @@ namespace Discord.WebSocket /// /// Message content is too long, length must be less or equal to . - public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null) - => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, options); /// public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false) @@ -235,8 +235,8 @@ namespace Discord.WebSocket async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler) => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false); /// - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) - => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions) + => await SendMessageAsync(text, isTTS, embed, options, allowedMentions).ConfigureAwait(false); //IChannel /// diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs index 26fcbe83c..c57c37db2 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs @@ -163,8 +163,8 @@ namespace Discord.WebSocket /// /// Message content is too long, length must be less or equal to . - public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null) - => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, options); /// public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false) @@ -299,8 +299,8 @@ namespace Discord.WebSocket async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler) => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false); /// - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) - => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions) + => await SendMessageAsync(text, isTTS, embed, options, allowedMentions).ConfigureAwait(false); //IAudioChannel /// diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs index ca7ca11dc..1b3b5bcd7 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs @@ -161,8 +161,8 @@ namespace Discord.WebSocket /// /// Message content is too long, length must be less or equal to . - public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null) - => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, allowedMentions, options); /// public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null, bool isSpoiler = false) @@ -308,8 +308,8 @@ namespace Discord.WebSocket async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options, bool isSpoiler) => await SendFileAsync(stream, filename, text, isTTS, embed, options, isSpoiler).ConfigureAwait(false); /// - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) - => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options, AllowedMentions allowedMentions) + => await SendMessageAsync(text, isTTS, embed, options, allowedMentions).ConfigureAwait(false); // INestedChannel /// diff --git a/test/Discord.Net.Tests.Unit/MockedEntities/MockedDMChannel.cs b/test/Discord.Net.Tests.Unit/MockedEntities/MockedDMChannel.cs index 724bc84ef..c8d68fb4d 100644 --- a/test/Discord.Net.Tests.Unit/MockedEntities/MockedDMChannel.cs +++ b/test/Discord.Net.Tests.Unit/MockedEntities/MockedDMChannel.cs @@ -83,7 +83,7 @@ namespace Discord throw new NotImplementedException(); } - public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null) + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null) { throw new NotImplementedException(); } diff --git a/test/Discord.Net.Tests.Unit/MockedEntities/MockedGroupChannel.cs b/test/Discord.Net.Tests.Unit/MockedEntities/MockedGroupChannel.cs index 8b4e8b0d0..5a26b713f 100644 --- a/test/Discord.Net.Tests.Unit/MockedEntities/MockedGroupChannel.cs +++ b/test/Discord.Net.Tests.Unit/MockedEntities/MockedGroupChannel.cs @@ -91,7 +91,7 @@ namespace Discord throw new NotImplementedException(); } - public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null) + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null) { throw new NotImplementedException(); } diff --git a/test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs b/test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs index ca84219fd..a57c72899 100644 --- a/test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs +++ b/test/Discord.Net.Tests.Unit/MockedEntities/MockedTextChannel.cs @@ -177,7 +177,7 @@ namespace Discord throw new NotImplementedException(); } - public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null) + public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null, AllowedMentions allowedMentions = null) { throw new NotImplementedException(); } From b6c981227d5adcb3af12e402af83ed92d76c0586 Mon Sep 17 00:00:00 2001 From: Arnav Borborah Date: Thu, 7 May 2020 09:14:37 -0400 Subject: [PATCH 26/28] docs: Use `Timeout.Infinite` instead of -1 so that intent is clearer (#1443) --- samples/01_basic_ping_bot/Program.cs | 3 ++- samples/02_commands_framework/Program.cs | 3 ++- samples/03_sharded_client/Program.cs | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/samples/01_basic_ping_bot/Program.cs b/samples/01_basic_ping_bot/Program.cs index 4d6674e97..7fbe04993 100644 --- a/samples/01_basic_ping_bot/Program.cs +++ b/samples/01_basic_ping_bot/Program.cs @@ -1,4 +1,5 @@ using System; +using System.Threading; using System.Threading.Tasks; using Discord; using Discord.WebSocket; @@ -43,7 +44,7 @@ namespace _01_basic_ping_bot await _client.StartAsync(); // Block the program until it is closed. - await Task.Delay(-1); + await Task.Delay(Timeout.Infinite); } private Task LogAsync(LogMessage log) diff --git a/samples/02_commands_framework/Program.cs b/samples/02_commands_framework/Program.cs index ccbc8e165..67cb87764 100644 --- a/samples/02_commands_framework/Program.cs +++ b/samples/02_commands_framework/Program.cs @@ -1,5 +1,6 @@ using System; using System.Net.Http; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Discord; @@ -45,7 +46,7 @@ namespace _02_commands_framework // Here we initialize the logic required to register our commands. await services.GetRequiredService().InitializeAsync(); - await Task.Delay(-1); + await Task.Delay(Timeout.Infinite); } } diff --git a/samples/03_sharded_client/Program.cs b/samples/03_sharded_client/Program.cs index 7a2f99168..753f400a1 100644 --- a/samples/03_sharded_client/Program.cs +++ b/samples/03_sharded_client/Program.cs @@ -1,4 +1,5 @@ using System; +using System.Threading; using System.Threading.Tasks; using _03_sharded_client.Services; using Discord; @@ -45,7 +46,7 @@ namespace _03_sharded_client await client.LoginAsync(TokenType.Bot, Environment.GetEnvironmentVariable("token")); await client.StartAsync(); - await Task.Delay(-1); + await Task.Delay(Timeout.Infinite); } } From c68cc85895341640b6a16611b8efdc589479b93b Mon Sep 17 00:00:00 2001 From: Joe4evr Date: Thu, 7 May 2020 15:16:57 +0200 Subject: [PATCH 27/28] feature: Add cache purging methods (#1478) --- .../Extensions/CollectionExtensions.cs | 4 ++-- src/Discord.Net.WebSocket/ClientState.cs | 23 +++++++++++++++++++ .../DiscordSocketClient.cs | 12 ++++++++++ .../Entities/Guilds/SocketGuild.cs | 22 ++++++++++++++++++ 4 files changed, 59 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net.Core/Extensions/CollectionExtensions.cs b/src/Discord.Net.Core/Extensions/CollectionExtensions.cs index e5d6025c2..f6ba7624d 100644 --- a/src/Discord.Net.Core/Extensions/CollectionExtensions.cs +++ b/src/Discord.Net.Core/Extensions/CollectionExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; @@ -15,7 +15,7 @@ namespace Discord //public static IReadOnlyCollection ToReadOnlyCollection(this IReadOnlyDictionary source) // => new CollectionWrapper(source.Select(x => x.Value), () => source.Count); public static IReadOnlyCollection ToReadOnlyCollection(this IDictionary source) - => new CollectionWrapper(source.Select(x => x.Value), () => source.Count); + => new CollectionWrapper(source.Values, () => source.Count); public static IReadOnlyCollection ToReadOnlyCollection(this IEnumerable query, IReadOnlyCollection source) => new CollectionWrapper(query, () => source.Count); public static IReadOnlyCollection ToReadOnlyCollection(this IEnumerable query, Func countFunc) diff --git a/src/Discord.Net.WebSocket/ClientState.cs b/src/Discord.Net.WebSocket/ClientState.cs index dad185d66..f2e370d02 100644 --- a/src/Discord.Net.WebSocket/ClientState.cs +++ b/src/Discord.Net.WebSocket/ClientState.cs @@ -82,6 +82,20 @@ namespace Discord.WebSocket } return null; } + internal void PurgeAllChannels() + { + foreach (var guild in _guilds.Values) + guild.PurgeChannelCache(this); + + PurgeDMChannels(); + } + internal void PurgeDMChannels() + { + foreach (var channel in _dmChannels.Values) + _channels.TryRemove(channel.Id, out _); + + _dmChannels.Clear(); + } internal SocketGuild GetGuild(ulong id) { @@ -96,7 +110,11 @@ namespace Discord.WebSocket internal SocketGuild RemoveGuild(ulong id) { if (_guilds.TryRemove(id, out SocketGuild guild)) + { + guild.PurgeChannelCache(this); + guild.PurgeGuildUserCache(); return guild; + } return null; } @@ -116,5 +134,10 @@ namespace Discord.WebSocket return user; return null; } + internal void PurgeUsers() + { + foreach (var guild in _guilds.Values) + guild.PurgeGuildUserCache(); + } } } diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index 1819966b3..20c97bc98 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -306,6 +306,14 @@ namespace Discord.WebSocket /// public override SocketChannel GetChannel(ulong id) => State.GetChannel(id); + /// + /// Clears all cached channels from the client. + /// + public void PurgeChannelCache() => State.PurgeAllChannels(); + /// + /// Clears cached DM channels from the client. + /// + public void PurgeDMChannelCache() => State.PurgeDMChannels(); /// public override SocketUser GetUser(ulong id) @@ -313,6 +321,10 @@ namespace Discord.WebSocket /// public override SocketUser GetUser(string username, string discriminator) => State.Users.FirstOrDefault(x => x.Discriminator == discriminator && x.Username == username); + /// + /// Clears cached users from the client. + /// + public void PurgeUserCache() => State.PurgeUsers(); internal SocketGlobalUser GetOrCreateUser(ClientState state, Discord.API.User model) { return state.GetOrAddUser(model.Id, x => diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index da9a316eb..fb0a56c24 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -623,6 +623,13 @@ namespace Discord.WebSocket return state.RemoveChannel(id) as SocketGuildChannel; return null; } + internal void PurgeChannelCache(ClientState state) + { + foreach (var channelId in _channels) + state.RemoveChannel(channelId); + + _channels.Clear(); + } //Voice Regions /// @@ -797,6 +804,21 @@ namespace Discord.WebSocket } return null; } + internal void PurgeGuildUserCache() + { + var members = Users; + var self = CurrentUser; + _members.Clear(); + _members.TryAdd(self.Id, self); + + DownloadedMemberCount = _members.Count; + + foreach (var member in members) + { + if (member.Id != self.Id) + member.GlobalUser.RemoveRef(Discord); + } + } /// public async Task DownloadUsersAsync() From 03af8e0bb4b1b6fd43c7264064a7f6501346d657 Mon Sep 17 00:00:00 2001 From: TheKingEagle <30922258+rmsoftware-development@users.noreply.github.com> Date: Thu, 7 May 2020 09:19:15 -0400 Subject: [PATCH 28/28] fix: Call GuildAvailableAsync for dispatch(GUILD_CREATE) case (#1473) * Fix for Issue #1471 This change will allow `GuildAvailable` to fire when the client joins a new guild, as well as properly update `IsConnected`. * Removed unnecessary statement; --- src/Discord.Net.WebSocket/DiscordSocketClient.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index 20c97bc98..be7432bc3 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -640,6 +640,7 @@ namespace Discord.WebSocket if (guild != null) { await TimedInvokeAsync(_joinedGuildEvent, nameof(JoinedGuild), guild).ConfigureAwait(false); + await GuildAvailableAsync(guild).ConfigureAwait(false); } else {