From df6579260ee69045fefcc8917c86b805ed6deea3 Mon Sep 17 00:00:00 2001 From: Khionu Sybiern Date: Wed, 1 Mar 2017 07:13:38 -0500 Subject: [PATCH 001/243] Fix detection of IDependencyMap impl Not pretty, but it works. --- src/Discord.Net.Commands/Utilities/ReflectionUtils.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs b/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs index 1333b9640..b8fb1f64a 100644 --- a/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs +++ b/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs @@ -58,7 +58,7 @@ namespace Discord.Commands { if (targetType == typeof(CommandService)) arg = service; - else if (targetType == typeof(IDependencyMap)) + else if (targetType.GetTypeInfo().ImplementedInterfaces.Contains(typeof(IDependencyMap))) arg = map; else throw new InvalidOperationException($"Failed to create \"{baseType.FullName}\", dependency \"{targetType.Name}\" was not found."); From c350debdbaacb6f3ba75e38b962831954787a97e Mon Sep 17 00:00:00 2001 From: Khionu Sybiern Date: Wed, 1 Mar 2017 08:08:07 -0500 Subject: [PATCH 002/243] Better implimentation of detection --- src/Discord.Net.Commands/Utilities/ReflectionUtils.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs b/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs index b8fb1f64a..fdbc8d015 100644 --- a/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs +++ b/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs @@ -51,6 +51,8 @@ namespace Discord.Commands }; } + private static readonly TypeInfo _dependencyTypeInfo = typeof(IDependencyMap).GetTypeInfo(); + internal static object GetMember(Type targetType, IDependencyMap map, CommandService service, TypeInfo baseType) { object arg; @@ -58,7 +60,7 @@ namespace Discord.Commands { if (targetType == typeof(CommandService)) arg = service; - else if (targetType.GetTypeInfo().ImplementedInterfaces.Contains(typeof(IDependencyMap))) + else if (_dependencyTypeInfo.IsAssignableFrom(targetType.GetTypeInfo())) arg = map; else throw new InvalidOperationException($"Failed to create \"{baseType.FullName}\", dependency \"{targetType.Name}\" was not found."); From 4274900d431e1316738a9e34bac76f667a8a123a Mon Sep 17 00:00:00 2001 From: Khionu Sybiern Date: Wed, 1 Mar 2017 14:19:28 -0500 Subject: [PATCH 003/243] Implimented discussed changes --- src/Discord.Net.Commands/Dependencies/DependencyMap.cs | 4 ++++ src/Discord.Net.Commands/Utilities/ReflectionUtils.cs | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net.Commands/Dependencies/DependencyMap.cs b/src/Discord.Net.Commands/Dependencies/DependencyMap.cs index f5adf1a8c..b89ab4370 100644 --- a/src/Discord.Net.Commands/Dependencies/DependencyMap.cs +++ b/src/Discord.Net.Commands/Dependencies/DependencyMap.cs @@ -38,6 +38,8 @@ namespace Discord.Commands public void AddFactory(Func factory) where T : class { var t = typeof(T); + if (typeof(T) == typeof(IDependencyMap) || typeof(T) == typeof(CommandService)) + throw new InvalidOperationException("The dependency map cannot contain services directly added as IDependencyMap or CommandService. Only Implimentations and Derivatives are permitted"); if (map.ContainsKey(t)) throw new InvalidOperationException($"The dependency map already contains \"{t.FullName}\""); map.Add(t, factory); @@ -48,6 +50,8 @@ namespace Discord.Commands var t = typeof(T); if (map.ContainsKey(t)) return false; + if (typeof(T) == typeof(IDependencyMap) || typeof(T) == typeof(CommandService)) + throw new InvalidOperationException("The dependency map cannot contain services directly added as IDependencyMap or CommandService. Only Implimentations and Derivatives are permitted"); map.Add(t, factory); return true; } diff --git a/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs b/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs index fdbc8d015..2eaa6a882 100644 --- a/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs +++ b/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs @@ -60,7 +60,7 @@ namespace Discord.Commands { if (targetType == typeof(CommandService)) arg = service; - else if (_dependencyTypeInfo.IsAssignableFrom(targetType.GetTypeInfo())) + else if (targetType == typeof(IDependencyMap) || targetType == map.GetType()) arg = map; else throw new InvalidOperationException($"Failed to create \"{baseType.FullName}\", dependency \"{targetType.Name}\" was not found."); From ba406bb646cb66103a412cb4ecc5ac6aaffeb8e5 Mon Sep 17 00:00:00 2001 From: Khionu Sybiern Date: Thu, 2 Mar 2017 01:39:45 -0500 Subject: [PATCH 004/243] Split typechecks into their own conditions --- .../Dependencies/DependencyMap.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Discord.Net.Commands/Dependencies/DependencyMap.cs b/src/Discord.Net.Commands/Dependencies/DependencyMap.cs index b89ab4370..7fb8d33c9 100644 --- a/src/Discord.Net.Commands/Dependencies/DependencyMap.cs +++ b/src/Discord.Net.Commands/Dependencies/DependencyMap.cs @@ -38,8 +38,10 @@ namespace Discord.Commands public void AddFactory(Func factory) where T : class { var t = typeof(T); - if (typeof(T) == typeof(IDependencyMap) || typeof(T) == typeof(CommandService)) - throw new InvalidOperationException("The dependency map cannot contain services directly added as IDependencyMap or CommandService. Only Implimentations and Derivatives are permitted"); + if (typeof(T) == typeof(IDependencyMap)) + throw new InvalidOperationException("IDependencyMap is used internally and cannot be added as a dependency"); + if (typeof(T) == typeof(CommandService)) + throw new InvalidOperationException("CommandService is used internally and cannot be added as a dependency"); if (map.ContainsKey(t)) throw new InvalidOperationException($"The dependency map already contains \"{t.FullName}\""); map.Add(t, factory); @@ -50,8 +52,10 @@ namespace Discord.Commands var t = typeof(T); if (map.ContainsKey(t)) return false; - if (typeof(T) == typeof(IDependencyMap) || typeof(T) == typeof(CommandService)) - throw new InvalidOperationException("The dependency map cannot contain services directly added as IDependencyMap or CommandService. Only Implimentations and Derivatives are permitted"); + if (typeof(T) == typeof(IDependencyMap)) + throw new InvalidOperationException("IDependencyMap is used internally and cannot be added as a dependency"); + if (typeof(T) == typeof(CommandService)) + throw new InvalidOperationException("CommandService is used internally and cannot be added as a dependency"); map.Add(t, factory); return true; } From 78076bd9df6ef603a95bd868080199c8286ba2df Mon Sep 17 00:00:00 2001 From: Mushroom Date: Fri, 3 Mar 2017 01:51:46 +0000 Subject: [PATCH 005/243] Added support for reaction 'me' field --- .../Entities/Messages/IUserMessage.cs | 2 +- .../Entities/Messages/ReactionMetadata.cs | 11 +++++++++++ .../Entities/Messages/RestUserMessage.cs | 2 +- .../Entities/Messages/RpcUserMessage.cs | 2 +- .../Entities/Messages/SocketUserMessage.cs | 2 +- 5 files changed, 15 insertions(+), 4 deletions(-) create mode 100644 src/Discord.Net.Core/Entities/Messages/ReactionMetadata.cs diff --git a/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs b/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs index 73d402041..f238ca6bc 100644 --- a/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs +++ b/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs @@ -14,7 +14,7 @@ namespace Discord Task UnpinAsync(RequestOptions options = null); /// Returns all reactions included in this message. - IReadOnlyDictionary Reactions { get; } + IReadOnlyDictionary Reactions { get; } /// Adds a reaction to this message. Task AddReactionAsync(Emoji emoji, RequestOptions options = null); diff --git a/src/Discord.Net.Core/Entities/Messages/ReactionMetadata.cs b/src/Discord.Net.Core/Entities/Messages/ReactionMetadata.cs new file mode 100644 index 000000000..005276202 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Messages/ReactionMetadata.cs @@ -0,0 +1,11 @@ +namespace Discord +{ + public struct ReactionMetadata + { + /// Gets the number of reactions + public int ReactionCount { get; internal set; } + + /// Returns true if the current user has used this reaction + public bool IsMe { get; internal set; } + } +} diff --git a/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs b/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs index ee806dbc1..a9197188e 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs @@ -29,7 +29,7 @@ namespace Discord.Rest public override IReadOnlyCollection MentionedRoleIds => MessageHelper.FilterTagsByKey(TagType.RoleMention, _tags); public override IReadOnlyCollection MentionedUsers => MessageHelper.FilterTagsByValue(TagType.UserMention, _tags); public override IReadOnlyCollection Tags => _tags; - public IReadOnlyDictionary Reactions => _reactions.ToDictionary(x => x.Emoji, x => x.Count); + public IReadOnlyDictionary Reactions => _reactions.ToDictionary(x => x.Emoji, x => new ReactionMetadata { ReactionCount = x.Count, IsMe = x.Me }); internal RestUserMessage(BaseDiscordClient discord, ulong id, IMessageChannel channel, IUser author) : base(discord, id, channel, author) diff --git a/src/Discord.Net.Rpc/Entities/Messages/RpcUserMessage.cs b/src/Discord.Net.Rpc/Entities/Messages/RpcUserMessage.cs index 240290fab..23d277dd9 100644 --- a/src/Discord.Net.Rpc/Entities/Messages/RpcUserMessage.cs +++ b/src/Discord.Net.Rpc/Entities/Messages/RpcUserMessage.cs @@ -29,7 +29,7 @@ namespace Discord.Rpc public override IReadOnlyCollection MentionedRoleIds => MessageHelper.FilterTagsByKey(TagType.RoleMention, _tags); public override IReadOnlyCollection MentionedUserIds => MessageHelper.FilterTagsByKey(TagType.UserMention, _tags); public override IReadOnlyCollection Tags => _tags; - public IReadOnlyDictionary Reactions => ImmutableDictionary.Create(); + public IReadOnlyDictionary Reactions => ImmutableDictionary.Create(); internal RpcUserMessage(DiscordRpcClient discord, ulong id, RestVirtualMessageChannel channel, RpcUser author) : base(discord, id, channel, author) diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs index e1a6853e2..93085234e 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs @@ -30,7 +30,7 @@ namespace Discord.WebSocket public override IReadOnlyCollection MentionedChannels => MessageHelper.FilterTagsByValue(TagType.ChannelMention, _tags); public override IReadOnlyCollection MentionedRoles => MessageHelper.FilterTagsByValue(TagType.RoleMention, _tags); public override IReadOnlyCollection MentionedUsers => MessageHelper.FilterTagsByValue(TagType.UserMention, _tags); - public IReadOnlyDictionary Reactions => _reactions.GroupBy(r => r.Emoji).ToDictionary(x => x.Key, x => x.Count()); + public IReadOnlyDictionary Reactions => _reactions.GroupBy(r => r.Emoji).ToDictionary(x => x.Key, x => new ReactionMetadata { ReactionCount = x.Count(), IsMe = x.Any(y => y.UserId == Discord.CurrentUser.Id) }); internal SocketUserMessage(DiscordSocketClient discord, ulong id, ISocketMessageChannel channel, SocketUser author) : base(discord, id, channel, author) From 4b4506f24356d3ecd585de6863d6fe99e7928033 Mon Sep 17 00:00:00 2001 From: Joe4evr Date: Tue, 7 Mar 2017 17:19:51 +0100 Subject: [PATCH 006/243] ConnectAsync -> StartAsync --- docs/guides/samples/first-steps.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/guides/samples/first-steps.cs b/docs/guides/samples/first-steps.cs index 3f1377ed7..95aacc9d3 100644 --- a/docs/guides/samples/first-steps.cs +++ b/docs/guides/samples/first-steps.cs @@ -77,7 +77,8 @@ class Program // Login and connect. await _client.LoginAsync(TokenType.Bot, /* */); - await _client.ConnectAsync(); + // Prior to rc-00608 this was ConnectAsync(); + await _client.StartAsync(); // Wait infinitely so your bot actually stays connected. await Task.Delay(-1); @@ -125,4 +126,4 @@ class Program // await msg.Channel.SendMessageAsync(result.ErrorReason); } } -} \ No newline at end of file +} From b5f80a7a6c9611c5ed5e975330a0421a47f7c218 Mon Sep 17 00:00:00 2001 From: Flamanis Date: Tue, 7 Mar 2017 18:29:53 -0600 Subject: [PATCH 007/243] Should fix Linq ArgumentNullException --- src/Discord.Net.Commands/Readers/UserTypeReader.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net.Commands/Readers/UserTypeReader.cs b/src/Discord.Net.Commands/Readers/UserTypeReader.cs index a5f92a277..cfeaf3576 100644 --- a/src/Discord.Net.Commands/Readers/UserTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/UserTypeReader.cs @@ -60,12 +60,14 @@ namespace Discord.Commands { foreach (var channelUser in channelUsers.Where(x => string.Equals(input, x.Username, StringComparison.OrdinalIgnoreCase))) AddResult(results, channelUser as T, channelUser.Username == input ? 0.65f : 0.55f); - + + if(context.Guild != null) foreach (var guildUser in guildUsers.Where(x => string.Equals(input, x.Username, StringComparison.OrdinalIgnoreCase))) AddResult(results, guildUser as T, guildUser.Username == input ? 0.60f : 0.50f); } //By Nickname (0.5-0.6) + if(context.Guild != null) { foreach (var channelUser in channelUsers.Where(x => string.Equals(input, (x as IGuildUser).Nickname, StringComparison.OrdinalIgnoreCase))) AddResult(results, channelUser as T, (channelUser as IGuildUser).Nickname == input ? 0.65f : 0.55f); From c643ceaa477e7158538b6c1c097e1372a4871215 Mon Sep 17 00:00:00 2001 From: Flamanis Date: Tue, 7 Mar 2017 18:41:31 -0600 Subject: [PATCH 008/243] Add space after if and before ( --- src/Discord.Net.Commands/Readers/UserTypeReader.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net.Commands/Readers/UserTypeReader.cs b/src/Discord.Net.Commands/Readers/UserTypeReader.cs index cfeaf3576..69c77d664 100644 --- a/src/Discord.Net.Commands/Readers/UserTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/UserTypeReader.cs @@ -61,13 +61,13 @@ namespace Discord.Commands foreach (var channelUser in channelUsers.Where(x => string.Equals(input, x.Username, StringComparison.OrdinalIgnoreCase))) AddResult(results, channelUser as T, channelUser.Username == input ? 0.65f : 0.55f); - if(context.Guild != null) + if (context.Guild != null) foreach (var guildUser in guildUsers.Where(x => string.Equals(input, x.Username, StringComparison.OrdinalIgnoreCase))) AddResult(results, guildUser as T, guildUser.Username == input ? 0.60f : 0.50f); } //By Nickname (0.5-0.6) - if(context.Guild != null) + if (context.Guild != null) { foreach (var channelUser in channelUsers.Where(x => string.Equals(input, (x as IGuildUser).Nickname, StringComparison.OrdinalIgnoreCase))) AddResult(results, channelUser as T, (channelUser as IGuildUser).Nickname == input ? 0.65f : 0.55f); From dc2230de869e5c46a3f2259066b18db7152e46ca Mon Sep 17 00:00:00 2001 From: Flamanis Date: Wed, 8 Mar 2017 00:48:18 -0600 Subject: [PATCH 009/243] guildUsers instantiated to empty collection, removed added nullchecks A null conditional operator was required at line 70 to avoid a nullref. --- src/Discord.Net.Commands/Readers/UserTypeReader.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Discord.Net.Commands/Readers/UserTypeReader.cs b/src/Discord.Net.Commands/Readers/UserTypeReader.cs index 69c77d664..5600ed27a 100644 --- a/src/Discord.Net.Commands/Readers/UserTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/UserTypeReader.cs @@ -14,7 +14,7 @@ namespace Discord.Commands { var results = new Dictionary(); IReadOnlyCollection channelUsers = (await context.Channel.GetUsersAsync(CacheMode.CacheOnly).Flatten().ConfigureAwait(false)).ToArray(); //TODO: must be a better way? - IReadOnlyCollection guildUsers = null; + IReadOnlyCollection guildUsers = ImmutableArray.Create(); ulong id; if (context.Guild != null) @@ -61,15 +61,13 @@ namespace Discord.Commands foreach (var channelUser in channelUsers.Where(x => string.Equals(input, x.Username, StringComparison.OrdinalIgnoreCase))) AddResult(results, channelUser as T, channelUser.Username == input ? 0.65f : 0.55f); - if (context.Guild != null) foreach (var guildUser in guildUsers.Where(x => string.Equals(input, x.Username, StringComparison.OrdinalIgnoreCase))) AddResult(results, guildUser as T, guildUser.Username == input ? 0.60f : 0.50f); } //By Nickname (0.5-0.6) - if (context.Guild != null) { - foreach (var channelUser in channelUsers.Where(x => string.Equals(input, (x as IGuildUser).Nickname, StringComparison.OrdinalIgnoreCase))) + foreach (var channelUser in channelUsers.Where(x => string.Equals(input, (x as IGuildUser)?.Nickname, StringComparison.OrdinalIgnoreCase))) AddResult(results, channelUser as T, (channelUser as IGuildUser).Nickname == input ? 0.65f : 0.55f); foreach (var guildUser in guildUsers.Where(x => string.Equals(input, (x as IGuildUser).Nickname, StringComparison.OrdinalIgnoreCase))) From 94ea80b45eeeceacd070360128de3fe567e5103a Mon Sep 17 00:00:00 2001 From: Flamanis Date: Wed, 8 Mar 2017 00:51:10 -0600 Subject: [PATCH 010/243] Modified User#Discrim check to properly check guild --- src/Discord.Net.Commands/Readers/UserTypeReader.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.Commands/Readers/UserTypeReader.cs b/src/Discord.Net.Commands/Readers/UserTypeReader.cs index 5600ed27a..d7fc6cfdc 100644 --- a/src/Discord.Net.Commands/Readers/UserTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/UserTypeReader.cs @@ -50,7 +50,7 @@ namespace Discord.Commands string.Equals(username, x.Username, StringComparison.OrdinalIgnoreCase)); AddResult(results, channelUser as T, channelUser?.Username == username ? 0.85f : 0.75f); - var guildUser = channelUsers.FirstOrDefault(x => x.DiscriminatorValue == discriminator && + var guildUser = guildUsers.FirstOrDefault(x => x.DiscriminatorValue == discriminator && string.Equals(username, x.Username, StringComparison.OrdinalIgnoreCase)); AddResult(results, guildUser as T, guildUser?.Username == username ? 0.80f : 0.70f); } From bb78c50b6f6b2b0b56f6f68d4844c8e8ed6b25cf Mon Sep 17 00:00:00 2001 From: Christopher F Date: Thu, 9 Mar 2017 19:25:43 -0500 Subject: [PATCH 011/243] Fix ConnectionState on DiscordSocketClient This should resolve a plethora of relates issues, including user downloading not working (#542), possibly #531 --- src/Discord.Net.WebSocket/DiscordSocketClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index 66c25e5f6..fdb8b2359 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -45,7 +45,7 @@ namespace Discord.WebSocket /// Gets the shard of of this client. public int ShardId { get; } /// Gets the current connection state of this client. - public ConnectionState ConnectionState { get; private set; } + public ConnectionState ConnectionState => _connection.State; /// Gets the estimated round-trip latency, in milliseconds, to the gateway server. public int Latency { get; private set; } internal UserStatus Status { get; private set; } = UserStatus.Online; From bea5e37db8369d6cfdb067bfe496e49534289f13 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Fri, 10 Mar 2017 20:38:55 -0500 Subject: [PATCH 012/243] Add installing guide :spoon: better docs soon come --- docs/guides/images/install-vs-deps.png | Bin 0 -> 12746 bytes docs/guides/images/install-vs-nuget.png | Bin 0 -> 119670 bytes docs/guides/installing.md | 119 ++++++++++++++++++++++++ docs/guides/samples/netstd11.cs | 9 ++ docs/guides/samples/nuget.config | 6 ++ docs/guides/toc.yml | 2 + 6 files changed, 136 insertions(+) create mode 100644 docs/guides/images/install-vs-deps.png create mode 100644 docs/guides/images/install-vs-nuget.png create mode 100644 docs/guides/installing.md create mode 100644 docs/guides/samples/netstd11.cs create mode 100644 docs/guides/samples/nuget.config diff --git a/docs/guides/images/install-vs-deps.png b/docs/guides/images/install-vs-deps.png new file mode 100644 index 0000000000000000000000000000000000000000..ab10bd1bd4ecdb145a28bdbecb0107f8154cd1dd GIT binary patch literal 12746 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 QU~Y#U=$%t33aY!y@;eK8S=y; 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 YO<4rdD+2%3V$U8O4TGyCpqs(}3m?2V>i_@% literal 0 HcmV?d00001 diff --git a/docs/guides/images/install-vs-nuget.png b/docs/guides/images/install-vs-nuget.png new file mode 100644 index 0000000000000000000000000000000000000000..64da79a9ff2c16ded006fb55e2cd89151b9a2340 GIT binary patch literal 119670 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
~p2)L2YXmDS>jXzj;9(rS^ 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 literal 0 HcmV?d00001 diff --git a/docs/guides/installing.md b/docs/guides/installing.md new file mode 100644 index 000000000..37a8b3d45 --- /dev/null +++ b/docs/guides/installing.md @@ -0,0 +1,119 @@ +--- +title: Installing Discord.Net +--- + +Discord.Net is distributed through the NuGet package manager, and it is +recommended to use NuGet to get started. + +Optionally, you may compile from source and install yourself. + +# Supported Platforms + +Currently, Discord.Net targets [.NET Standard] 1.3, and offers support for +.NET Standard 1.1. If your application will be targeting .NET Standard 1.1, +please see the [additional steps](#installing-on-.net-standard-1.1). + +Since Discord.Net is built on the .NET Standard, it is also recommended to +create applications using [.NET Core], though you are not required to. When +using .NET Framework, it is suggested to target `.NET 4.6.1` or higher. + +[.NET Standard]: https://docs.microsoft.com/en-us/dotnet/articles/standard/library +[.NET Core]: https://docs.microsoft.com/en-us/dotnet/articles/core/ + +# Installing with NuGet + +Release builds of Discord.Net 1.0 will be published to the +[official NuGet feed]. + +Development builds of Discord.Net 1.0, as well as [addons](TODO) are published +to our development [MyGet feed]. + +Direct feed link: `https://www.myget.org/F/discord-net/api/v3/index.json` + +Not sure how to add a direct feed? See how [with Visual Studio] +or [without Visual Studio](#configuring-nuget-without-visual-studio) + +[official NuGet feed]: https://nuget.org +[MyGet feed]: https://www.myget.org/feed/Packages/discord-net +[with Visual Studio]: https://docs.microsoft.com/en-us/nuget/tools/package-manager-ui#package-sources + + +## Using Visual Studio + +1. Create a solution for your bot +2. In Solution Explorer, find the 'Dependencies' element under your bot's +project +3. Right click on 'Dependencies', and select 'Manage NuGet packages' +![Step 3](images/install-vs-deps.png) +4. In the 'browse' tab, search for 'Discord.Net' +> [!TIP] +> Don't forget to change your package source if you're installing from the +> developer feed. +> Also make sure to check 'Enable Prereleases' if installing a dev build! +5. Install the 'Discord.Net' package +![Step 5](images/install-vs-nuget.png) + +## Using JetBrains Rider +**todo** + +## Using Visual Studio Code +**todo** + +# Compiling from Source + +In order to compile Discord.Net, you require the following: + +### Using Visual Studio + +- [Visual Studio 2017](https://www.visualstudio.com/) +- [.NET Core SDK 1.0](https://www.microsoft.com/net/download/core#/sdk) + +The .NET Core and Docker (Preview) workload is required during Visual Studio +installation. + +### Using Command Line + +- [.NET Core SDK 1.0](https://www.microsoft.com/net/download/core#/sdk) + +# Additional Information + +## Installing on .NET Standard 1.1 + +For applications targeting a runtime corresponding with .NET Standard 1.1 or 1.2, +the builtin WebSocket and UDP provider will not work. For applications which +utilize a WebSocket connection to Discord (WebSocket or RPC), third-party +provider packages will need to be installed and configured. + +First, install the following packages through NuGet, or compile yourself, if +you prefer: + +- Discord.Net.Providers.WS4Net +- Discord.Net.Providers.UDPClient + +Note that `Discord.Net.Providers.UDPClient` is _only_ required if your bot will +be utilizing voice chat. + +Next, you will need to configure your [DiscordSocketClient] to use these custom +providers over the default ones. + +To do this, set the `WebSocketProvider` and optionally `UdpSocketProvider` +properties on the [DiscordSocketConfig] that you are passing into your +client. + +[!code-csharp[NET Standard 1.1 Example](samples/netstd11.cs)] + +[DiscordSocketClient]: xref:Discord.WebSocket.DiscordSocketClient +[DiscordSocketConfig]: xref:Discord.WebSocket.DiscordSocketConfig + +## Configuring NuGet without Visual Studio + +If you plan on deploying your bot or developing outside of Visual Studio, you +will need to create a local NuGet configuration file for your project. + +To do this, create a file named `nuget.config` alongside the root of your +application, where the project solution is located. + +Paste the following snippets into this configuration file, adding any additional +feeds as necessary. + +[!code-xml[NuGet Configuration](samples/nuget.config)] \ No newline at end of file diff --git a/docs/guides/samples/netstd11.cs b/docs/guides/samples/netstd11.cs new file mode 100644 index 000000000..a8573696a --- /dev/null +++ b/docs/guides/samples/netstd11.cs @@ -0,0 +1,9 @@ +using Discord.Providers.WS4Net; +using Discord.Providers.UDPClient; +using Discord.WebSocket; +// ... +var client = new DiscordSocketClient(new DiscordSocketConfig +{ + WebSocketProvider = WS4NetProvider.Instance, + UdpSocketProvider = UDPClientProvider.Instance, +}); \ No newline at end of file diff --git a/docs/guides/samples/nuget.config b/docs/guides/samples/nuget.config new file mode 100644 index 000000000..bf706a08b --- /dev/null +++ b/docs/guides/samples/nuget.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/docs/guides/toc.yml b/docs/guides/toc.yml index a420e4a1c..308293d1e 100644 --- a/docs/guides/toc.yml +++ b/docs/guides/toc.yml @@ -1,4 +1,6 @@ +- name: Installing + href: installing.md - name: Getting Started href: intro.md - name: Terminology From d111214bffa09e096959af9b80655137093e8078 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Sat, 11 Mar 2017 15:32:14 -0500 Subject: [PATCH 013/243] Added new getting started guide Thanks @MinnDevelopment for his awesome work on the JDA guide that had no influence here at all. --- docs/guides/images/intro-add-bot.png | Bin 0 -> 26232 bytes docs/guides/images/intro-client-id.png | Bin 0 -> 5109 bytes docs/guides/images/intro-create-app.png | Bin 0 -> 52035 bytes docs/guides/images/intro-create-bot.png | Bin 0 -> 45743 bytes docs/guides/images/intro-token.png | Bin 0 -> 26594 bytes docs/guides/installing.md | 10 +- docs/guides/intro.md | 228 +++++++++++++++--- docs/guides/samples/intro/async-context.cs | 15 ++ docs/guides/samples/intro/client.cs | 16 ++ docs/guides/samples/intro/complete.cs | 42 ++++ docs/guides/samples/intro/logging.cs | 22 ++ docs/guides/samples/intro/message.cs | 14 ++ .../{first-steps.cs => intro/structure.cs} | 5 +- 13 files changed, 318 insertions(+), 34 deletions(-) create mode 100644 docs/guides/images/intro-add-bot.png create mode 100644 docs/guides/images/intro-client-id.png create mode 100644 docs/guides/images/intro-create-app.png create mode 100644 docs/guides/images/intro-create-bot.png create mode 100644 docs/guides/images/intro-token.png create mode 100644 docs/guides/samples/intro/async-context.cs create mode 100644 docs/guides/samples/intro/client.cs create mode 100644 docs/guides/samples/intro/complete.cs create mode 100644 docs/guides/samples/intro/logging.cs create mode 100644 docs/guides/samples/intro/message.cs rename docs/guides/samples/{first-steps.cs => intro/structure.cs} (96%) diff --git a/docs/guides/images/intro-add-bot.png b/docs/guides/images/intro-add-bot.png new file mode 100644 index 0000000000000000000000000000000000000000..e40997ed3ad0cb5d9cc51912c98f9e316f141d84 GIT binary patch literal 26232 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 DuYgQm literal 0 HcmV?d00001 diff --git a/docs/guides/images/intro-client-id.png b/docs/guides/images/intro-client-id.png new file mode 100644 index 0000000000000000000000000000000000000000..e370aa2ecbb1c2e9b98b7f7cd35fa664e1e3bc28 GIT binary patch literal 5109 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 RUQVJ2(AP21F4ue#^It;zlF|SG literal 0 HcmV?d00001 diff --git a/docs/guides/images/intro-create-app.png b/docs/guides/images/intro-create-app.png new file mode 100644 index 0000000000000000000000000000000000000000..7aceb84b41cf809f45b591c6a5a1e6f4ccbbe2b1 GIT binary patch literal 52035 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 G(EkD3K*%=$ literal 0 HcmV?d00001 diff --git a/docs/guides/images/intro-create-bot.png b/docs/guides/images/intro-create-bot.png new file mode 100644 index 0000000000000000000000000000000000000000..0522358cfcb35a17632b03394333891988fc0c0e GIT binary patch literal 45743 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�za0T3WZx8>F( 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 literal 0 HcmV?d00001 diff --git a/docs/guides/installing.md b/docs/guides/installing.md index 37a8b3d45..077d46213 100644 --- a/docs/guides/installing.md +++ b/docs/guides/installing.md @@ -11,7 +11,7 @@ Optionally, you may compile from source and install yourself. Currently, Discord.Net targets [.NET Standard] 1.3, and offers support for .NET Standard 1.1. If your application will be targeting .NET Standard 1.1, -please see the [additional steps](#installing-on-.net-standard-1.1). +please see the [additional steps](#installing-on-.net-standard-11). Since Discord.Net is built on the .NET Standard, it is also recommended to create applications using [.NET Core], though you are not required to. When @@ -46,11 +46,13 @@ project 3. Right click on 'Dependencies', and select 'Manage NuGet packages' ![Step 3](images/install-vs-deps.png) 4. In the 'browse' tab, search for 'Discord.Net' + > [!TIP] -> Don't forget to change your package source if you're installing from the -> developer feed. -> Also make sure to check 'Enable Prereleases' if installing a dev build! +Don't forget to change your package source if you're installing from the +developer feed. +Also make sure to check 'Enable Prereleases' if installing a dev build! 5. Install the 'Discord.Net' package + ![Step 5](images/install-vs-nuget.png) ## Using JetBrains Rider diff --git a/docs/guides/intro.md b/docs/guides/intro.md index f16bc9883..314f2c32e 100644 --- a/docs/guides/intro.md +++ b/docs/guides/intro.md @@ -2,49 +2,223 @@ title: Getting Started --- -# Getting Started +# Making a Ping-Pong bot -## Requirements +One of the first steps to getting started with the Discord API is to +write a basic ping-pong bot. We will expand on this to create more +diverse commands later, but for now, it is a good starting point. -Discord.Net supports logging in with all variations of Discord Accounts, however the Discord API reccomends using a `Bot Account`. +## Creating a Discord Bot -You may [register a bot account here](https://discordapp.com/developers/applications/me). +Before you can begin writing your bot, it is necessary to create a bot +account on Discord. -Bot accounts must be added to a server, you must use the [OAuth 2 Flow](https://discordapp.com/developers/docs/topics/oauth2#adding-bots-to-guilds) to add them to servers. +1. Visit the [Discord Applications Portal] +2. Create a New Application +3. Give the application a name (this will be the bot's initial +username). +4. Create the Application +![Step 4](images/intro-create-app.png) +5. In the application review page, click **Create a Bot User** +![Step 5](images/intro-create-bot.png) +6. Confirm the popup +7. If this bot will be public, check 'Public Bot'. +**Do not tick any other options!** -## Installation +[Discord Applications Portal]: https://discordapp.com/developers/applications/me -You can install Discord.Net 1.0 from our [MyGet Feed](https://www.myget.org/feed/Packages/discord-net). +## Adding your bot to a server -**For most users writing bots, install only `Discord.Net.WebSocket`.** +Bots **can not** use invite links, they must be explicitly invited +through the OAuth2 flow. -You may add the MyGet feed to Visual Studio directly from `https://www.myget.org/F/discord-net/api/v3/index.json`. +1. Open your bot's application on the [Discord Applications Portal] +2. Retrieve the app's **Client ID**. +![Step 2](images/intro-client-id.png) +3. Create an OAuth2 authorization URL +`https://discordapp.com/oauth2/authorize?client_id=&scope=bot` +4. Open the authorization URL in your browser +5. Select a server -You can also pull the latest source from [GitHub](https://github.com/RogueException/Discord.Net). +>[!NOTE] +Only servers where you have the `MANAGE_SERVER` permission will be +present in this list. + +6. Click authorize +![Step 6](images/intro-add-bot.png) + +## Connecting to Discord + +If you have not already created a project and installed Discord.Net, +do that now. (see the [Installing](installing.md) section) + +### Async + +Discord.Net uses .NET's Task-based Asynchronous Pattern ([TAP]) +extensively - nearly every operation is asynchronous. + +It is highly recommended that these operations be awaited in a +properly established async context whenever possible. Establishing an +async context can be problematic, but not hard. + +To do so, we will be creating an async main in your console +application, and rewriting the static main method to invoke the new +async main. + +[!code-csharp[Async Context](samples/intro/async-context.cs)] + +As a result of this, your program will now start, and immidiately +jump into an async context. This will allow us later on to create a +connection to Discord, without needing to worry about setting up the +correct async implementation. + +>[!TIP] +If your application throws any exceptions within an async context, +they will be thrown all the way back up to the first non-async method. +Since our first non-async method is the program's Main method, this +means that **all** unhandled exceptions will be thrown up there, which +will crash your application. Discord.Net will prevent exceptions in +event handlers from crashing your program, but any exceptions in your +async main **will** cause the application to crash. ->[!WARNING] ->The versions of Discord.Net on NuGet are behind the versions this ->documentation is written for. ->You MUST install from MyGet or Source! +### Creating a logging method -## Async +Before we create and configure a Discord client, we will add a method +to handle Discord.Net's log events. -Discord.Net uses C# tasks extensiely - nearly all operations return -one. +To allow agnostic support of as many log providers as possible, we +log information through a Log event, with a proprietary LogMessage +parameter. See the [API Documentation] for this event. -It is highly reccomended these tasks be awaited whenever possible. -To do so requires the calling method to be marked as async, which -can be problematic in a console application. An example of how to -get around this is provided below. +If you are using your own logging framework, this is where you would +invoke it. For the sake of simplicity, we will only be logging to +the Console. -For more information, go to [MSDN's Async-Await section.](https://msdn.microsoft.com/en-us/library/hh191443.aspx) +[!code-csharp[Async Context](samples/intro/logging.cs)] -## First Steps +### Creating a Discord Client -[!code-csharp[Main](samples/first-steps.cs)] +Finally, we can create a connection to Discord. Since we are writing +a bot, we will be using a [DiscordSocketClient], along with socket +entities. See the [terminology](terminology.md) if you're unsure of +the differences. + +To do so, create an instance of [DiscordSocketClient] in your async +main, passing in a configuration object only if necessary. For most +users, the default will work fine. + +Before connecting, we should hook the client's log event to the +log handler that was just created. Events in Discord.Net work +similarly to other events in C#, so hook this event the way that +you typically would. + +Next, you will need to 'login to Discord' with the `LoginAsync` method. + +You may create a variable to hold your bot's token (this can be found +on your bot's application page on the [Discord Applications Portal]). +![Token](images/intro-token.png) + +>[!IMPORTANT] +Your bot's token can be used to gain total access to your bot, so +**do __NOT__ share this token with anyone!**. It may behoove you to +store this token in an external file if you plan on distributing the +source code for your bot. + +We may now invoke the client's `StartAsync` method, which will +start connection/reconnection logic. It is important to note that +**this method returns as soon as connection logic has been started!** + +Any methods that rely on the client's state should go in an event +handler. >[!NOTE] ->In previous versions of Discord.Net, you had to hook into the `Ready` and `GuildAvailable` events to determine when your client was ready for use. ->In 1.0, the [ConnectAsync] method will automatically wait for the Ready event, and for all guilds to stream. To avoid this, pass `false` into `ConnectAsync`. +Connection logic is incomplete as of the current build. Events will +soon be added to indicate when the client's state is ready for use; +(rewrite this section when possible) + +Finally, we will want to block the async main method from returning +until after the application is exited. To do this, we can await an +infinite delay, or any other blocking method, such as reading from +the console. + +The following lines can now be added: + +[!code-csharp[Create client](samples/intro/client.cs)] + +At this point, feel free to start your program and see your bot come +online in Discord. + +>[!TIP] +Encountering a `PlatformNotSupportedException` when starting your bot? +This means that you are targeting a platform where .NET's default +WebSocket client is not supported. Refer to the [installing guide] +for how to fix this. + +[TAP]: https://docs.microsoft.com/en-us/dotnet/articles/csharp/async +[API Documentation]: xref:Discord.Rest.BaseDiscordClient#Discord_Rest_BaseDiscordClient_Log +[DiscordSocketClient]: xref:Discord.WebSocket.DiscordSocketClient +[installing guide]: installing.md#installing-on-.net-standard-11 + +### Handling a 'ping' + +Now that we have learned how to open a connection to Discord, we can +begin handling messages that users are sending. + +To start out, our bot will listen for any message where the content +is equal to `!ping`, and respond back with `Pong!`. + +Since we want to listen for new messages, the event to hook in to +is [MessageReceived]. + +In your program, add a method that matches the signature of the +MessageReceived event - it must be a method (`Func`) that returns the +type `Task`, and takes a single parameter, a [SocketMessage]. Also, +since we will be sending data to Discord in this method, we will flag +it as `async`. + +In this method, we will add an `if` block, to determine if the message +content fits the rules of our scenario - recall that it must be equal +to `!ping`. + +Inside the branch of this condition, we will want to send a message +back to the channel from which the message came - `Pong!`. To find the +channel, look for the `Channel` property on the message parameter. + +Next, we will want to send a message to this channel. Since the +channel object is of type [SocketMessageChannel], we can invoke the +`SendMessageAsync` instance method. For the message content, send back +a string containing 'Pong!'. + +You should have now added the following lines: + +[!code-csharp[Message](samples/intro/message.cs)] + +Now, your first bot is complete. You may continue to add on to this +if you desire, but for any bot that will be carrying out multiple +commands, it is strongly encouraged to use the command framework, as +shown below. + +For your reference, you may view the [completed program]. + +[MessageReceived]: xref:Discord.WebSocket.DiscordSocketClient#Discord_WebSocket_DiscordSocketClient_MessageReceived +[SocketMessage]: xref:Discord.WebSocket.SocketMessage +[SocketMessageChannel]: xref:Discord.WebSocket.SocketMessageChannel +[completed program]: samples/intro/complete.cs + +# Building a bot with commands + +This section will show you how to write a program that is ready for +[commands](commands.md). Note that this will not be explaining _how_ +to write commands or services, it will only be covering the general +structure. + +For reference, view an [annotated example] of this structure. + +[annotated example]: samples/intro/structure.cs + +It is important to know that the recommended design pattern of bots +should be to separate the program (initialization and command handler), +the modules (handle commands), and the services (persistent storage, +pure functions, data manipulation). -[ConnectAsync]: xref:Discord.WebSocket.DiscordSocketClient#Discord_WebSocket_DiscordSocketClient_ConnectAsync_System_Boolean_ \ No newline at end of file +**todo:** diagram of bot structure \ No newline at end of file diff --git a/docs/guides/samples/intro/async-context.cs b/docs/guides/samples/intro/async-context.cs new file mode 100644 index 000000000..c01ddec55 --- /dev/null +++ b/docs/guides/samples/intro/async-context.cs @@ -0,0 +1,15 @@ +using System; +using System.Threading.Tasks; + +namespace MyBot +{ + public class Program + { + public static void Main(string[] args) + => new Program().MainAsync().GetAwaiter().GetResult(); + + public async Task MainAsync() + { + } + } +} \ No newline at end of file diff --git a/docs/guides/samples/intro/client.cs b/docs/guides/samples/intro/client.cs new file mode 100644 index 000000000..ea7c91932 --- /dev/null +++ b/docs/guides/samples/intro/client.cs @@ -0,0 +1,16 @@ +// Program.cs +using Discord.WebSocket; +// ... +public async Task MainAsync() +{ + var client = new DiscordSocketClient(); + + client.Log += Log; + + string token = "abcdefg..."; // Remember to keep this private! + await client.LoginAsync(TokenType.Bot, token); + await client.StartAsync(); + + // Block this task until the program is closed. + await Task.Delay(-1); +} \ No newline at end of file diff --git a/docs/guides/samples/intro/complete.cs b/docs/guides/samples/intro/complete.cs new file mode 100644 index 000000000..b59b6b4d9 --- /dev/null +++ b/docs/guides/samples/intro/complete.cs @@ -0,0 +1,42 @@ +using Discord; +using Discord.WebSocket; +using System; +using System.Threading.Tasks; + +namespace MyBot +{ + public class Program + { + public static void Main(string[] args) + => new Program().MainAsync().GetAwaiter().GetResult(); + + public async Task MainAsync() + { + var client = new DiscordSocketClient(); + + client.Log += Log; + client.MessageReceived += MessageReceived; + + string token = "abcdefg..."; // Remember to keep this private! + await client.LoginAsync(TokenType.Bot, token); + await client.StartAsync(); + + // Block this task until the program is closed. + await Task.Delay(-1); + } + + private async Task MessageReceived(SocketMessage message) + { + if (message.Content == "!ping") + { + await message.Channel.SendMessageAsync("Pong!"); + } + } + + private Task Log(LogMessage msg) + { + Console.WriteLine(msg.ToString()); + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/docs/guides/samples/intro/logging.cs b/docs/guides/samples/intro/logging.cs new file mode 100644 index 000000000..4fb85a063 --- /dev/null +++ b/docs/guides/samples/intro/logging.cs @@ -0,0 +1,22 @@ +using Discord; +using System; +using System.Threading.Tasks; + +namespace MyBot +{ + public class Program + { + public static void Main(string[] args) + => new Program().MainAsync().GetAwaiter().GetResult(); + + public async Task MainAsync() + { + } + + private Task Log(LogMessage msg) + { + Console.WriteLine(msg.ToString()); + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/docs/guides/samples/intro/message.cs b/docs/guides/samples/intro/message.cs new file mode 100644 index 000000000..d3cda46e5 --- /dev/null +++ b/docs/guides/samples/intro/message.cs @@ -0,0 +1,14 @@ +public async Task MainAsync() +{ + // client.Log ... + client.MessageReceived += MessageReceived; + // ... +} + +private async Task MessageReceived(SocketMessage message) +{ + if (message.Content == "!ping") + { + await message.Channel.SendMessageAsync("Pong!"); + } +} \ No newline at end of file diff --git a/docs/guides/samples/first-steps.cs b/docs/guides/samples/intro/structure.cs similarity index 96% rename from docs/guides/samples/first-steps.cs rename to docs/guides/samples/intro/structure.cs index 95aacc9d3..01dff7bc6 100644 --- a/docs/guides/samples/first-steps.cs +++ b/docs/guides/samples/intro/structure.cs @@ -77,7 +77,6 @@ class Program // Login and connect. await _client.LoginAsync(TokenType.Bot, /* */); - // Prior to rc-00608 this was ConnectAsync(); await _client.StartAsync(); // Wait infinitely so your bot actually stays connected. @@ -96,10 +95,10 @@ class Program await _commands.AddModuleAsync(); // Subscribe a handler to see if a message invokes a command. - _client.MessageReceived += CmdHandler; + _client.MessageReceived += HandleCommandAsync; } - private async Task CmdHandler(SocketMessage arg) + private async Task HandleCommandAsync(SocketMessage arg) { // Bail out if it's a System Message. var msg = arg as SocketUserMessage; From 59a530fe1c8355dd59d7fc3d72693c9dbaaaf238 Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Sun, 12 Mar 2017 13:28:55 +0100 Subject: [PATCH 014/243] Add installing with nuget using VSC to guides --- docs/guides/installing.md | 15 ++++++++++++--- docs/guides/samples/project.csproj | 13 +++++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 docs/guides/samples/project.csproj diff --git a/docs/guides/installing.md b/docs/guides/installing.md index 077d46213..fa1280929 100644 --- a/docs/guides/installing.md +++ b/docs/guides/installing.md @@ -51,6 +51,7 @@ project Don't forget to change your package source if you're installing from the developer feed. Also make sure to check 'Enable Prereleases' if installing a dev build! + 5. Install the 'Discord.Net' package ![Step 5](images/install-vs-nuget.png) @@ -59,7 +60,15 @@ Also make sure to check 'Enable Prereleases' if installing a dev build! **todo** ## Using Visual Studio Code -**todo** + +1. Create a new project for your bot +2. Add Discord.Net to your .csproj + +[!code-xml[Sample .csproj](samples/project.csproj)] + +> [!TIP] +Don't forget to add the package source to a [NuGet.Config file](#configuring-nuget-without-visual-studio) if you're installing from the +developer feed. # Compiling from Source @@ -81,7 +90,7 @@ installation. ## Installing on .NET Standard 1.1 -For applications targeting a runtime corresponding with .NET Standard 1.1 or 1.2, +For applications targeting a runtime corresponding with .NET Standard 1.1 or 1.2, the builtin WebSocket and UDP provider will not work. For applications which utilize a WebSocket connection to Discord (WebSocket or RPC), third-party provider packages will need to be installed and configured. @@ -118,4 +127,4 @@ application, where the project solution is located. Paste the following snippets into this configuration file, adding any additional feeds as necessary. -[!code-xml[NuGet Configuration](samples/nuget.config)] \ No newline at end of file +[!code-xml[NuGet Configuration](samples/nuget.config)] diff --git a/docs/guides/samples/project.csproj b/docs/guides/samples/project.csproj new file mode 100644 index 000000000..8daf71877 --- /dev/null +++ b/docs/guides/samples/project.csproj @@ -0,0 +1,13 @@ + + + + Exe + netcoreapp1.1 + true + + + + + + + From 7e7df27024a7281b7ec60e49a17429681925251f Mon Sep 17 00:00:00 2001 From: RogueException Date: Mon, 13 Mar 2017 21:10:07 -0300 Subject: [PATCH 015/243] Ready event waits until guilds are downloaded --- .../DiscordSocketClient.cs | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index fdb8b2359..bdc0127c2 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -500,12 +500,21 @@ namespace Discord.WebSocket await SyncGuildsAsync().ConfigureAwait(false); _lastGuildAvailableTime = Environment.TickCount; - _guildDownloadTask = WaitForGuildsAsync(_connection.CancelToken, _gatewayLogger); - - await _readyEvent.InvokeAsync().ConfigureAwait(false); - + _guildDownloadTask = WaitForGuildsAsync(_connection.CancelToken, _gatewayLogger) + .ContinueWith(async x => + { + if (x.IsFaulted) + { + _connection.Error(x.Exception); + return; + } + else if (_connection.CancelToken.IsCancellationRequested) + return; + + await _readyEvent.InvokeAsync().ConfigureAwait(false); + await _gatewayLogger.InfoAsync("Ready").ConfigureAwait(false); + }); var _ = _connection.CompleteAsync(); - await _gatewayLogger.InfoAsync("Ready").ConfigureAwait(false); } break; case "RESUMED": @@ -1580,12 +1589,12 @@ namespace Discord.WebSocket await _gatewayLogger.ErrorAsync("Heartbeat Errored", ex).ConfigureAwait(false); } } - public async Task WaitForGuildsAsync() + /*public async Task WaitForGuildsAsync() { var downloadTask = _guildDownloadTask; if (downloadTask != null) await _guildDownloadTask.ConfigureAwait(false); - } + }*/ private async Task WaitForGuildsAsync(CancellationToken cancelToken, Logger logger) { //Wait for GUILD_AVAILABLEs From cab41851ba563f70d0d2ed46abe9f271d1ce464f Mon Sep 17 00:00:00 2001 From: RogueException Date: Mon, 13 Mar 2017 21:33:41 -0300 Subject: [PATCH 016/243] Prevent duplicate GuildUnavailables --- src/Discord.Net.Core/Utils/Cacheable.cs | 1 - .../DiscordShardedClient.Events.cs | 1 - .../DiscordSocketClient.cs | 37 ++++++++++++++----- .../Entities/Guilds/SocketGuild.cs | 9 +++-- 4 files changed, 32 insertions(+), 16 deletions(-) diff --git a/src/Discord.Net.Core/Utils/Cacheable.cs b/src/Discord.Net.Core/Utils/Cacheable.cs index 10b61be90..f17aa8699 100644 --- a/src/Discord.Net.Core/Utils/Cacheable.cs +++ b/src/Discord.Net.Core/Utils/Cacheable.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Threading.Tasks; namespace Discord diff --git a/src/Discord.Net.WebSocket/DiscordShardedClient.Events.cs b/src/Discord.Net.WebSocket/DiscordShardedClient.Events.cs index 874062c56..c52675e70 100644 --- a/src/Discord.Net.WebSocket/DiscordShardedClient.Events.cs +++ b/src/Discord.Net.WebSocket/DiscordShardedClient.Events.cs @@ -1,6 +1,5 @@ using System; using System.Threading.Tasks; -using Discord.Net; namespace Discord.WebSocket { diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index bdc0127c2..b7dd3729d 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -237,8 +237,8 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Raising virtual GuildUnavailables").ConfigureAwait(false); foreach (var guild in State.Guilds) { - if (guild._available) - await _guildUnavailableEvent.InvokeAsync(guild).ConfigureAwait(false); + if (guild.IsAvailable) + await GuildUnavailableAsync(guild).ConfigureAwait(false); } } @@ -477,10 +477,10 @@ namespace Discord.WebSocket { var model = data.Guilds[i]; var guild = AddGuild(model, state); - if (!guild._available || ApiClient.AuthTokenType == TokenType.User) + if (!guild.IsAvailable || ApiClient.AuthTokenType == TokenType.User) unavailableGuilds++; else - await _guildAvailableEvent.InvokeAsync(guild).ConfigureAwait(false); + await GuildAvailableAsync(guild).ConfigureAwait(false); } for (int i = 0; i < data.PrivateChannels.Length; i++) AddPrivateChannel(data.PrivateChannels[i], state); @@ -526,8 +526,8 @@ namespace Discord.WebSocket //Notify the client that these guilds are available again foreach (var guild in State.Guilds) { - if (guild._available) - await _guildAvailableEvent.InvokeAsync(guild).ConfigureAwait(false); + if (guild.IsAvailable) + await GuildAvailableAsync(guild).ConfigureAwait(false); } await _gatewayLogger.InfoAsync("Resumed previous session").ConfigureAwait(false); @@ -553,7 +553,7 @@ namespace Discord.WebSocket var unavailableGuilds = _unavailableGuilds; if (unavailableGuilds != 0) _unavailableGuilds = unavailableGuilds - 1; - await _guildAvailableEvent.InvokeAsync(guild).ConfigureAwait(false); + await GuildAvailableAsync(guild).ConfigureAwait(false); } else { @@ -630,7 +630,7 @@ namespace Discord.WebSocket //This is treated as an extension of GUILD_AVAILABLE _unavailableGuilds--; _lastGuildAvailableTime = Environment.TickCount; - await _guildAvailableEvent.InvokeAsync(guild).ConfigureAwait(false); + await GuildAvailableAsync(guild).ConfigureAwait(false); await _guildUpdatedEvent.InvokeAsync(before, guild).ConfigureAwait(false); } else @@ -651,7 +651,7 @@ namespace Discord.WebSocket var guild = State.GetGuild(data.Id); if (guild != null) { - await _guildUnavailableEvent.InvokeAsync(guild).ConfigureAwait(false); + await GuildUnavailableAsync(guild).ConfigureAwait(false); _unavailableGuilds++; } else @@ -668,7 +668,7 @@ namespace Discord.WebSocket var guild = RemoveGuild(data.Id); if (guild != null) { - await _guildUnavailableEvent.InvokeAsync(guild).ConfigureAwait(false); + await GuildUnavailableAsync(guild).ConfigureAwait(false); await _leftGuildEvent.InvokeAsync(guild).ConfigureAwait(false); } else @@ -1659,6 +1659,23 @@ namespace Discord.WebSocket return channel; } + private async Task GuildAvailableAsync(SocketGuild guild) + { + if (!guild.IsConnected) + { + guild.IsConnected = true; + await _guildAvailableEvent.InvokeAsync(guild).ConfigureAwait(false); + } + } + private async Task GuildUnavailableAsync(SocketGuild guild) + { + if (guild.IsConnected) + { + guild.IsConnected = false; + await _guildUnavailableEvent.InvokeAsync(guild).ConfigureAwait(false); + } + } + //IDiscordClient ConnectionState IDiscordClient.ConnectionState => _connection.State; diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index 007f52124..141d777b7 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -32,7 +32,6 @@ namespace Discord.WebSocket private ImmutableArray _emojis; private ImmutableArray _features; private AudioClient _audioClient; - internal bool _available; public string Name { get; private set; } public int AFKTimeout { get; private set; } @@ -40,8 +39,10 @@ namespace Discord.WebSocket public VerificationLevel VerificationLevel { get; private set; } public MfaLevel MfaLevel { get; private set; } public DefaultMessageNotifications DefaultMessageNotifications { get; private set; } - public int MemberCount { get; set; } + public int MemberCount { get; internal set; } public int DownloadedMemberCount { get; private set; } + internal bool IsAvailable { get; private set; } + public bool IsConnected { get; internal set; } internal ulong? AFKChannelId { get; private set; } internal ulong? EmbedChannelId { get; private set; } @@ -120,8 +121,8 @@ namespace Discord.WebSocket } internal void Update(ClientState state, ExtendedModel model) { - _available = !(model.Unavailable ?? false); - if (!_available) + IsAvailable = !(model.Unavailable ?? false); + if (!IsAvailable) { if (_channels == null) _channels = new ConcurrentHashSet(); From 900b9b082e5e4fb2ff4eeffaa565286339c443b6 Mon Sep 17 00:00:00 2001 From: RogueException Date: Mon, 13 Mar 2017 22:17:35 -0300 Subject: [PATCH 017/243] Removed persistant guild list for user downloads --- .../DiscordSocketClient.cs | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index b7dd3729d..d51df219d 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -40,7 +40,6 @@ namespace Discord.WebSocket private int _nextAudioId; private DateTimeOffset? _statusSince; private RestApplication _applicationInfo; - private ConcurrentHashSet _downloadUsersFor; /// Gets the shard of of this client. public int ShardId { get; } @@ -88,7 +87,6 @@ namespace Discord.WebSocket WebSocketProvider = config.WebSocketProvider; AlwaysDownloadUsers = config.AlwaysDownloadUsers; State = new ClientState(0, 0); - _downloadUsersFor = new ConcurrentHashSet(); _heartbeatTimes = new ConcurrentQueue(); _stateLock = new SemaphoreSlim(1, 1); @@ -120,12 +118,9 @@ namespace Discord.WebSocket GuildAvailable += g => { - if (ConnectionState == ConnectionState.Connected && (AlwaysDownloadUsers || _downloadUsersFor.ContainsKey(g.Id))) + if (ConnectionState == ConnectionState.Connected && AlwaysDownloadUsers && !g.HasAllMembers) { - if (!g.HasAllMembers) - { - var _ = g.DownloadUsersAsync(); - } + var _ = g.DownloadUsersAsync(); } return Task.Delay(0); }; @@ -159,7 +154,6 @@ namespace Discord.WebSocket await StopAsync().ConfigureAwait(false); _applicationInfo = null; _voiceRegions = ImmutableDictionary.Create(); - _downloadUsersFor.Clear(); } public async Task StartAsync() @@ -192,9 +186,6 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Sending Status").ConfigureAwait(false); await SendStatusAsync().ConfigureAwait(false); - - await ProcessUserDownloadsAsync(_downloadUsersFor.Select(x => GetGuild(x)) - .Where(x => x != null).ToImmutableArray()).ConfigureAwait(false); } finally { @@ -317,9 +308,6 @@ namespace Discord.WebSocket /// Downloads the users list for the provided guilds, if they don't have a complete list. public async Task DownloadUsersAsync(IEnumerable guilds) { - foreach (var guild in guilds) - _downloadUsersFor.TryAdd(guild.Id); - if (ConnectionState == ConnectionState.Connected) { //Race condition leads to guilds being requested twice, probably okay @@ -664,7 +652,6 @@ namespace Discord.WebSocket { await _gatewayLogger.DebugAsync($"Received Dispatch (GUILD_DELETE)").ConfigureAwait(false); - _downloadUsersFor.TryRemove(data.Id); var guild = RemoveGuild(data.Id); if (guild != null) { From 92028f59e9f602a01260078a8cabf2088daabac0 Mon Sep 17 00:00:00 2001 From: RogueException Date: Mon, 13 Mar 2017 22:25:38 -0300 Subject: [PATCH 018/243] Removed SocketClient's explicit ConnectionState --- src/Discord.Net.WebSocket/DiscordSocketClient.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index d51df219d..1dcf7dd5b 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -1664,8 +1664,6 @@ namespace Discord.WebSocket } //IDiscordClient - ConnectionState IDiscordClient.ConnectionState => _connection.State; - async Task IDiscordClient.GetApplicationInfoAsync() => await GetApplicationInfoAsync().ConfigureAwait(false); From ab43f8277150a6fc231cff57132899b28b5f5f0f Mon Sep 17 00:00:00 2001 From: William Haskell Date: Tue, 14 Mar 2017 18:47:28 +0100 Subject: [PATCH 019/243] Update audio_ffmpeg.cs AudioApplication is a required argument --- docs/guides/samples/audio_ffmpeg.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guides/samples/audio_ffmpeg.cs b/docs/guides/samples/audio_ffmpeg.cs index 877050caf..716ec3d6c 100644 --- a/docs/guides/samples/audio_ffmpeg.cs +++ b/docs/guides/samples/audio_ffmpeg.cs @@ -3,7 +3,7 @@ private async Task SendAsync(IAudioClient client, string path) // Create FFmpeg using the previous example var ffmpeg = CreateStream(path); var output = ffmpeg.StandardOutput.BaseStream; - var discord = client.CreatePCMStream(1920); + var discord = client.CreatePCMStream(AudioApplication.Mixed, 1920); await output.CopyToAsync(discord); await discord.FlushAsync(); -} \ No newline at end of file +} From 22f5e8ff46eb4122e49d5ec46bd5e908c92eb4ea Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Tue, 14 Mar 2017 20:10:37 +0100 Subject: [PATCH 020/243] Fixed the example precondition attribute --- docs/guides/samples/require_owner.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/guides/samples/require_owner.cs b/docs/guides/samples/require_owner.cs index 567b3d2af..137446553 100644 --- a/docs/guides/samples/require_owner.cs +++ b/docs/guides/samples/require_owner.cs @@ -1,10 +1,14 @@ // (Note: This precondition is obsolete, it is recommended to use the RequireOwnerAttribute that is bundled with Discord.Commands) +using Discord.Commands; +using Discord.WebSocket; +using System.Threading.Tasks; + // Inherit from PreconditionAttribute public class RequireOwnerAttribute : PreconditionAttribute { // Override the CheckPermissions method - public async override Task CheckPermissions(CommandContext context, CommandInfo command, IDependencyMap map) + public async override Task CheckPermissions(ICommandContext context, CommandInfo command, IDependencyMap map) { // Get the ID of the bot's owner var ownerId = (await map.Get().GetApplicationInfoAsync()).Owner.Id; From bfc4fd686e23c89f6ade6d1bdca34a9e5692f353 Mon Sep 17 00:00:00 2001 From: james7132 Date: Wed, 15 Mar 2017 00:33:07 +0000 Subject: [PATCH 021/243] Automatic animated avatar detection --- src/Discord.Net.Core/CDN.cs | 11 +++++++++-- src/Discord.Net.Core/Entities/Users/AvatarFormat.cs | 1 + src/Discord.Net.Core/Entities/Users/IUser.cs | 2 +- src/Discord.Net.Rest/Entities/Users/RestUser.cs | 4 ++-- src/Discord.Net.Rpc/Entities/Users/RpcUser.cs | 2 +- .../Entities/Users/SocketUser.cs | 2 +- 6 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/Discord.Net.Core/CDN.cs b/src/Discord.Net.Core/CDN.cs index b7a5346ea..6a4e03212 100644 --- a/src/Discord.Net.Core/CDN.cs +++ b/src/Discord.Net.Core/CDN.cs @@ -4,8 +4,15 @@ { public static string GetApplicationIconUrl(ulong appId, string iconId) => iconId != null ? $"{DiscordConfig.CDNUrl}app-icons/{appId}/{iconId}.jpg" : null; - public static string GetUserAvatarUrl(ulong userId, string avatarId, ushort size, AvatarFormat format) - => avatarId != null ? $"{DiscordConfig.CDNUrl}avatars/{userId}/{avatarId}.{format.ToString().ToLower()}?size={size}" : null; + public static string GetUserAvatarUrl(ulong userId, string avatarId, ushort size, AvatarFormat format) { + if (avatarId == null) + return null; + var base = $"{DiscordConfig.CDNUrl}avatars/{userId}/{avatarId}"; + if (format == AvatarFormat.Auto) + return base + (avatarId.StartsWith("a_") ? "gif" : "png") + $"?size={size}"; + else + return base + format.ToString().ToLower() + $"?size={size}"; + } public static string GetGuildIconUrl(ulong guildId, string iconId) => iconId != null ? $"{DiscordConfig.CDNUrl}icons/{guildId}/{iconId}.jpg" : null; public static string GetGuildSplashUrl(ulong guildId, string splashId) diff --git a/src/Discord.Net.Core/Entities/Users/AvatarFormat.cs b/src/Discord.Net.Core/Entities/Users/AvatarFormat.cs index 29c17cede..ef9e4b375 100644 --- a/src/Discord.Net.Core/Entities/Users/AvatarFormat.cs +++ b/src/Discord.Net.Core/Entities/Users/AvatarFormat.cs @@ -2,6 +2,7 @@ { public enum AvatarFormat { + Auto, WebP, Png, Jpeg, diff --git a/src/Discord.Net.Core/Entities/Users/IUser.cs b/src/Discord.Net.Core/Entities/Users/IUser.cs index b7e807d8e..4d36295f3 100644 --- a/src/Discord.Net.Core/Entities/Users/IUser.cs +++ b/src/Discord.Net.Core/Entities/Users/IUser.cs @@ -7,7 +7,7 @@ namespace Discord /// Gets the id of this user's avatar. string AvatarId { get; } /// Gets the url to this user's avatar. - string GetAvatarUrl(AvatarFormat format = AvatarFormat.Png, ushort size = 128); + string GetAvatarUrl(AvatarFormat format = AvatarFormat.Auto, ushort size = 128); /// Gets the per-username unique id for this user. string Discriminator { get; } /// Gets the per-username unique id for this user. diff --git a/src/Discord.Net.Rest/Entities/Users/RestUser.cs b/src/Discord.Net.Rest/Entities/Users/RestUser.cs index b439fb886..c57d96c5e 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestUser.cs @@ -13,7 +13,7 @@ namespace Discord.Rest public ushort DiscriminatorValue { get; private set; } public string AvatarId { get; private set; } - public string GetAvatarUrl(AvatarFormat format = AvatarFormat.Png, ushort size = 128) => CDN.GetUserAvatarUrl(Id, AvatarId, size, format); + public string GetAvatarUrl(AvatarFormat format = AvatarFormat.Auto, ushort size = 128) => CDN.GetUserAvatarUrl(Id, AvatarId, size, format); public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); public string Discriminator => DiscriminatorValue.ToString("D4"); public string Mention => MentionUtils.MentionUser(Id); @@ -41,7 +41,7 @@ namespace Discord.Rest if (model.Username.IsSpecified) Username = model.Username.Value; } - + public virtual async Task UpdateAsync(RequestOptions options = null) { var model = await Discord.ApiClient.GetUserAsync(Id, options).ConfigureAwait(false); diff --git a/src/Discord.Net.Rpc/Entities/Users/RpcUser.cs b/src/Discord.Net.Rpc/Entities/Users/RpcUser.cs index e78aee008..6a75f07a3 100644 --- a/src/Discord.Net.Rpc/Entities/Users/RpcUser.cs +++ b/src/Discord.Net.Rpc/Entities/Users/RpcUser.cs @@ -14,7 +14,7 @@ namespace Discord.Rpc public ushort DiscriminatorValue { get; private set; } public string AvatarId { get; private set; } - public string GetAvatarUrl(AvatarFormat format = AvatarFormat.Png, ushort size = 128) => CDN.GetUserAvatarUrl(Id, AvatarId, size, format); + public string GetAvatarUrl(AvatarFormat format = AvatarFormat.Auto, ushort size = 128) => CDN.GetUserAvatarUrl(Id, AvatarId, size, format); public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); public string Discriminator => DiscriminatorValue.ToString("D4"); public string Mention => MentionUtils.MentionUser(Id); diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs index 574e79b6e..5a0c22d36 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs @@ -15,7 +15,7 @@ namespace Discord.WebSocket internal abstract SocketGlobalUser GlobalUser { get; } internal abstract SocketPresence Presence { get; set; } - public string GetAvatarUrl(AvatarFormat format = AvatarFormat.Png, ushort size = 128) => CDN.GetUserAvatarUrl(Id, AvatarId, size, format); + public string GetAvatarUrl(AvatarFormat format = AvatarFormat.Auto, ushort size = 128) => CDN.GetUserAvatarUrl(Id, AvatarId, size, format); public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); public string Discriminator => DiscriminatorValue.ToString("D4"); public string Mention => MentionUtils.MentionUser(Id); From 46b6b8c03bbdd30636790aa4626a9822c6bc395f Mon Sep 17 00:00:00 2001 From: james7132 Date: Wed, 15 Mar 2017 00:48:37 +0000 Subject: [PATCH 022/243] Review fixes --- src/Discord.Net.Core/CDN.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Discord.Net.Core/CDN.cs b/src/Discord.Net.Core/CDN.cs index 6a4e03212..40e25e186 100644 --- a/src/Discord.Net.Core/CDN.cs +++ b/src/Discord.Net.Core/CDN.cs @@ -4,14 +4,15 @@ { public static string GetApplicationIconUrl(ulong appId, string iconId) => iconId != null ? $"{DiscordConfig.CDNUrl}app-icons/{appId}/{iconId}.jpg" : null; - public static string GetUserAvatarUrl(ulong userId, string avatarId, ushort size, AvatarFormat format) { + public static string GetUserAvatarUrl(ulong userId, string avatarId, ushort size, AvatarFormat format) + { if (avatarId == null) return null; - var base = $"{DiscordConfig.CDNUrl}avatars/{userId}/{avatarId}"; + var baseUrl = $"{DiscordConfig.CDNUrl}avatars/{userId}/{avatarId}"; if (format == AvatarFormat.Auto) - return base + (avatarId.StartsWith("a_") ? "gif" : "png") + $"?size={size}"; + return baseUrl + (avatarId.StartsWith("a_") ? "gif" : "png") + $"?size={size}"; else - return base + format.ToString().ToLower() + $"?size={size}"; + return baseUrl + format.ToString().ToLower() + $"?size={size}"; } public static string GetGuildIconUrl(ulong guildId, string iconId) => iconId != null ? $"{DiscordConfig.CDNUrl}icons/{guildId}/{iconId}.jpg" : null; From 58d9fb1ed688255e6efd726652e11c588a8710dd Mon Sep 17 00:00:00 2001 From: Sentinent Date: Tue, 14 Mar 2017 21:46:14 -0700 Subject: [PATCH 023/243] Fixed GetReactionsUsersAsync returning an empty enumerable --- src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs b/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs index 4f8d52263..2147cc20f 100644 --- a/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs +++ b/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs @@ -53,7 +53,7 @@ namespace Discord.Rest { var args = new GetReactionUsersParams(); func(args); - return (await client.ApiClient.GetReactionUsersAsync(msg.Channel.Id, msg.Id, emoji, args, options).ConfigureAwait(false)).Select(u => u as IUser).Where(u => u != null).ToImmutableArray(); + return (await client.ApiClient.GetReactionUsersAsync(msg.Channel.Id, msg.Id, emoji, args, options).ConfigureAwait(false)).Select(u => RestUser.Create(client, u)).ToImmutableArray(); } public static async Task PinAsync(IMessage msg, BaseDiscordClient client, From 3bd920ce66d6f7ff24bdf6e7077ea24499ddb9c5 Mon Sep 17 00:00:00 2001 From: RogueException Date: Mon, 13 Mar 2017 23:19:54 -0300 Subject: [PATCH 024/243] Merged UserPresenceUpdated into UserUpdated --- .../DiscordShardedClient.cs | 2 +- .../DiscordSocketClient.Events.cs | 6 --- .../DiscordSocketClient.cs | 44 +++---------------- 3 files changed, 8 insertions(+), 44 deletions(-) diff --git a/src/Discord.Net.WebSocket/DiscordShardedClient.cs b/src/Discord.Net.WebSocket/DiscordShardedClient.cs index 06f83c8dc..cb1f32fab 100644 --- a/src/Discord.Net.WebSocket/DiscordShardedClient.cs +++ b/src/Discord.Net.WebSocket/DiscordShardedClient.cs @@ -297,7 +297,7 @@ namespace Discord.WebSocket client.UserBanned += (user, guild) => _userBannedEvent.InvokeAsync(user, guild); client.UserUnbanned += (user, guild) => _userUnbannedEvent.InvokeAsync(user, guild); client.UserUpdated += (oldUser, newUser) => _userUpdatedEvent.InvokeAsync(oldUser, newUser); - client.UserPresenceUpdated += (guild, user, oldPresence, newPresence) => _userPresenceUpdatedEvent.InvokeAsync(guild, user, oldPresence, newPresence); + client.GuildMemberUpdated += (oldUser, newUser) => _guildMemberUpdatedEvent.InvokeAsync(oldUser, newUser); client.UserVoiceStateUpdated += (user, oldVoiceState, newVoiceState) => _userVoiceStateUpdatedEvent.InvokeAsync(user, oldVoiceState, newVoiceState); client.CurrentUserUpdated += (oldUser, newUser) => _selfUpdatedEvent.InvokeAsync(oldUser, newUser); client.UserIsTyping += (oldUser, newUser) => _userIsTypingEvent.InvokeAsync(oldUser, newUser); diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.Events.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.Events.cs index 313e661f3..fb155e535 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.Events.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.Events.cs @@ -185,12 +185,6 @@ namespace Discord.WebSocket remove { _guildMemberUpdatedEvent.Remove(value); } } private readonly AsyncEvent> _guildMemberUpdatedEvent = new AsyncEvent>(); - public event Func, SocketUser, SocketPresence, SocketPresence, Task> UserPresenceUpdated - { - add { _userPresenceUpdatedEvent.Add(value); } - remove { _userPresenceUpdatedEvent.Remove(value); } - } - private readonly AsyncEvent, SocketUser, SocketPresence, SocketPresence, Task>> _userPresenceUpdatedEvent = new AsyncEvent, SocketUser, SocketPresence, SocketPresence, Task>>(); public event Func UserVoiceStateUpdated { add { _userVoiceStateUpdatedEvent.Add(value); } diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index 1dcf7dd5b..71ba813d5 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -1318,46 +1318,16 @@ namespace Discord.WebSocket return; } - SocketPresence beforePresence; - SocketGlobalUser beforeGlobal; var user = guild.GetUser(data.User.Id); - if (user != null) - { - beforePresence = user.Presence.Clone(); - beforeGlobal = user.GlobalUser.Clone(); - user.Update(State, data); - } - else - { - beforePresence = new SocketPresence(UserStatus.Offline, null); - user = guild.AddOrUpdateUser(data); - beforeGlobal = user.GlobalUser.Clone(); - } - - if (data.User.Username.IsSpecified || data.User.Avatar.IsSpecified) - { - await _userUpdatedEvent.InvokeAsync(beforeGlobal, user).ConfigureAwait(false); - return; - } - await _userPresenceUpdatedEvent.InvokeAsync(guild, user, beforePresence, user.Presence).ConfigureAwait(false); + if (user == null) + guild.AddOrUpdateUser(data); } - else - { - var channel = State.GetChannel(data.User.Id); - if (channel != null) - { - var user = channel.GetUser(data.User.Id); - var beforePresence = user.Presence.Clone(); - var before = user.GlobalUser.Clone(); - user.Update(State, data); - await _userPresenceUpdatedEvent.InvokeAsync(Optional.Create(), user, beforePresence, user.Presence).ConfigureAwait(false); - if (data.User.Username.IsSpecified || data.User.Avatar.IsSpecified) - { - await _userUpdatedEvent.InvokeAsync(before, user).ConfigureAwait(false); - } - } - } + var globalUser = State.GetUser(data.User.Id); + var before = globalUser.Clone(); + globalUser.Update(State, data); + + await _userUpdatedEvent.InvokeAsync(before, globalUser).ConfigureAwait(false); } break; case "TYPING_START": From 1d5b7a2b0182e8fd01bec68281b33dfe4bd01029 Mon Sep 17 00:00:00 2001 From: RogueException Date: Mon, 13 Mar 2017 23:51:37 -0300 Subject: [PATCH 025/243] Removed Unknown status --- src/Discord.Net.Core/Entities/Users/UserStatus.cs | 3 +-- src/Discord.Net.Rest/Entities/Users/RestUser.cs | 2 +- src/Discord.Net.Rpc/Entities/Users/RpcUser.cs | 2 +- src/Discord.Net.WebSocket/DiscordSocketClient.cs | 2 +- .../Entities/Users/SocketGlobalUser.cs | 6 ------ 5 files changed, 4 insertions(+), 11 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Users/UserStatus.cs b/src/Discord.Net.Core/Entities/Users/UserStatus.cs index d183c139d..74a52a0fa 100644 --- a/src/Discord.Net.Core/Entities/Users/UserStatus.cs +++ b/src/Discord.Net.Core/Entities/Users/UserStatus.cs @@ -2,12 +2,11 @@ { public enum UserStatus { - Unknown, + Offline, Online, Idle, AFK, DoNotDisturb, Invisible, - Offline } } diff --git a/src/Discord.Net.Rest/Entities/Users/RestUser.cs b/src/Discord.Net.Rest/Entities/Users/RestUser.cs index b439fb886..a5322e0f3 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestUser.cs @@ -18,7 +18,7 @@ namespace Discord.Rest public string Discriminator => DiscriminatorValue.ToString("D4"); public string Mention => MentionUtils.MentionUser(Id); public virtual Game? Game => null; - public virtual UserStatus Status => UserStatus.Unknown; + public virtual UserStatus Status => UserStatus.Offline; internal RestUser(BaseDiscordClient discord, ulong id) : base(discord, id) diff --git a/src/Discord.Net.Rpc/Entities/Users/RpcUser.cs b/src/Discord.Net.Rpc/Entities/Users/RpcUser.cs index e78aee008..8ddf08ab2 100644 --- a/src/Discord.Net.Rpc/Entities/Users/RpcUser.cs +++ b/src/Discord.Net.Rpc/Entities/Users/RpcUser.cs @@ -19,7 +19,7 @@ namespace Discord.Rpc public string Discriminator => DiscriminatorValue.ToString("D4"); public string Mention => MentionUtils.MentionUser(Id); public virtual Game? Game => null; - public virtual UserStatus Status => UserStatus.Unknown; + public virtual UserStatus Status => UserStatus.Offline; internal RpcUser(DiscordRpcClient discord, ulong id) : base(discord, id) diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index 71ba813d5..4c713c956 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -1323,7 +1323,7 @@ namespace Discord.WebSocket guild.AddOrUpdateUser(data); } - var globalUser = State.GetUser(data.User.Id); + var globalUser = GetOrCreateUser(State, data.User); //TODO: Memory leak, users removed from friends list will never RemoveRef. var before = globalUser.Clone(); globalUser.Update(State, data); diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs index f0b23543e..4870937a1 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs @@ -47,11 +47,5 @@ namespace Discord.WebSocket } internal new SocketGlobalUser Clone() => MemberwiseClone() as SocketGlobalUser; - - //Updates are only ever called from the gateway thread, thus threadsafe - internal override void Update(ClientState state, Model model) - { - base.Update(state, model); - } } } From 254c83ecff0c28fbf8219eea91de09e53d491722 Mon Sep 17 00:00:00 2001 From: RogueException Date: Tue, 14 Mar 2017 03:14:57 -0300 Subject: [PATCH 026/243] Started adding audio receive --- .../API/Voice/SpeakingEvent.cs | 15 +++ .../Audio/AudioClient.cs | 114 ++++++++++++++++-- src/Discord.Net.WebSocket/Audio/AudioMode.cs | 13 -- .../Audio/Streams/BufferedWriteStream.cs | 7 +- .../Audio/Streams/InputStream.cs | 21 +++- .../Audio/Streams/OpusDecodeStream.cs | 6 +- .../Audio/Streams/RTPReadStream.cs | 22 +++- .../DiscordSocketClient.cs | 35 +++--- .../DiscordSocketConfig.cs | 6 +- .../Entities/Channels/SocketVoiceChannel.cs | 8 +- .../Entities/Guilds/SocketGuild.cs | 41 ++++++- .../Entities/Users/SocketGuildUser.cs | 4 +- 12 files changed, 216 insertions(+), 76 deletions(-) create mode 100644 src/Discord.Net.WebSocket/API/Voice/SpeakingEvent.cs delete mode 100644 src/Discord.Net.WebSocket/Audio/AudioMode.cs diff --git a/src/Discord.Net.WebSocket/API/Voice/SpeakingEvent.cs b/src/Discord.Net.WebSocket/API/Voice/SpeakingEvent.cs new file mode 100644 index 000000000..0272a8f53 --- /dev/null +++ b/src/Discord.Net.WebSocket/API/Voice/SpeakingEvent.cs @@ -0,0 +1,15 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Voice +{ + internal class SpeakingEvent + { + [JsonProperty("user_id")] + public ulong UserId { get; set; } + [JsonProperty("ssrc")] + public uint Ssrc { get; set; } + [JsonProperty("speaking")] + public bool Speaking { get; set; } + } +} diff --git a/src/Discord.Net.WebSocket/Audio/AudioClient.cs b/src/Discord.Net.WebSocket/Audio/AudioClient.cs index e2586d0f3..9fbfc348e 100644 --- a/src/Discord.Net.WebSocket/Audio/AudioClient.cs +++ b/src/Discord.Net.WebSocket/Audio/AudioClient.cs @@ -17,6 +17,18 @@ namespace Discord.Audio //TODO: Add audio reconnecting internal class AudioClient : IAudioClient, IDisposable { + internal struct StreamPair + { + public AudioInStream Reader; + public AudioOutStream Writer; + + public StreamPair(AudioInStream reader, AudioOutStream writer) + { + Reader = reader; + Writer = writer; + } + } + public event Func Connected { add { _connectedEvent.Add(value); } @@ -41,6 +53,8 @@ namespace Discord.Audio private readonly ConnectionManager _connection; private readonly SemaphoreSlim _stateLock; private readonly ConcurrentQueue _heartbeatTimes; + private readonly ConcurrentDictionary _ssrcMap; + private readonly ConcurrentDictionary _streams; private Task _heartbeatTask; private long _lastMessageTime; @@ -75,6 +89,8 @@ namespace Discord.Audio _connection.Connected += () => _connectedEvent.InvokeAsync(); _connection.Disconnected += (ex, recon) => _disconnectedEvent.InvokeAsync(ex); _heartbeatTimes = new ConcurrentQueue(); + _ssrcMap = new ConcurrentDictionary(); + _streams = new ConcurrentDictionary(); _serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() }; _serializer.Error += (s, e) => @@ -166,6 +182,35 @@ namespace Discord.Audio throw new ArgumentException("Value must be 120, 240, 480, 960, 1920 or 2880", nameof(samplesPerFrame)); } + internal void CreateInputStream(ulong userId) + { + //Assume Thread-safe + if (!_streams.ContainsKey(userId)) + { + var readerStream = new InputStream(); + var writerStream = new OpusDecodeStream(new RTPReadStream(readerStream, _secretKey)); + _streams.TryAdd(userId, new StreamPair(readerStream, writerStream)); + } + } + internal AudioInStream GetInputStream(ulong id) + { + StreamPair streamPair; + if (_streams.TryGetValue(id, out streamPair)) + return streamPair.Reader; + return null; + } + internal void RemoveInputStream(ulong userId) + { + _streams.TryRemove(userId, out var ignored); + } + internal void ClearInputStreams() + { + foreach (var pair in _streams.Values) + pair.Reader.Dispose(); + _ssrcMap.Clear(); + _streams.Clear(); + } + private async Task ProcessMessageAsync(VoiceOpCode opCode, object payload) { _lastMessageTime = Environment.TickCount; @@ -219,6 +264,14 @@ namespace Discord.Audio } } break; + case VoiceOpCode.Speaking: + { + await _audioLogger.DebugAsync("Received Speaking").ConfigureAwait(false); + + var data = (payload as JToken).ToObject(_serializer); + _ssrcMap[data.Ssrc] = data.UserId; //TODO: Memory Leak: SSRCs are never cleaned up + } + break; default: await _audioLogger.WarningAsync($"Unknown OpCode ({opCode})").ConfigureAwait(false); return; @@ -234,19 +287,56 @@ namespace Discord.Audio { if (!_connection.IsCompleted) { - if (packet.Length == 70) + if (packet.Length != 70) { - string ip; - int port; - try - { - ip = Encoding.UTF8.GetString(packet, 4, 70 - 6).TrimEnd('\0'); - port = packet[69] | (packet[68] << 8); - } - catch { return; } - - await _audioLogger.DebugAsync("Received Discovery").ConfigureAwait(false); - await ApiClient.SendSelectProtocol(ip, port).ConfigureAwait(false); + await _audioLogger.DebugAsync($"Malformed Packet").ConfigureAwait(false); + return; + } + string ip; + int port; + try + { + ip = Encoding.UTF8.GetString(packet, 4, 70 - 6).TrimEnd('\0'); + port = packet[69] | (packet[68] << 8); + } + catch (Exception ex) + { + await _audioLogger.DebugAsync($"Malformed Packet", ex).ConfigureAwait(false); + return; + } + + await _audioLogger.DebugAsync("Received Discovery").ConfigureAwait(false); + await ApiClient.SendSelectProtocol(ip, port).ConfigureAwait(false); + } + else + { + uint ssrc; + ulong userId; + StreamPair pair; + + if (!RTPReadStream.TryReadSsrc(packet, 0, out ssrc)) + { + await _audioLogger.DebugAsync($"Malformed Frame").ConfigureAwait(false); + return; + } + if (!_ssrcMap.TryGetValue(ssrc, out userId)) + { + await _audioLogger.DebugAsync($"Unknown SSRC {ssrc}").ConfigureAwait(false); + return; + } + if (!_streams.TryGetValue(userId, out pair)) + { + await _audioLogger.DebugAsync($"Unknown User {userId}").ConfigureAwait(false); + return; + } + try + { + await pair.Writer.WriteAsync(packet, 0, packet.Length).ConfigureAwait(false); + await _audioLogger.DebugAsync($"Received {packet.Length} bytes from user {userId}").ConfigureAwait(false); + } + catch (Exception ex) + { + await _audioLogger.DebugAsync($"Malformed Frame", ex).ConfigureAwait(false); } } } diff --git a/src/Discord.Net.WebSocket/Audio/AudioMode.cs b/src/Discord.Net.WebSocket/Audio/AudioMode.cs deleted file mode 100644 index 7cc5a08c1..000000000 --- a/src/Discord.Net.WebSocket/Audio/AudioMode.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; - -namespace Discord.Audio -{ - [Flags] - public enum AudioMode : byte - { - Disabled = 0, - Outgoing = 1, - Incoming = 2, - Both = Outgoing | Incoming - } -} diff --git a/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs index dcd053cc1..3040da855 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs @@ -59,9 +59,6 @@ namespace Discord.Audio.Streams { return Task.Run(async () => { -#if DEBUG - uint num = 0; -#endif try { while (!_isPreloaded && !_cancelToken.IsCancellationRequested) @@ -82,7 +79,7 @@ namespace Discord.Audio.Streams _queueLock.Release(); nextTick += _ticksPerFrame; #if DEBUG - var _ = _logger.DebugAsync($"{num++}: Sent {frame.Bytes} bytes ({_queuedFrames.Count} frames buffered)"); + var _ = _logger.DebugAsync($"Sent {frame.Bytes} bytes ({_queuedFrames.Count} frames buffered)"); #endif } else @@ -93,7 +90,7 @@ namespace Discord.Audio.Streams nextTick += _ticksPerFrame; } #if DEBUG - var _ = _logger.DebugAsync($"{num++}: Buffer underrun"); + var _ = _logger.DebugAsync($"Buffer underrun"); #endif } } diff --git a/src/Discord.Net.WebSocket/Audio/Streams/InputStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/InputStream.cs index d46db128b..f10638bba 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/InputStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/InputStream.cs @@ -12,12 +12,13 @@ namespace Discord.Audio.Streams private ushort _nextSeq; private uint _nextTimestamp; private bool _hasHeader; + private bool _isDisposed; - public override bool CanRead => true; + public override bool CanRead => !_isDisposed; public override bool CanSeek => false; - public override bool CanWrite => true; + public override bool CanWrite => false; - public InputStream(byte[] secretKey) + public InputStream() { _frames = new ConcurrentQueue(); } @@ -54,10 +55,13 @@ namespace Discord.Audio.Streams { cancelToken.ThrowIfCancellationRequested(); - if (_frames.Count > 1000) + if (_frames.Count > 100) //1-2 seconds + { + _hasHeader = false; return Task.Delay(0); //Buffer overloaded - if (_hasHeader) - throw new InvalidOperationException("Received payload with an RTP header"); + } + if (!_hasHeader) + throw new InvalidOperationException("Received payload without an RTP header"); byte[] payload = new byte[count]; Buffer.BlockCopy(buffer, offset, payload, 0, count); @@ -69,5 +73,10 @@ namespace Discord.Audio.Streams _hasHeader = false; return Task.Delay(0); } + + protected override void Dispose(bool isDisposing) + { + _isDisposed = true; + } } } diff --git a/src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs index 9df553bfe..2dc5a8781 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs @@ -6,15 +6,17 @@ namespace Discord.Audio.Streams /// Converts Opus to PCM public class OpusDecodeStream : AudioOutStream { + public const int SampleRate = OpusEncodeStream.SampleRate; + private readonly AudioOutStream _next; private readonly byte[] _buffer; private readonly OpusDecoder _decoder; - public OpusDecodeStream(AudioOutStream next, int samplingRate, int channels = OpusConverter.MaxChannels, int bufferSize = 4000) + public OpusDecodeStream(AudioOutStream next, int channels = OpusConverter.MaxChannels, int bufferSize = 4000) { _next = next; _buffer = new byte[bufferSize]; - _decoder = new OpusDecoder(samplingRate, channels); + _decoder = new OpusDecoder(SampleRate, channels); } public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) diff --git a/src/Discord.Net.WebSocket/Audio/Streams/RTPReadStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/RTPReadStream.cs index 9a57612bf..b4aad9430 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/RTPReadStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/RTPReadStream.cs @@ -31,11 +31,14 @@ namespace Discord.Audio.Streams { cancelToken.ThrowIfCancellationRequested(); + if (buffer[offset + 0] != 0x80 || buffer[offset + 1] != 0x78) + return; + var payload = new byte[count - 12]; Buffer.BlockCopy(buffer, offset + 12, payload, 0, count - 12); - ushort seq = (ushort)((buffer[offset + 3] << 8) | - (buffer[offset + 2] << 0)); + ushort seq = (ushort)((buffer[offset + 2] << 8) | + (buffer[offset + 3] << 0)); uint timestamp = (uint)((buffer[offset + 4] << 24) | (buffer[offset + 5] << 16) | @@ -45,5 +48,20 @@ namespace Discord.Audio.Streams _queue.WriteHeader(seq, timestamp); await (_next ?? _queue as Stream).WriteAsync(buffer, offset, count, cancelToken).ConfigureAwait(false); } + + public static bool TryReadSsrc(byte[] buffer, int offset, out uint ssrc) + { + if (buffer.Length - offset < 12) + { + ssrc = 0; + return false; + } + + ssrc = (uint)((buffer[offset + 8] << 24) | + (buffer[offset + 9] << 16) | + (buffer[offset + 10] << 16) | + (buffer[offset + 11] << 0)); + return true; + } } } diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index 4c713c956..4f2f70321 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -1,6 +1,5 @@ using Discord.API; using Discord.API.Gateway; -using Discord.Audio; using Discord.Logging; using Discord.Net.Converters; using Discord.Net.Udp; @@ -54,7 +53,6 @@ namespace Discord.WebSocket internal int TotalShards { get; private set; } internal int MessageCacheSize { get; private set; } internal int LargeThreshold { get; private set; } - internal AudioMode AudioMode { get; private set; } internal ClientState State { get; private set; } internal UdpSocketProvider UdpSocketProvider { get; private set; } internal WebSocketProvider WebSocketProvider { get; private set; } @@ -82,7 +80,6 @@ namespace Discord.WebSocket TotalShards = config.TotalShards ?? 1; MessageCacheSize = config.MessageCacheSize; LargeThreshold = config.LargeThreshold; - AudioMode = config.AudioMode; UdpSocketProvider = config.UdpSocketProvider; WebSocketProvider = config.WebSocketProvider; AlwaysDownloadUsers = config.AlwaysDownloadUsers; @@ -520,7 +517,7 @@ namespace Discord.WebSocket await _gatewayLogger.InfoAsync("Resumed previous session").ConfigureAwait(false); } - return; + break; //Guilds case "GUILD_CREATE": @@ -605,7 +602,7 @@ namespace Discord.WebSocket return; } } - return; + break; case "GUILD_SYNC": { await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_SYNC)").ConfigureAwait(false); @@ -627,7 +624,7 @@ namespace Discord.WebSocket return; } } - return; + break; case "GUILD_DELETE": { var data = (payload as JToken).ToObject(_serializer); @@ -1217,8 +1214,8 @@ namespace Discord.WebSocket await _gatewayLogger.WarningAsync("MESSAGE_REACTION_ADD referenced an unknown channel.").ConfigureAwait(false); return; } - break; } + break; case "MESSAGE_REACTION_REMOVE": { await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_REACTION_REMOVE)").ConfigureAwait(false); @@ -1242,8 +1239,8 @@ namespace Discord.WebSocket await _gatewayLogger.WarningAsync("MESSAGE_REACTION_REMOVE referenced an unknown channel.").ConfigureAwait(false); return; } - break; } + break; case "MESSAGE_REACTION_REMOVE_ALL": { await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_REACTION_REMOVE_ALL)").ConfigureAwait(false); @@ -1265,8 +1262,8 @@ namespace Discord.WebSocket await _gatewayLogger.WarningAsync("MESSAGE_REACTION_REMOVE_ALL referenced an unknown channel.").ConfigureAwait(false); return; } - break; } + break; case "MESSAGE_DELETE_BULK": { await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_DELETE_BULK)").ConfigureAwait(false); @@ -1447,10 +1444,9 @@ namespace Discord.WebSocket } break; case "VOICE_SERVER_UPDATE": - await _gatewayLogger.DebugAsync("Received Dispatch (VOICE_SERVER_UPDATE)").ConfigureAwait(false); - - if (AudioMode != AudioMode.Disabled) { + await _gatewayLogger.DebugAsync("Received Dispatch (VOICE_SERVER_UPDATE)").ConfigureAwait(false); + var data = (payload as JToken).ToObject(_serializer); var guild = State.GetGuild(data.GuildId); if (guild != null) @@ -1464,7 +1460,7 @@ namespace Discord.WebSocket return; } } - return; + break; //Ignored (User only) case "CHANNEL_PINS_ACK": @@ -1475,32 +1471,31 @@ namespace Discord.WebSocket break; case "GUILD_INTEGRATIONS_UPDATE": await _gatewayLogger.DebugAsync("Ignored Dispatch (GUILD_INTEGRATIONS_UPDATE)").ConfigureAwait(false); - return; + break; case "MESSAGE_ACK": await _gatewayLogger.DebugAsync("Ignored Dispatch (MESSAGE_ACK)").ConfigureAwait(false); - return; + break; case "USER_SETTINGS_UPDATE": await _gatewayLogger.DebugAsync("Ignored Dispatch (USER_SETTINGS_UPDATE)").ConfigureAwait(false); - return; + break; case "WEBHOOKS_UPDATE": await _gatewayLogger.DebugAsync("Ignored Dispatch (WEBHOOKS_UPDATE)").ConfigureAwait(false); - return; + break; //Others default: await _gatewayLogger.WarningAsync($"Unknown Dispatch ({type})").ConfigureAwait(false); - return; + break; } break; default: await _gatewayLogger.WarningAsync($"Unknown OpCode ({opCode})").ConfigureAwait(false); - return; + break; } } catch (Exception ex) { await _gatewayLogger.ErrorAsync($"Error handling {opCode}{(type != null ? $" ({type})" : "")}", ex).ConfigureAwait(false); - return; } } diff --git a/src/Discord.Net.WebSocket/DiscordSocketConfig.cs b/src/Discord.Net.WebSocket/DiscordSocketConfig.cs index f42744c79..9ef030d72 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketConfig.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketConfig.cs @@ -1,5 +1,4 @@ -using Discord.Audio; -using Discord.Net.Udp; +using Discord.Net.Udp; using Discord.Net.WebSockets; using Discord.Rest; @@ -27,9 +26,6 @@ namespace Discord.WebSocket /// public int LargeThreshold { get; set; } = 250; - /// Gets or sets the type of audio this DiscordClient supports. - public AudioMode AudioMode { get; set; } = AudioMode.Disabled; - /// Gets or sets the provider used to generate new websocket connections. public WebSocketProvider WebSocketProvider { get; set; } /// Gets or sets the provider used to generate new udp sockets. diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs index 71017a7c8..9ec0da72a 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs @@ -42,13 +42,7 @@ namespace Discord.WebSocket public async Task ConnectAsync() { - var audioMode = Discord.AudioMode; - if (audioMode == AudioMode.Disabled) - throw new InvalidOperationException($"Audio is not enabled on this client, {nameof(DiscordSocketConfig.AudioMode)} in {nameof(DiscordSocketConfig)} must be set."); - - return await Guild.ConnectAudioAsync(Id, - (audioMode & AudioMode.Incoming) == 0, - (audioMode & AudioMode.Outgoing) == 0).ConfigureAwait(false); + return await Guild.ConnectAudioAsync(Id, false, false).ConfigureAwait(false); } public override SocketGuildUser GetUser(ulong id) diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index 141d777b7..be63f4da2 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -426,9 +426,23 @@ namespace Discord.WebSocket internal SocketVoiceState AddOrUpdateVoiceState(ClientState state, VoiceStateModel model) { var voiceChannel = state.GetChannel(model.ChannelId.Value) as SocketVoiceChannel; - var voiceState = SocketVoiceState.Create(voiceChannel, model); - _voiceStates[model.UserId] = voiceState; - return voiceState; + var before = GetVoiceState(model.UserId) ?? SocketVoiceState.Default; + var after = SocketVoiceState.Create(voiceChannel, model); + _voiceStates[model.UserId] = after; + + if (before.VoiceChannel?.Id != after.VoiceChannel?.Id) + { + if (model.UserId == CurrentUser.Id) + RepopulateAudioStreams(); + else + { + _audioClient?.RemoveInputStream(model.UserId); //User changed channels, end their stream + if (CurrentUser.VoiceChannel != null && after.VoiceChannel?.Id == CurrentUser.VoiceChannel?.Id) + _audioClient.CreateInputStream(model.UserId); + } + } + + return after; } internal SocketVoiceState? GetVoiceState(ulong id) { @@ -446,6 +460,10 @@ namespace Discord.WebSocket } //Audio + internal AudioInStream GetAudioStream(ulong userId) + { + return _audioClient?.GetInputStream(userId); + } internal async Task ConnectAudioAsync(ulong channelId, bool selfDeaf, bool selfMute) { selfDeaf = false; @@ -531,6 +549,7 @@ namespace Discord.WebSocket } }; _audioClient = audioClient; + RepopulateAudioStreams(); } _audioClient.Connected += () => { @@ -554,6 +573,22 @@ namespace Discord.WebSocket } } + internal void RepopulateAudioStreams() + { + if (_audioClient != null) + { + _audioClient.ClearInputStreams(); //We changed channels, end all current streams + if (CurrentUser.VoiceChannel != null) + { + foreach (var pair in _voiceStates) + { + if (pair.Value.VoiceChannel?.Id == CurrentUser.VoiceChannel?.Id) + _audioClient.CreateInputStream(pair.Key); + } + } + } + } + public override string ToString() => Name; private string DebuggerDisplay => $"{Name} ({Id})"; internal SocketGuild Clone() => MemberwiseClone() as SocketGuild; diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs index 5162839d7..1b2e10332 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs @@ -1,4 +1,5 @@ -using Discord.Rest; +using Discord.Audio; +using Discord.Rest; using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -37,6 +38,7 @@ namespace Discord.WebSocket public SocketVoiceChannel VoiceChannel => VoiceState?.VoiceChannel; public string VoiceSessionId => VoiceState?.VoiceSessionId ?? ""; public SocketVoiceState? VoiceState => Guild.GetVoiceState(Id); + public AudioInStream AudioStream => Guild.GetAudioStream(Id); /// The position of the user within the role hirearchy. /// The returned value equal to the position of the highest role the user has, From 2b16c8620d51711ebe64c1c9e684ebf105d5e17a Mon Sep 17 00:00:00 2001 From: RogueException Date: Wed, 15 Mar 2017 23:40:03 -0300 Subject: [PATCH 027/243] Avoid catching log errors --- src/Discord.Net.WebSocket/Audio/AudioClient.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net.WebSocket/Audio/AudioClient.cs b/src/Discord.Net.WebSocket/Audio/AudioClient.cs index 9fbfc348e..226d8eb7f 100644 --- a/src/Discord.Net.WebSocket/Audio/AudioClient.cs +++ b/src/Discord.Net.WebSocket/Audio/AudioClient.cs @@ -332,12 +332,13 @@ namespace Discord.Audio try { await pair.Writer.WriteAsync(packet, 0, packet.Length).ConfigureAwait(false); - await _audioLogger.DebugAsync($"Received {packet.Length} bytes from user {userId}").ConfigureAwait(false); } catch (Exception ex) { await _audioLogger.DebugAsync($"Malformed Frame", ex).ConfigureAwait(false); + return; } + await _audioLogger.DebugAsync($"Received {packet.Length} bytes from user {userId}").ConfigureAwait(false); } } From 587ec65e79a57579d9434c92d3623086a396e306 Mon Sep 17 00:00:00 2001 From: RogueException Date: Thu, 16 Mar 2017 05:35:01 -0300 Subject: [PATCH 028/243] Added Discord.Net.Webhook --- Discord.Net.sln | 15 ++++ src/Discord.Net.Core/AssemblyInfo.cs | 1 + src/Discord.Net.Core/TokenType.cs | 1 + .../API/Rest/CreateWebhookMessageParams.cs | 28 +++++++ .../API/Rest/UploadWebhookFileParams.cs | 41 ++++++++++ src/Discord.Net.Rest/AssemblyInfo.cs | 1 + src/Discord.Net.Rest/DiscordRestApiClient.cs | 42 +++++++++- src/Discord.Net.Webhook/AssemblyInfo.cs | 3 + .../Discord.Net.Webhook.csproj | 27 ++++++ .../DiscordWebhookClient.cs | 82 +++++++++++++++++++ 10 files changed, 239 insertions(+), 2 deletions(-) create mode 100644 src/Discord.Net.Rest/API/Rest/CreateWebhookMessageParams.cs create mode 100644 src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs create mode 100644 src/Discord.Net.Webhook/AssemblyInfo.cs create mode 100644 src/Discord.Net.Webhook/Discord.Net.Webhook.csproj create mode 100644 src/Discord.Net.Webhook/DiscordWebhookClient.cs diff --git a/Discord.Net.sln b/Discord.Net.sln index 6308b4444..1ba7549c3 100644 --- a/Discord.Net.sln +++ b/Discord.Net.sln @@ -29,6 +29,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{F66D75C0-E30 EndProject Project("{13B669BE-BB05-4DDF-9536-439F39A36129}") = "Discord.Net.Relay", "src\Discord.Net.Relay\Discord.Net.Relay.csproj", "{2705FCB3-68C9-4CEB-89CC-01F8EC80512B}" EndProject +Project("{13B669BE-BB05-4DDF-9536-439F39A36129}") = "Discord.Net.Webhook", "src\Discord.Net.Webhook\Discord.Net.Webhook.csproj", "{9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -159,6 +161,18 @@ Global {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Release|x64.Build.0 = Release|x64 {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Release|x86.ActiveCfg = Release|x86 {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Release|x86.Build.0 = Release|x86 + {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Debug|x64.ActiveCfg = Debug|x64 + {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Debug|x64.Build.0 = Debug|x64 + {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Debug|x86.ActiveCfg = Debug|x86 + {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Debug|x86.Build.0 = Debug|x86 + {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Release|Any CPU.Build.0 = Release|Any CPU + {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Release|x64.ActiveCfg = Release|x64 + {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Release|x64.Build.0 = Release|x64 + {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Release|x86.ActiveCfg = Release|x86 + {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Release|x86.Build.0 = Release|x86 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -171,5 +185,6 @@ Global {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7} = {B0657AAE-DCC5-4FBF-8E5D-1FB578CF3012} {ABC9F4B9-2452-4725-B522-754E0A02E282} = {B0657AAE-DCC5-4FBF-8E5D-1FB578CF3012} {2705FCB3-68C9-4CEB-89CC-01F8EC80512B} = {F66D75C0-E304-46E0-9C3A-294F340DB37D} + {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30} = {F66D75C0-E304-46E0-9C3A-294F340DB37D} EndGlobalSection EndGlobal diff --git a/src/Discord.Net.Core/AssemblyInfo.cs b/src/Discord.Net.Core/AssemblyInfo.cs index c75729acf..116bc3850 100644 --- a/src/Discord.Net.Core/AssemblyInfo.cs +++ b/src/Discord.Net.Core/AssemblyInfo.cs @@ -4,5 +4,6 @@ [assembly: InternalsVisibleTo("Discord.Net.Rest")] [assembly: InternalsVisibleTo("Discord.Net.Rpc")] [assembly: InternalsVisibleTo("Discord.Net.WebSocket")] +[assembly: InternalsVisibleTo("Discord.Net.Webhook")] [assembly: InternalsVisibleTo("Discord.Net.Commands")] [assembly: InternalsVisibleTo("Discord.Net.Tests")] \ No newline at end of file diff --git a/src/Discord.Net.Core/TokenType.cs b/src/Discord.Net.Core/TokenType.cs index 519f4bf0b..e19197cd6 100644 --- a/src/Discord.Net.Core/TokenType.cs +++ b/src/Discord.Net.Core/TokenType.cs @@ -5,5 +5,6 @@ User, Bearer, Bot, + Webhook } } diff --git a/src/Discord.Net.Rest/API/Rest/CreateWebhookMessageParams.cs b/src/Discord.Net.Rest/API/Rest/CreateWebhookMessageParams.cs new file mode 100644 index 000000000..970a30201 --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/CreateWebhookMessageParams.cs @@ -0,0 +1,28 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + internal class CreateWebhookMessageParams + { + [JsonProperty("content")] + public string Content { get; } + + [JsonProperty("nonce")] + public Optional Nonce { get; set; } + [JsonProperty("tts")] + public Optional IsTTS { get; set; } + [JsonProperty("embeds")] + public Optional Embeds { get; set; } + [JsonProperty("username")] + public Optional Username { get; set; } + [JsonProperty("avatar_url")] + public Optional AvatarUrl { get; set; } + + public CreateWebhookMessageParams(string content) + { + Content = content; + } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs b/src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs new file mode 100644 index 000000000..f2c34c015 --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/UploadWebhookFileParams.cs @@ -0,0 +1,41 @@ +#pragma warning disable CS1591 +using Discord.Net.Rest; +using System.Collections.Generic; +using System.IO; + +namespace Discord.API.Rest +{ + internal class UploadWebhookFileParams + { + public Stream File { get; } + + public Optional Filename { get; set; } + public Optional Content { get; set; } + public Optional Nonce { get; set; } + public Optional IsTTS { get; set; } + public Optional Username { get; set; } + public Optional AvatarUrl { get; set; } + + public UploadWebhookFileParams(Stream file) + { + File = file; + } + + public IReadOnlyDictionary ToDictionary() + { + var d = new Dictionary(); + d["file"] = new MultipartFile(File, Filename.GetValueOrDefault("unknown.dat")); + if (Content.IsSpecified) + d["content"] = Content.Value; + if (IsTTS.IsSpecified) + d["tts"] = IsTTS.Value.ToString(); + if (Nonce.IsSpecified) + d["nonce"] = Nonce.Value; + if (Username.IsSpecified) + d["username"] = Username.Value; + if (AvatarUrl.IsSpecified) + d["avatar_url"] = AvatarUrl.Value; + return d; + } + } +} diff --git a/src/Discord.Net.Rest/AssemblyInfo.cs b/src/Discord.Net.Rest/AssemblyInfo.cs index aff0626bf..a4f045ab5 100644 --- a/src/Discord.Net.Rest/AssemblyInfo.cs +++ b/src/Discord.Net.Rest/AssemblyInfo.cs @@ -2,5 +2,6 @@ [assembly: InternalsVisibleTo("Discord.Net.Rpc")] [assembly: InternalsVisibleTo("Discord.Net.WebSocket")] +[assembly: InternalsVisibleTo("Discord.Net.Webhook")] [assembly: InternalsVisibleTo("Discord.Net.Commands")] [assembly: InternalsVisibleTo("Discord.Net.Tests")] \ No newline at end of file diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index 9e9ac4611..b9c0e9fda 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -120,7 +120,8 @@ namespace Discord.API AuthTokenType = tokenType; AuthToken = token; - RestClient.SetHeader("authorization", GetPrefixedToken(AuthTokenType, AuthToken)); + if (tokenType != TokenType.Webhook) + RestClient.SetHeader("authorization", GetPrefixedToken(AuthTokenType, AuthToken)); LoginState = LoginState.LoggedIn; } @@ -438,8 +439,8 @@ namespace Discord.API } public async Task CreateMessageAsync(ulong channelId, CreateMessageParams args, RequestOptions options = null) { - Preconditions.NotEqual(channelId, 0, nameof(channelId)); Preconditions.NotNull(args, nameof(args)); + Preconditions.NotEqual(channelId, 0, nameof(channelId)); if (!args.Embed.IsSpecified || args.Embed.Value == null) Preconditions.NotNullOrEmpty(args.Content, nameof(args.Content)); @@ -450,6 +451,22 @@ namespace Discord.API var ids = new BucketIds(channelId: channelId); return await SendJsonAsync("POST", () => $"channels/{channelId}/messages", args, ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); } + public async Task CreateWebhookMessageAsync(ulong webhookId, CreateWebhookMessageParams args, RequestOptions options = null) + { + if (AuthTokenType != TokenType.Webhook) + throw new InvalidOperationException($"This operation may only be called with a {nameof(TokenType.Webhook)} token."); + + Preconditions.NotNull(args, nameof(args)); + Preconditions.NotEqual(webhookId, 0, nameof(webhookId)); + if (!args.Embeds.IsSpecified || args.Embeds.Value == null || args.Embeds.Value.Length == 0) + Preconditions.NotNullOrEmpty(args.Content, nameof(args.Content)); + + if (args.Content.Length > DiscordConfig.MaxMessageSize) + throw new ArgumentException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content)); + options = RequestOptions.CreateOrClone(options); + + await SendJsonAsync("POST", () => $"webhooks/{webhookId}/{AuthToken}", args, new BucketIds(), clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); + } public async Task UploadFileAsync(ulong channelId, UploadFileParams args, RequestOptions options = null) { Preconditions.NotNull(args, nameof(args)); @@ -469,6 +486,27 @@ namespace Discord.API var ids = new BucketIds(channelId: channelId); return await SendMultipartAsync("POST", () => $"channels/{channelId}/messages", args.ToDictionary(), ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); } + public async Task UploadWebhookFileAsync(ulong webhookId, UploadWebhookFileParams args, RequestOptions options = null) + { + if (AuthTokenType != TokenType.Webhook) + throw new InvalidOperationException($"This operation may only be called with a {nameof(TokenType.Webhook)} token."); + + Preconditions.NotNull(args, nameof(args)); + Preconditions.NotEqual(webhookId, 0, nameof(webhookId)); + options = RequestOptions.CreateOrClone(options); + + if (args.Content.GetValueOrDefault(null) == null) + args.Content = ""; + else if (args.Content.IsSpecified) + { + if (args.Content.Value == null) + 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)); + } + + await SendMultipartAsync("POST", () => $"webhooks/{webhookId}/{AuthToken}", args.ToDictionary(), new BucketIds(), clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); + } public async Task DeleteMessageAsync(ulong channelId, ulong messageId, RequestOptions options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); diff --git a/src/Discord.Net.Webhook/AssemblyInfo.cs b/src/Discord.Net.Webhook/AssemblyInfo.cs new file mode 100644 index 000000000..c6b5997b4 --- /dev/null +++ b/src/Discord.Net.Webhook/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Discord.Net.Tests")] \ No newline at end of file diff --git a/src/Discord.Net.Webhook/Discord.Net.Webhook.csproj b/src/Discord.Net.Webhook/Discord.Net.Webhook.csproj new file mode 100644 index 000000000..747586aea --- /dev/null +++ b/src/Discord.Net.Webhook/Discord.Net.Webhook.csproj @@ -0,0 +1,27 @@ + + + 1.0.0 + rc-dev + rc-$(BuildNumber) + netstandard1.1;netstandard1.3 + Discord.Net.Webhook + RogueException + A core Discord.Net library containing the Webhook client and models. + discord;discordapp + https://github.com/RogueException/Discord.Net + http://opensource.org/licenses/MIT + git + git://github.com/RogueException/Discord.Net + Discord.Webhook + true + + + + + + + $(NoWarn);CS1573;CS1591 + true + true + + \ No newline at end of file diff --git a/src/Discord.Net.Webhook/DiscordWebhookClient.cs b/src/Discord.Net.Webhook/DiscordWebhookClient.cs new file mode 100644 index 000000000..201ed8f09 --- /dev/null +++ b/src/Discord.Net.Webhook/DiscordWebhookClient.cs @@ -0,0 +1,82 @@ +using Discord.API.Rest; +using Discord.Rest; +using System; +using System.IO; +using System.Threading.Tasks; +using System.Linq; +using Discord.Logging; + +namespace Discord.Webhook +{ + public partial class DiscordWebhookClient + { + public event Func Log { add { _logEvent.Add(value); } remove { _logEvent.Remove(value); } } + internal readonly AsyncEvent> _logEvent = new AsyncEvent>(); + + private readonly ulong _webhookId; + internal readonly Logger _restLogger; + + internal API.DiscordRestApiClient ApiClient { get; } + internal LogManager LogManager { get; } + + /// Creates a new Webhook discord client. + public DiscordWebhookClient(ulong webhookId, string webhookToken) + : this(webhookId, webhookToken, new DiscordRestConfig()) { } + /// Creates a new Webhook discord client. + public DiscordWebhookClient(ulong webhookId, string webhookToken, DiscordRestConfig config) + { + _webhookId = webhookId; + + ApiClient = CreateApiClient(config); + LogManager = new LogManager(config.LogLevel); + LogManager.Message += async msg => await _logEvent.InvokeAsync(msg).ConfigureAwait(false); + + _restLogger = LogManager.CreateLogger("Rest"); + + ApiClient.RequestQueue.RateLimitTriggered += async (id, info) => + { + if (info == null) + await _restLogger.WarningAsync($"Preemptive Rate limit triggered: {id ?? "null"}").ConfigureAwait(false); + else + await _restLogger.WarningAsync($"Rate limit triggered: {id ?? "null"}").ConfigureAwait(false); + }; + ApiClient.SentRequest += async (method, endpoint, millis) => await _restLogger.VerboseAsync($"{method} {endpoint}: {millis} ms").ConfigureAwait(false); + ApiClient.LoginAsync(TokenType.Webhook, webhookToken).GetAwaiter().GetResult(); + } + private static API.DiscordRestApiClient CreateApiClient(DiscordRestConfig config) + => new API.DiscordRestApiClient(config.RestClientProvider, DiscordRestConfig.UserAgent); + + public async Task SendMessageAsync(string text, bool isTTS = false, Embed[] embeds = null, + string username = null, string avatarUrl = null, RequestOptions options = null) + { + var args = new CreateWebhookMessageParams(text) { IsTTS = isTTS }; + if (embeds != null) + args.Embeds = embeds.Select(x => x.ToModel()).ToArray(); + if (username != null) + args.Username = username; + if (avatarUrl != null) + args.AvatarUrl = username; + await ApiClient.CreateWebhookMessageAsync(_webhookId, args, options).ConfigureAwait(false); + } + +#if NETSTANDARD1_3 + public async Task SendFileAsync(string filePath, string text, bool isTTS = false, + string username = null, string avatarUrl = null, RequestOptions options = null) + { + string filename = Path.GetFileName(filePath); + using (var file = File.OpenRead(filePath)) + await SendFileAsync(file, filename, text, isTTS, username, avatarUrl, options).ConfigureAwait(false); + } +#endif + public async Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, + string username = null, string avatarUrl = null, RequestOptions options = null) + { + var args = new UploadWebhookFileParams(stream) { Filename = filename, Content = text, IsTTS = isTTS }; + if (username != null) + args.Username = username; + if (avatarUrl != null) + args.AvatarUrl = username; + await ApiClient.UploadWebhookFileAsync(_webhookId, args, options).ConfigureAwait(false); + } + } +} From bd0a329912802376ed3426bf2b914a9890d14dcb Mon Sep 17 00:00:00 2001 From: james7132 Date: Thu, 16 Mar 2017 16:06:40 +0000 Subject: [PATCH 029/243] Simplify format logic --- src/Discord.Net.Core/CDN.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Discord.Net.Core/CDN.cs b/src/Discord.Net.Core/CDN.cs index 40e25e186..c5f05c0c3 100644 --- a/src/Discord.Net.Core/CDN.cs +++ b/src/Discord.Net.Core/CDN.cs @@ -8,11 +8,9 @@ { if (avatarId == null) return null; - var baseUrl = $"{DiscordConfig.CDNUrl}avatars/{userId}/{avatarId}"; if (format == AvatarFormat.Auto) - return baseUrl + (avatarId.StartsWith("a_") ? "gif" : "png") + $"?size={size}"; - else - return baseUrl + format.ToString().ToLower() + $"?size={size}"; + format = avatarId.StartsWith("a_") ? AvatarFormat.Gif : AvatarFormat.Png; + return $"{DiscordConfig.CDNUrl}avatars/{userId}/{avatarId}.{format.ToString().ToLower()}?size={size}"; } public static string GetGuildIconUrl(ulong guildId, string iconId) => iconId != null ? $"{DiscordConfig.CDNUrl}icons/{guildId}/{iconId}.jpg" : null; From 711a4e90a4427572fb4fab0d780e00c016d088fc Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 18 Mar 2017 08:30:33 -0300 Subject: [PATCH 030/243] Renamed AvatarFormat -> ImageFormat. Cleaned up. --- src/Discord.Net.Core/CDN.cs | 25 +++++++++++++++---- .../{Users/AvatarFormat.cs => ImageFormat.cs} | 2 +- src/Discord.Net.Core/Entities/Users/IUser.cs | 2 +- .../Entities/Users/RestUser.cs | 3 ++- src/Discord.Net.Rpc/Entities/Users/RpcUser.cs | 3 ++- .../Entities/Users/SocketUser.cs | 3 ++- 6 files changed, 28 insertions(+), 10 deletions(-) rename src/Discord.Net.Core/Entities/{Users/AvatarFormat.cs => ImageFormat.cs} (78%) diff --git a/src/Discord.Net.Core/CDN.cs b/src/Discord.Net.Core/CDN.cs index c5f05c0c3..d3ade3722 100644 --- a/src/Discord.Net.Core/CDN.cs +++ b/src/Discord.Net.Core/CDN.cs @@ -1,16 +1,17 @@ -namespace Discord +using System; + +namespace Discord { public static class CDN { public static string GetApplicationIconUrl(ulong appId, string iconId) => iconId != null ? $"{DiscordConfig.CDNUrl}app-icons/{appId}/{iconId}.jpg" : null; - public static string GetUserAvatarUrl(ulong userId, string avatarId, ushort size, AvatarFormat format) + public static string GetUserAvatarUrl(ulong userId, string avatarId, ushort size, ImageFormat format) { if (avatarId == null) return null; - if (format == AvatarFormat.Auto) - format = avatarId.StartsWith("a_") ? AvatarFormat.Gif : AvatarFormat.Png; - return $"{DiscordConfig.CDNUrl}avatars/{userId}/{avatarId}.{format.ToString().ToLower()}?size={size}"; + string extension = FormatToExtension(format, avatarId); + return $"{DiscordConfig.CDNUrl}avatars/{userId}/{avatarId}.{extension}?size={size}"; } public static string GetGuildIconUrl(ulong guildId, string iconId) => iconId != null ? $"{DiscordConfig.CDNUrl}icons/{guildId}/{iconId}.jpg" : null; @@ -20,5 +21,19 @@ => iconId != null ? $"{DiscordConfig.CDNUrl}channel-icons/{channelId}/{iconId}.jpg" : null; public static string GetEmojiUrl(ulong emojiId) => $"{DiscordConfig.CDNUrl}emojis/{emojiId}.png"; + + private static string FormatToExtension(ImageFormat format, string imageId) + { + if (format == ImageFormat.Auto) + format = imageId.StartsWith("a_") ? ImageFormat.Gif : ImageFormat.Png; + switch (format) + { + case ImageFormat.Gif: return "gif"; + case ImageFormat.Jpeg: return "jpeg"; + case ImageFormat.Png: return "png"; + case ImageFormat.WebP: return "webp"; + default: throw new ArgumentException(nameof(format)); + } + } } } diff --git a/src/Discord.Net.Core/Entities/Users/AvatarFormat.cs b/src/Discord.Net.Core/Entities/ImageFormat.cs similarity index 78% rename from src/Discord.Net.Core/Entities/Users/AvatarFormat.cs rename to src/Discord.Net.Core/Entities/ImageFormat.cs index ef9e4b375..302da79c8 100644 --- a/src/Discord.Net.Core/Entities/Users/AvatarFormat.cs +++ b/src/Discord.Net.Core/Entities/ImageFormat.cs @@ -1,6 +1,6 @@ namespace Discord { - public enum AvatarFormat + public enum ImageFormat { Auto, WebP, diff --git a/src/Discord.Net.Core/Entities/Users/IUser.cs b/src/Discord.Net.Core/Entities/Users/IUser.cs index 4d36295f3..62060da22 100644 --- a/src/Discord.Net.Core/Entities/Users/IUser.cs +++ b/src/Discord.Net.Core/Entities/Users/IUser.cs @@ -7,7 +7,7 @@ namespace Discord /// Gets the id of this user's avatar. string AvatarId { get; } /// Gets the url to this user's avatar. - string GetAvatarUrl(AvatarFormat format = AvatarFormat.Auto, ushort size = 128); + string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128); /// Gets the per-username unique id for this user. string Discriminator { get; } /// Gets the per-username unique id for this user. diff --git a/src/Discord.Net.Rest/Entities/Users/RestUser.cs b/src/Discord.Net.Rest/Entities/Users/RestUser.cs index 4d7ab5d15..0acfe3ddf 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestUser.cs @@ -13,7 +13,8 @@ namespace Discord.Rest public ushort DiscriminatorValue { get; private set; } public string AvatarId { get; private set; } - public string GetAvatarUrl(AvatarFormat format = AvatarFormat.Auto, ushort size = 128) => CDN.GetUserAvatarUrl(Id, AvatarId, size, format); + public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) + => CDN.GetUserAvatarUrl(Id, AvatarId, size, format); public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); public string Discriminator => DiscriminatorValue.ToString("D4"); public string Mention => MentionUtils.MentionUser(Id); diff --git a/src/Discord.Net.Rpc/Entities/Users/RpcUser.cs b/src/Discord.Net.Rpc/Entities/Users/RpcUser.cs index 24c715491..daf299e2a 100644 --- a/src/Discord.Net.Rpc/Entities/Users/RpcUser.cs +++ b/src/Discord.Net.Rpc/Entities/Users/RpcUser.cs @@ -14,7 +14,8 @@ namespace Discord.Rpc public ushort DiscriminatorValue { get; private set; } public string AvatarId { get; private set; } - public string GetAvatarUrl(AvatarFormat format = AvatarFormat.Auto, ushort size = 128) => CDN.GetUserAvatarUrl(Id, AvatarId, size, format); + public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) + => CDN.GetUserAvatarUrl(Id, AvatarId, size, format); public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); public string Discriminator => DiscriminatorValue.ToString("D4"); public string Mention => MentionUtils.MentionUser(Id); diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs index 5a0c22d36..5c73e3b6a 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs @@ -15,7 +15,8 @@ namespace Discord.WebSocket internal abstract SocketGlobalUser GlobalUser { get; } internal abstract SocketPresence Presence { get; set; } - public string GetAvatarUrl(AvatarFormat format = AvatarFormat.Auto, ushort size = 128) => CDN.GetUserAvatarUrl(Id, AvatarId, size, format); + public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) + => CDN.GetUserAvatarUrl(Id, AvatarId, size, format); public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); public string Discriminator => DiscriminatorValue.ToString("D4"); public string Mention => MentionUtils.MentionUser(Id); From 683541ba24b75ca06599f7273add2f635c19b59d Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 18 Mar 2017 08:38:24 -0300 Subject: [PATCH 031/243] Added RequireOwner support for User tokens --- .../Preconditions/RequireOwnerAttribute.cs | 17 ++++++++++++++--- src/Discord.Net.Core/IDiscordClient.cs | 1 + src/Discord.Net.Rest/BaseDiscordClient.cs | 1 + 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs index cfedcad23..0f4e8255d 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs @@ -12,9 +12,20 @@ namespace Discord.Commands { public override async Task CheckPermissions(ICommandContext context, CommandInfo command, IDependencyMap map) { - var application = await context.Client.GetApplicationInfoAsync(); - if (context.User.Id == application.Owner.Id) return PreconditionResult.FromSuccess(); - return PreconditionResult.FromError("Command can only be run by the owner of the bot"); + switch (context.Client.TokenType) + { + case TokenType.Bot: + var application = await context.Client.GetApplicationInfoAsync(); + if (context.User.Id != application.Owner.Id) + return PreconditionResult.FromError("Command can only be run by the owner of the bot"); + return PreconditionResult.FromSuccess(); + case TokenType.User: + if (context.User.Id != context.Client.CurrentUser.Id) + return PreconditionResult.FromError("Command can only be run by the owner of the bot"); + return PreconditionResult.FromSuccess(); + default: + return PreconditionResult.FromError($"{nameof(RequireOwnerAttribute)} is not supported by this {nameof(TokenType)}."); + } } } } diff --git a/src/Discord.Net.Core/IDiscordClient.cs b/src/Discord.Net.Core/IDiscordClient.cs index 1c5ec41c1..c434ccd7b 100644 --- a/src/Discord.Net.Core/IDiscordClient.cs +++ b/src/Discord.Net.Core/IDiscordClient.cs @@ -9,6 +9,7 @@ namespace Discord { ConnectionState ConnectionState { get; } ISelfUser CurrentUser { get; } + TokenType TokenType { get; } Task StartAsync(); Task StopAsync(); diff --git a/src/Discord.Net.Rest/BaseDiscordClient.cs b/src/Discord.Net.Rest/BaseDiscordClient.cs index 80c4cb598..df4f180b2 100644 --- a/src/Discord.Net.Rest/BaseDiscordClient.cs +++ b/src/Discord.Net.Rest/BaseDiscordClient.cs @@ -26,6 +26,7 @@ namespace Discord.Rest internal LogManager LogManager { get; } public LoginState LoginState { get; private set; } public ISelfUser CurrentUser { get; protected set; } + public TokenType TokenType => ApiClient.AuthTokenType; /// Creates a new REST-only discord client. internal BaseDiscordClient(DiscordRestConfig config, API.DiscordRestApiClient client) From 11ba30c6fa36890c57a98253014eb73d00055cd7 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 18 Mar 2017 08:48:18 -0300 Subject: [PATCH 032/243] Cleaned up DepMap type checks --- .../Dependencies/DependencyMap.cs | 28 +++++++++---------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/src/Discord.Net.Commands/Dependencies/DependencyMap.cs b/src/Discord.Net.Commands/Dependencies/DependencyMap.cs index 7fb8d33c9..c24b9db91 100644 --- a/src/Discord.Net.Commands/Dependencies/DependencyMap.cs +++ b/src/Discord.Net.Commands/Dependencies/DependencyMap.cs @@ -1,10 +1,16 @@ using System; using System.Collections.Generic; +using System.Linq; namespace Discord.Commands { public class DependencyMap : IDependencyMap { + private static readonly Type[] _typeBlacklist = new[] { + typeof(IDependencyMap), + typeof(CommandService) + }; + private Dictionary> map; public static DependencyMap Empty => new DependencyMap(); @@ -37,26 +43,18 @@ namespace Discord.Commands /// public void AddFactory(Func factory) where T : class { - var t = typeof(T); - if (typeof(T) == typeof(IDependencyMap)) - throw new InvalidOperationException("IDependencyMap is used internally and cannot be added as a dependency"); - if (typeof(T) == typeof(CommandService)) - throw new InvalidOperationException("CommandService is used internally and cannot be added as a dependency"); - if (map.ContainsKey(t)) - throw new InvalidOperationException($"The dependency map already contains \"{t.FullName}\""); - map.Add(t, factory); + if (!TryAddFactory(factory)) + throw new InvalidOperationException($"The dependency map already contains \"{typeof(T).FullName}\""); } /// public bool TryAddFactory(Func factory) where T : class { - var t = typeof(T); - if (map.ContainsKey(t)) + var type = typeof(T); + if (_typeBlacklist.Contains(type)) + throw new InvalidOperationException($"{type.FullName} is used internally and cannot be added as a dependency"); + if (map.ContainsKey(type)) return false; - if (typeof(T) == typeof(IDependencyMap)) - throw new InvalidOperationException("IDependencyMap is used internally and cannot be added as a dependency"); - if (typeof(T) == typeof(CommandService)) - throw new InvalidOperationException("CommandService is used internally and cannot be added as a dependency"); - map.Add(t, factory); + map.Add(type, factory); return true; } From efbd3cb6811079abd87d5ec9e0876ed68a768c0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Brawa=C5=84ski?= Date: Sat, 18 Mar 2017 12:54:49 +0100 Subject: [PATCH 033/243] GuildUserExtensions removed in favour of atomic role add/remove endpoints (#540) * Removed GuildUserExtensions and moved the methods to IGuildUser and implementations * Made changes per fox's suggestion: Change->Modify. New Modify overload. * Oops * Per Volt: reimplemented new endpoints * Fixing broken docstrings * I forgot that docstrings are XML * Implemented atomic add/remove role endpoints * Removed so people aren't irked * Added single-item role add/remove methods --- .../Entities/Users/GuildUserProperties.cs | 8 +++--- .../Entities/Users/IGuildUser.cs | 9 +++++++ .../Extensions/GuildUserExtensions.cs | 27 ------------------- src/Discord.Net.Rest/DiscordRestApiClient.cs | 20 ++++++++++++++ .../Entities/Users/RestGuildUser.cs | 12 +++++++++ .../Entities/Users/UserHelper.cs | 13 +++++++++ .../Entities/Users/SocketGuildUser.cs | 12 +++++++++ 7 files changed, 70 insertions(+), 31 deletions(-) delete mode 100644 src/Discord.Net.Core/Extensions/GuildUserExtensions.cs diff --git a/src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs b/src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs index 5ceffef0e..33b311604 100644 --- a/src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs +++ b/src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs @@ -41,16 +41,16 @@ namespace Discord /// What roles should the user have? /// /// - /// To add a role to a user: - /// To remove a role from a user: + /// To add a role to a user: + /// To remove a role from a user: /// public Optional> Roles { get; set; } /// /// What roles should the user have? /// /// - /// To add a role to a user: - /// To remove a role from a user: + /// To add a role to a user: + /// To remove a role from a user: /// public Optional> RoleIds { get; set; } /// diff --git a/src/Discord.Net.Core/Entities/Users/IGuildUser.cs b/src/Discord.Net.Core/Entities/Users/IGuildUser.cs index 79e8f5dcc..cd9516395 100644 --- a/src/Discord.Net.Core/Entities/Users/IGuildUser.cs +++ b/src/Discord.Net.Core/Entities/Users/IGuildUser.cs @@ -28,5 +28,14 @@ namespace Discord Task KickAsync(RequestOptions options = null); /// Modifies this user's properties in this guild. Task ModifyAsync(Action func, RequestOptions options = null); + + /// Adds a role to this user in this guild. + Task AddRoleAsync(IRole role, RequestOptions options = null); + /// Adds roles to this user in this guild. + Task AddRolesAsync(IEnumerable roles, RequestOptions options = null); + /// Removes a role from this user in this guild. + Task RemoveRoleAsync(IRole role, RequestOptions options = null); + /// Removes roles from this user in this guild. + Task RemoveRolesAsync(IEnumerable roles, RequestOptions options = null); } } diff --git a/src/Discord.Net.Core/Extensions/GuildUserExtensions.cs b/src/Discord.Net.Core/Extensions/GuildUserExtensions.cs deleted file mode 100644 index 9d152adf9..000000000 --- a/src/Discord.Net.Core/Extensions/GuildUserExtensions.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Discord -{ - public static class GuildUserExtensions - { - public static Task AddRolesAsync(this IGuildUser user, params IRole[] roles) - => ChangeRolesAsync(user, add: roles); - public static Task AddRolesAsync(this IGuildUser user, IEnumerable roles) - => ChangeRolesAsync(user, add: roles); - public static Task RemoveRolesAsync(this IGuildUser user, params IRole[] roles) - => ChangeRolesAsync(user, remove: roles); - public static Task RemoveRolesAsync(this IGuildUser user, IEnumerable roles) - => ChangeRolesAsync(user, remove: roles); - public static async Task ChangeRolesAsync(this IGuildUser user, IEnumerable add = null, IEnumerable remove = null) - { - IEnumerable roleIds = user.RoleIds; - if (remove != null) - roleIds = roleIds.Except(remove.Select(x => x.Id)); - if (add != null) - roleIds = roleIds.Concat(add.Select(x => x.Id)); - await user.ModifyAsync(x => x.RoleIds = roleIds.ToArray()).ConfigureAwait(false); - } - } -} diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index b9c0e9fda..2e404003f 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -388,6 +388,26 @@ namespace Discord.API break; } } + public async Task AddRoleAsync(ulong guildId, ulong userId, ulong roleId, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotEqual(userId, 0, nameof(userId)); + Preconditions.NotEqual(roleId, 0, nameof(roleId)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + await SendAsync("PUT", () => $"guilds/{guildId}/members/{userId}/roles/{roleId}", ids, options: options); + } + public async Task RemoveRoleAsync(ulong guildId, ulong userId, ulong roleId, RequestOptions options = null) + { + Preconditions.NotEqual(guildId, 0, nameof(guildId)); + Preconditions.NotEqual(userId, 0, nameof(userId)); + Preconditions.NotEqual(roleId, 0, nameof(roleId)); + options = RequestOptions.CreateOrClone(options); + + var ids = new BucketIds(guildId: guildId); + await SendAsync("DELETE", () => $"guilds/{guildId}/members/{userId}/roles/{roleId}", ids, options: options); + } //Channel Messages public async Task GetChannelMessageAsync(ulong channelId, ulong messageId, RequestOptions options = null) diff --git a/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs b/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs index 8de42608d..538f6b80f 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs @@ -84,6 +84,18 @@ namespace Discord.Rest } public Task KickAsync(RequestOptions options = null) => UserHelper.KickAsync(this, Discord, options); + /// + public Task AddRoleAsync(IRole role, RequestOptions options = null) + => AddRolesAsync(new[] { role }, options); + /// + public Task AddRolesAsync(IEnumerable roles, RequestOptions options = null) + => UserHelper.AddRolesAsync(this, Discord, roles, options); + /// + public Task RemoveRoleAsync(IRole role, RequestOptions options = null) + => RemoveRolesAsync(new[] { role }, options); + /// + public Task RemoveRolesAsync(IEnumerable roles, RequestOptions options = null) + => UserHelper.RemoveRolesAsync(this, Discord, roles, options); public ChannelPermissions GetPermissions(IGuildChannel channel) { diff --git a/src/Discord.Net.Rest/Entities/Users/UserHelper.cs b/src/Discord.Net.Rest/Entities/Users/UserHelper.cs index 5189851fd..82e59227d 100644 --- a/src/Discord.Net.Rest/Entities/Users/UserHelper.cs +++ b/src/Discord.Net.Rest/Entities/Users/UserHelper.cs @@ -1,5 +1,6 @@ using Discord.API.Rest; using System; +using System.Collections.Generic; using System.Threading.Tasks; using Model = Discord.API.User; using ImageModel = Discord.API.Image; @@ -63,5 +64,17 @@ namespace Discord.Rest var args = new CreateDMChannelParams(user.Id); return RestDMChannel.Create(client, await client.ApiClient.CreateDMChannelAsync(args, options).ConfigureAwait(false)); } + + public static async Task AddRolesAsync(IGuildUser user, BaseDiscordClient client, IEnumerable roles, RequestOptions options) + { + foreach (var role in roles) + await client.ApiClient.AddRoleAsync(user.Guild.Id, user.Id, role.Id, options); + } + + public static async Task RemoveRolesAsync(IGuildUser user, BaseDiscordClient client, IEnumerable roles, RequestOptions options) + { + foreach (var role in roles) + await client.ApiClient.RemoveRoleAsync(user.Guild.Id, user.Id, role.Id, options); + } } } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs index 1b2e10332..e92ebb0b1 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs @@ -109,6 +109,18 @@ namespace Discord.WebSocket => UserHelper.ModifyAsync(this, Discord, func, options); public Task KickAsync(RequestOptions options = null) => UserHelper.KickAsync(this, Discord, options); + /// + public Task AddRoleAsync(IRole role, RequestOptions options = null) + => AddRolesAsync(new[] { role }, options); + /// + public Task AddRolesAsync(IEnumerable roles, RequestOptions options = null) + => UserHelper.AddRolesAsync(this, Discord, roles, options); + /// + public Task RemoveRoleAsync(IRole role, RequestOptions options = null) + => RemoveRolesAsync(new[] { role }, options); + /// + public Task RemoveRolesAsync(IEnumerable roles, RequestOptions options = null) + => UserHelper.RemoveRolesAsync(this, Discord, roles, options); public ChannelPermissions GetPermissions(IGuildChannel channel) => new ChannelPermissions(Permissions.ResolveChannel(Guild, this, channel, GuildPermissions.RawValue)); From 3ddb01a5a2e288c508d9c461f0138a735dd16060 Mon Sep 17 00:00:00 2001 From: Finite Reality Date: Sat, 18 Mar 2017 11:55:56 +0000 Subject: [PATCH 034/243] Fix order of iteration in ExecuteAsync (#534) --- src/Discord.Net.Commands/CommandService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index 2c7955028..5f4f2a504 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -247,7 +247,7 @@ namespace Discord.Commands return searchResult; var commands = searchResult.Commands; - for (int i = commands.Count - 1; i >= 0; i--) + for (int i = 0; i < commands.Count; i++) { var preconditionResult = await commands[i].CheckPreconditionsAsync(context, dependencyMap).ConfigureAwait(false); if (!preconditionResult.IsSuccess) From fbd34d6719726bb82bc72014743f8ea808ad2562 Mon Sep 17 00:00:00 2001 From: Confruggy Date: Sat, 18 Mar 2017 12:57:56 +0100 Subject: [PATCH 035/243] Update MessageHelper.cs (#508) --- src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs b/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs index 2147cc20f..2c6f67477 100644 --- a/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs +++ b/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs @@ -36,7 +36,7 @@ namespace Discord.Rest } public static async Task RemoveReactionAsync(IMessage msg, IUser user, Emoji emoji, BaseDiscordClient client, RequestOptions options) - => await RemoveReactionAsync(msg, user, emoji.Id == 0 ? emoji.Name : $"{emoji.Name}:{emoji.Id}", client, options).ConfigureAwait(false); + => await RemoveReactionAsync(msg, user, emoji.Id == null ? emoji.Name : $"{emoji.Name}:{emoji.Id}", client, options).ConfigureAwait(false); public static async Task RemoveReactionAsync(IMessage msg, IUser user, string emoji, BaseDiscordClient client, RequestOptions options) { From 96a377a258e2d1d4463c5c9217fd0899467f218e Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 18 Mar 2017 09:32:04 -0300 Subject: [PATCH 036/243] If discord error code is 0, fall back to http code --- src/Discord.Net.Core/Net/HttpException.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.Core/Net/HttpException.cs b/src/Discord.Net.Core/Net/HttpException.cs index 4141979a0..1c872245c 100644 --- a/src/Discord.Net.Core/Net/HttpException.cs +++ b/src/Discord.Net.Core/Net/HttpException.cs @@ -20,7 +20,7 @@ namespace Discord.Net private static string CreateMessage(HttpStatusCode httpCode, int? discordCode = null, string reason = null) { string msg; - if (discordCode != null) + if (discordCode != null && discordCode != 0) { if (reason != null) msg = $"The server responded with error {(int)discordCode}: {reason}"; From 2160e5dac8b8f1743c79c4f96de7c95b3d0db9a0 Mon Sep 17 00:00:00 2001 From: Finite Reality Date: Sat, 18 Mar 2017 12:55:53 +0000 Subject: [PATCH 037/243] Improve parameter precondition type safety (#532) * Improve parameter precondition type safety Also removes some terrible code which was left over when I first implemented parameter preconditions. I don't know why that was there. With this commit, parameter preconditions should be much safer as they use generic methods instead of janky casting of objects. * Remove generic CheckPreconditions method --- src/Discord.Net.Commands/Info/CommandInfo.cs | 6 ++++-- src/Discord.Net.Commands/Info/ParameterInfo.cs | 10 +++------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/Discord.Net.Commands/Info/CommandInfo.cs b/src/Discord.Net.Commands/Info/CommandInfo.cs index 031d37581..63333f0d2 100644 --- a/src/Discord.Net.Commands/Info/CommandInfo.cs +++ b/src/Discord.Net.Commands/Info/CommandInfo.cs @@ -128,9 +128,11 @@ namespace Discord.Commands { object[] args = GenerateArgs(argList, paramList); - foreach (var parameter in Parameters) + for (int position = 0; position < Parameters.Count; position++) { - var result = await parameter.CheckPreconditionsAsync(context, args, map).ConfigureAwait(false); + var parameter = Parameters[position]; + var argument = args[position]; + var result = await parameter.CheckPreconditionsAsync(context, argument, map).ConfigureAwait(false); if (!result.IsSuccess) return ExecuteResult.FromError(result); } diff --git a/src/Discord.Net.Commands/Info/ParameterInfo.cs b/src/Discord.Net.Commands/Info/ParameterInfo.cs index a0cdf03d7..d49b84c7f 100644 --- a/src/Discord.Net.Commands/Info/ParameterInfo.cs +++ b/src/Discord.Net.Commands/Info/ParameterInfo.cs @@ -4,6 +4,7 @@ using System.Collections.Immutable; using System.Threading.Tasks; using Discord.Commands.Builders; +using System.Reflection; namespace Discord.Commands { @@ -40,19 +41,14 @@ namespace Discord.Commands _reader = builder.TypeReader; } - public async Task CheckPreconditionsAsync(ICommandContext context, object[] args, IDependencyMap map = null) + public async Task CheckPreconditionsAsync(ICommandContext context, object arg, IDependencyMap map = null) { if (map == null) map = DependencyMap.Empty; - int position = 0; - for(position = 0; position < Command.Parameters.Count; position++) - if (Command.Parameters[position] == this) - break; - foreach (var precondition in Preconditions) { - var result = await precondition.CheckPermissions(context, this, args[position], map).ConfigureAwait(false); + var result = await precondition.CheckPermissions(context, this, arg, map).ConfigureAwait(false); if (!result.IsSuccess) return result; } From 21959fe43c220d804f252cf65e75137336a1aa20 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 18 Mar 2017 10:36:37 -0300 Subject: [PATCH 038/243] Fixed several permission issues --- src/Discord.Net.Core/Utils/Permissions.cs | 27 ++++++++++++++++------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/src/Discord.Net.Core/Utils/Permissions.cs b/src/Discord.Net.Core/Utils/Permissions.cs index a6c545da0..d0e78472d 100644 --- a/src/Discord.Net.Core/Utils/Permissions.cs +++ b/src/Discord.Net.Core/Utils/Permissions.cs @@ -111,7 +111,7 @@ namespace Discord ulong resolvedPermissions = 0; ulong mask = ChannelPermissions.All(channel).RawValue; - if (/*user.Id == user.Guild.OwnerId || */GetValue(guildPermissions, GuildPermission.Administrator)) + if (GetValue(guildPermissions, GuildPermission.Administrator)) //Includes owner resolvedPermissions = mask; //Owners and administrators always have all permissions else { @@ -133,21 +133,32 @@ namespace Discord deniedPermissions |= perms.Value.DenyValue; } } - resolvedPermissions = (resolvedPermissions | allowedPermissions) & ~deniedPermissions; + resolvedPermissions = (resolvedPermissions & ~deniedPermissions) | allowedPermissions; } //Give/Take User permissions perms = channel.GetPermissionOverwrite(user); if (perms != null) - resolvedPermissions = (resolvedPermissions | perms.Value.AllowValue) & ~perms.Value.DenyValue; + resolvedPermissions = (resolvedPermissions & ~perms.Value.DenyValue) | perms.Value.AllowValue; //TODO: C#7 Typeswitch candidate var textChannel = channel as ITextChannel; - var voiceChannel = channel as IVoiceChannel; - if (textChannel != null && !GetValue(resolvedPermissions, ChannelPermission.ReadMessages)) - resolvedPermissions = 0; //No read permission on a text channel removes all other permissions - else if (voiceChannel != null && !GetValue(resolvedPermissions, ChannelPermission.Connect)) - resolvedPermissions = 0; //No connect permission on a voice channel removes all other permissions + if (textChannel != null) + { + if (!GetValue(resolvedPermissions, ChannelPermission.ReadMessages)) + { + //No read permission on a text channel removes all other permissions + resolvedPermissions = 0; + } + else if (!GetValue(resolvedPermissions, ChannelPermission.SendMessages)) + { + //No send permissions on a text channel removes all send-related permissions + resolvedPermissions &= ~(1UL << (int)ChannelPermission.SendTTSMessages); + resolvedPermissions &= ~(1UL << (int)ChannelPermission.MentionEveryone); + resolvedPermissions &= ~(1UL << (int)ChannelPermission.EmbedLinks); + resolvedPermissions &= ~(1UL << (int)ChannelPermission.AttachFiles); + } + } resolvedPermissions &= mask; //Ensure we didnt get any permissions this channel doesnt support (from guildPerms, for example) } From 5a6d6ee076439fdb60581bc3a31f91e487de8d5a Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 18 Mar 2017 10:37:57 -0300 Subject: [PATCH 039/243] Fixed notempty precondition error message --- src/Discord.Net.Core/Utils/Preconditions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.Core/Utils/Preconditions.cs b/src/Discord.Net.Core/Utils/Preconditions.cs index 65af6e49b..16046c2a9 100644 --- a/src/Discord.Net.Core/Utils/Preconditions.cs +++ b/src/Discord.Net.Core/Utils/Preconditions.cs @@ -46,7 +46,7 @@ namespace Discord private static ArgumentException CreateNotEmptyException(string name, string msg) { - if (msg == null) return new ArgumentException(name, "Argument cannot be blank."); + if (msg == null) return new ArgumentException("Argument cannot be blank", name); else return new ArgumentException(name, msg); } From bc469cbb4612d1976298445afd36930a540802e1 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 18 Mar 2017 11:33:38 -0300 Subject: [PATCH 040/243] Process everyone permission overwrites before role --- src/Discord.Net.Core/Utils/Permissions.cs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/Discord.Net.Core/Utils/Permissions.cs b/src/Discord.Net.Core/Utils/Permissions.cs index d0e78472d..a99c64094 100644 --- a/src/Discord.Net.Core/Utils/Permissions.cs +++ b/src/Discord.Net.Core/Utils/Permissions.cs @@ -115,16 +115,21 @@ namespace Discord resolvedPermissions = mask; //Owners and administrators always have all permissions else { + OverwritePermissions? perms; + //Start with this user's guild permissions resolvedPermissions = guildPermissions; + //Give/Take Everyone permissions + perms = channel.GetPermissionOverwrite(guild.EveryoneRole); + if (perms != null) + resolvedPermissions = (resolvedPermissions & ~perms.Value.DenyValue) | perms.Value.AllowValue; + //Give/Take Role permissions - OverwritePermissions? perms; - var roleIds = user.RoleIds; - if (roleIds.Count > 0) + ulong deniedPermissions = 0UL, allowedPermissions = 0UL; + foreach (var roleId in user.RoleIds) { - ulong deniedPermissions = 0UL, allowedPermissions = 0UL; - foreach (var roleId in roleIds) + if (roleId != guild.EveryoneRole.Id) { perms = channel.GetPermissionOverwrite(guild.GetRole(roleId)); if (perms != null) @@ -133,8 +138,8 @@ namespace Discord deniedPermissions |= perms.Value.DenyValue; } } - resolvedPermissions = (resolvedPermissions & ~deniedPermissions) | allowedPermissions; } + resolvedPermissions = (resolvedPermissions & ~deniedPermissions) | allowedPermissions; //Give/Take User permissions perms = channel.GetPermissionOverwrite(user); From a2b12520b2839a6c67cc6aa07c54c7ef4124dabe Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 18 Mar 2017 12:14:14 -0300 Subject: [PATCH 041/243] Added CommandService logging --- src/Discord.Net.Commands/CommandException.cs | 15 ++++++ src/Discord.Net.Commands/CommandService.cs | 17 +++++-- .../CommandServiceConfig.cs | 3 ++ src/Discord.Net.Commands/Info/CommandInfo.cs | 46 +++++++++++++++++-- .../Info/ParameterInfo.cs | 4 +- src/Discord.Net.Core/DiscordConfig.cs | 2 +- 6 files changed, 76 insertions(+), 11 deletions(-) create mode 100644 src/Discord.Net.Commands/CommandException.cs diff --git a/src/Discord.Net.Commands/CommandException.cs b/src/Discord.Net.Commands/CommandException.cs new file mode 100644 index 000000000..9a582d17b --- /dev/null +++ b/src/Discord.Net.Commands/CommandException.cs @@ -0,0 +1,15 @@ +using System; + +namespace Discord.Commands +{ + public class CommandException : Exception + { + public CommandInfo Command { get; } + public ICommandContext Content { get; } + + public CommandException(CommandInfo command, ICommandContext context, Exception ex) + : base($"Error occurred executing {command.GetLogText(context)}.", ex) + { + } + } +} diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index 5f4f2a504..e8db0100d 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -1,4 +1,6 @@ -using System; +using Discord.Commands.Builders; +using Discord.Logging; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; @@ -7,12 +9,13 @@ using System.Reflection; using System.Threading; using System.Threading.Tasks; -using Discord.Commands.Builders; - namespace Discord.Commands { public class CommandService { + public event Func Log { add { _logEvent.Add(value); } remove { _logEvent.Remove(value); } } + internal readonly AsyncEvent> _logEvent = new AsyncEvent>(); + private readonly SemaphoreSlim _moduleLock; private readonly ConcurrentDictionary _typedModuleDefs; private readonly ConcurrentDictionary> _typeReaders; @@ -24,6 +27,8 @@ namespace Discord.Commands internal readonly bool _caseSensitive; internal readonly char _separatorChar; internal readonly RunMode _defaultRunMode; + internal readonly Logger _cmdLogger; + internal readonly LogManager _logManager; public IEnumerable Modules => _moduleDefs.Select(x => x); public IEnumerable Commands => _moduleDefs.SelectMany(x => x.Commands); @@ -36,7 +41,11 @@ namespace Discord.Commands _separatorChar = config.SeparatorChar; _defaultRunMode = config.DefaultRunMode; if (_defaultRunMode == RunMode.Default) - throw new InvalidOperationException("The default run mode cannot be set to Default, it must be one of Sync, Mixed, or Async"); + throw new InvalidOperationException("The default run mode cannot be set to Default."); + + _logManager = new LogManager(config.LogLevel); + _logManager.Message += async msg => await _logEvent.InvokeAsync(msg).ConfigureAwait(false); + _cmdLogger = _logManager.CreateLogger("Command"); _moduleLock = new SemaphoreSlim(1, 1); _typedModuleDefs = new ConcurrentDictionary(); diff --git a/src/Discord.Net.Commands/CommandServiceConfig.cs b/src/Discord.Net.Commands/CommandServiceConfig.cs index 037e315c7..a94e433f5 100644 --- a/src/Discord.Net.Commands/CommandServiceConfig.cs +++ b/src/Discord.Net.Commands/CommandServiceConfig.cs @@ -8,5 +8,8 @@ public char SeparatorChar { get; set; } = ' '; /// Should commands be case-sensitive? public bool CaseSensitiveCommands { get; set; } = false; + + /// Gets or sets the minimum log level severity that will be sent to the Log event. + public LogSeverity LogLevel { get; set; } = LogSeverity.Info; } } diff --git a/src/Discord.Net.Commands/Info/CommandInfo.cs b/src/Discord.Net.Commands/Info/CommandInfo.cs index 63333f0d2..d953013ab 100644 --- a/src/Discord.Net.Commands/Info/CommandInfo.cs +++ b/src/Discord.Net.Commands/Info/CommandInfo.cs @@ -137,16 +137,48 @@ namespace Discord.Commands return ExecuteResult.FromError(result); } + await Module.Service._cmdLogger.DebugAsync($"Executing {GetLogText(context)}").ConfigureAwait(false); switch (RunMode) { case RunMode.Sync: //Always sync - await _action(context, args, map).ConfigureAwait(false); + try + { + await _action(context, args, map).ConfigureAwait(false); + } + catch (Exception ex) + { + ex = new CommandException(this, context, ex); + await Module.Service._cmdLogger.ErrorAsync(ex).ConfigureAwait(false); + throw; + } + await Module.Service._cmdLogger.VerboseAsync($"Executed {GetLogText(context)}").ConfigureAwait(false); break; case RunMode.Mixed: //Sync until first await statement - var t1 = _action(context, args, map); + var t1 = _action(context, args, map).ContinueWith(async t => + { + if (t.IsFaulted) + { + var ex = new CommandException(this, context, t.Exception); + await Module.Service._cmdLogger.ErrorAsync(ex).ConfigureAwait(false); + } + else + await Module.Service._cmdLogger.VerboseAsync($"Executed {GetLogText(context)}").ConfigureAwait(false); + }); break; case RunMode.Async: //Always async - var t2 = Task.Run(() => _action(context, args, map)); + var t2 = Task.Run(() => + { + var _ = _action(context, args, map).ContinueWith(async t => + { + if (t.IsFaulted) + { + var ex = new CommandException(this, context, t.Exception); + await Module.Service._cmdLogger.ErrorAsync(ex).ConfigureAwait(false); + } + else + await Module.Service._cmdLogger.VerboseAsync($"Executed {GetLogText(context)}").ConfigureAwait(false); + }); + }); break; } return ExecuteResult.FromSuccess(); @@ -189,5 +221,13 @@ namespace Discord.Commands private static T[] ConvertParamsList(IEnumerable paramsList) => paramsList.Cast().ToArray(); + + internal string GetLogText(ICommandContext context) + { + if (context.Guild != null) + return $"\"{Name}\" for {context.User} in {context.Guild}/{context.Channel}"; + else + return $"\"{Name}\" for {context.User} in {context.Channel}"; + } } } \ No newline at end of file diff --git a/src/Discord.Net.Commands/Info/ParameterInfo.cs b/src/Discord.Net.Commands/Info/ParameterInfo.cs index d49b84c7f..4ef145b9e 100644 --- a/src/Discord.Net.Commands/Info/ParameterInfo.cs +++ b/src/Discord.Net.Commands/Info/ParameterInfo.cs @@ -1,11 +1,9 @@ +using Discord.Commands.Builders; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Threading.Tasks; -using Discord.Commands.Builders; -using System.Reflection; - namespace Discord.Commands { public class ParameterInfo diff --git a/src/Discord.Net.Core/DiscordConfig.cs b/src/Discord.Net.Core/DiscordConfig.cs index 78a5b0e1e..b1e075e5b 100644 --- a/src/Discord.Net.Core/DiscordConfig.cs +++ b/src/Discord.Net.Core/DiscordConfig.cs @@ -22,7 +22,7 @@ namespace Discord /// Gets or sets how a request should act in the case of an error, by default. public RetryMode DefaultRetryMode { get; set; } = RetryMode.AlwaysRetry; - /// Gets or sets the minimum log level severity that will be sent to the LogMessage event. + /// Gets or sets the minimum log level severity that will be sent to the Log event. public LogSeverity LogLevel { get; set; } = LogSeverity.Info; /// Gets or sets whether the initial log entry should be printed. From ba07484fe9145b868d9f98a149da27484b6e5b76 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 18 Mar 2017 12:23:51 -0300 Subject: [PATCH 042/243] Removed Mixed RunMode --- src/Discord.Net.Commands/Info/CommandInfo.cs | 54 +++++++------------- src/Discord.Net.Commands/RunMode.cs | 1 - 2 files changed, 19 insertions(+), 36 deletions(-) diff --git a/src/Discord.Net.Commands/Info/CommandInfo.cs b/src/Discord.Net.Commands/Info/CommandInfo.cs index d953013ab..26b6163ab 100644 --- a/src/Discord.Net.Commands/Info/CommandInfo.cs +++ b/src/Discord.Net.Commands/Info/CommandInfo.cs @@ -137,47 +137,15 @@ namespace Discord.Commands return ExecuteResult.FromError(result); } - await Module.Service._cmdLogger.DebugAsync($"Executing {GetLogText(context)}").ConfigureAwait(false); switch (RunMode) { case RunMode.Sync: //Always sync - try - { - await _action(context, args, map).ConfigureAwait(false); - } - catch (Exception ex) - { - ex = new CommandException(this, context, ex); - await Module.Service._cmdLogger.ErrorAsync(ex).ConfigureAwait(false); - throw; - } - await Module.Service._cmdLogger.VerboseAsync($"Executed {GetLogText(context)}").ConfigureAwait(false); - break; - case RunMode.Mixed: //Sync until first await statement - var t1 = _action(context, args, map).ContinueWith(async t => - { - if (t.IsFaulted) - { - var ex = new CommandException(this, context, t.Exception); - await Module.Service._cmdLogger.ErrorAsync(ex).ConfigureAwait(false); - } - else - await Module.Service._cmdLogger.VerboseAsync($"Executed {GetLogText(context)}").ConfigureAwait(false); - }); + var t1 = ExecuteAsyncInternal(context, args, map); break; case RunMode.Async: //Always async - var t2 = Task.Run(() => + var t2 = Task.Run(async () => { - var _ = _action(context, args, map).ContinueWith(async t => - { - if (t.IsFaulted) - { - var ex = new CommandException(this, context, t.Exception); - await Module.Service._cmdLogger.ErrorAsync(ex).ConfigureAwait(false); - } - else - await Module.Service._cmdLogger.VerboseAsync($"Executed {GetLogText(context)}").ConfigureAwait(false); - }); + await ExecuteAsyncInternal(context, args, map).ConfigureAwait(false); }); break; } @@ -189,6 +157,22 @@ namespace Discord.Commands } } + private async Task ExecuteAsyncInternal(ICommandContext context, object[] args, IDependencyMap map) + { + await Module.Service._cmdLogger.DebugAsync($"Executing {GetLogText(context)}").ConfigureAwait(false); + try + { + await _action(context, args, map).ConfigureAwait(false); + } + catch (Exception ex) + { + ex = new CommandException(this, context, ex); + await Module.Service._cmdLogger.ErrorAsync(ex).ConfigureAwait(false); + throw; + } + await Module.Service._cmdLogger.VerboseAsync($"Executed {GetLogText(context)}").ConfigureAwait(false); + } + private object[] GenerateArgs(IEnumerable argList, IEnumerable paramsList) { int argCount = Parameters.Count; diff --git a/src/Discord.Net.Commands/RunMode.cs b/src/Discord.Net.Commands/RunMode.cs index 2bb5dbbf6..ecb6a4b58 100644 --- a/src/Discord.Net.Commands/RunMode.cs +++ b/src/Discord.Net.Commands/RunMode.cs @@ -4,7 +4,6 @@ { Default, Sync, - Mixed, Async } } From 83bd16f329114e000c7ccafd5e7d1bbb465b6b24 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Sat, 18 Mar 2017 20:36:06 -0400 Subject: [PATCH 043/243] Don't attempt to convert Embed entities to models if the entity is null Allows null embeds to be passed into ModifyAsync --- Discord.Net.sln | 127 ++++++++---------- .../Extensions/EntityExtensions.cs | 1 + 2 files changed, 58 insertions(+), 70 deletions(-) diff --git a/Discord.Net.sln b/Discord.Net.sln index 1ba7549c3..ac75f147e 100644 --- a/Discord.Net.sln +++ b/Discord.Net.sln @@ -1,7 +1,6 @@ - Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26014.0 +VisualStudioVersion = 15.0.26228.4 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Core", "src\Discord.Net.Core\Discord.Net.Core.csproj", "{91E9E7BD-75C9-4E98-84AA-2C271922E5C2}" EndProject @@ -43,16 +42,16 @@ Global GlobalSection(ProjectConfigurationPlatforms) = postSolution {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Debug|x64.ActiveCfg = Debug|x64 - {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Debug|x64.Build.0 = Debug|x64 - {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Debug|x86.ActiveCfg = Debug|x86 - {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Debug|x86.Build.0 = Debug|x86 + {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Debug|x64.ActiveCfg = Debug|Any CPU + {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Debug|x64.Build.0 = Debug|Any CPU + {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Debug|x86.ActiveCfg = Debug|Any CPU + {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Debug|x86.Build.0 = Debug|Any CPU {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Release|Any CPU.ActiveCfg = Release|Any CPU {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Release|Any CPU.Build.0 = Release|Any CPU - {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Release|x64.ActiveCfg = Release|x64 - {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Release|x64.Build.0 = Release|x64 - {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Release|x86.ActiveCfg = Release|x86 - {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Release|x86.Build.0 = Release|x86 + {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Release|x64.ActiveCfg = Release|Any CPU + {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Release|x64.Build.0 = Release|Any CPU + {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Release|x86.ActiveCfg = Release|Any CPU + {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Release|x86.Build.0 = Release|Any CPU {BFC6DC28-0351-4573-926A-D4124244C04F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {BFC6DC28-0351-4573-926A-D4124244C04F}.Debug|Any CPU.Build.0 = Debug|Any CPU {BFC6DC28-0351-4573-926A-D4124244C04F}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -91,88 +90,76 @@ Global {078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}.Release|x86.Build.0 = Debug|Any CPU {688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Debug|x64.ActiveCfg = Debug|x64 - {688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Debug|x64.Build.0 = Debug|x64 - {688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Debug|x86.ActiveCfg = Debug|x86 - {688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Debug|x86.Build.0 = Debug|x86 + {688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Debug|x64.ActiveCfg = Debug|Any CPU + {688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Debug|x64.Build.0 = Debug|Any CPU + {688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Debug|x86.ActiveCfg = Debug|Any CPU + {688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Debug|x86.Build.0 = Debug|Any CPU {688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Release|Any CPU.ActiveCfg = Release|Any CPU {688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Release|Any CPU.Build.0 = Release|Any CPU - {688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Release|x64.ActiveCfg = Release|x64 - {688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Release|x64.Build.0 = Release|x64 - {688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Release|x86.ActiveCfg = Release|x86 - {688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Release|x86.Build.0 = Release|x86 + {688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Release|x64.ActiveCfg = Release|Any CPU + {688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Release|x64.Build.0 = Release|Any CPU + {688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Release|x86.ActiveCfg = Release|Any CPU + {688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Release|x86.Build.0 = Release|Any CPU {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Debug|x64.ActiveCfg = Debug|x64 - {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Debug|x64.Build.0 = Debug|x64 - {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Debug|x86.ActiveCfg = Debug|x86 - {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Debug|x86.Build.0 = Debug|x86 + {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Debug|x64.ActiveCfg = Debug|Any CPU + {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Debug|x64.Build.0 = Debug|Any CPU + {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Debug|x86.ActiveCfg = Debug|Any CPU + {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Debug|x86.Build.0 = Debug|Any CPU {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Release|Any CPU.ActiveCfg = Release|Any CPU {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Release|Any CPU.Build.0 = Release|Any CPU - {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Release|x64.ActiveCfg = Release|x64 - {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Release|x64.Build.0 = Release|x64 - {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Release|x86.ActiveCfg = Release|x86 - {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Release|x86.Build.0 = Release|x86 - {547261FC-8BA3-40EA-A040-A38ABDAA8D72}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {547261FC-8BA3-40EA-A040-A38ABDAA8D72}.Debug|Any CPU.Build.0 = Debug|Any CPU - {547261FC-8BA3-40EA-A040-A38ABDAA8D72}.Debug|x64.ActiveCfg = Debug|x64 - {547261FC-8BA3-40EA-A040-A38ABDAA8D72}.Debug|x64.Build.0 = Debug|x64 - {547261FC-8BA3-40EA-A040-A38ABDAA8D72}.Debug|x86.ActiveCfg = Debug|x86 - {547261FC-8BA3-40EA-A040-A38ABDAA8D72}.Debug|x86.Build.0 = Debug|x86 - {547261FC-8BA3-40EA-A040-A38ABDAA8D72}.Release|Any CPU.ActiveCfg = Release|Any CPU - {547261FC-8BA3-40EA-A040-A38ABDAA8D72}.Release|Any CPU.Build.0 = Release|Any CPU - {547261FC-8BA3-40EA-A040-A38ABDAA8D72}.Release|x64.ActiveCfg = Release|x64 - {547261FC-8BA3-40EA-A040-A38ABDAA8D72}.Release|x64.Build.0 = Release|x64 - {547261FC-8BA3-40EA-A040-A38ABDAA8D72}.Release|x86.ActiveCfg = Release|x86 - {547261FC-8BA3-40EA-A040-A38ABDAA8D72}.Release|x86.Build.0 = Release|x86 + {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Release|x64.ActiveCfg = Release|Any CPU + {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Release|x64.Build.0 = Release|Any CPU + {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Release|x86.ActiveCfg = Release|Any CPU + {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Release|x86.Build.0 = Release|Any CPU {ABC9F4B9-2452-4725-B522-754E0A02E282}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {ABC9F4B9-2452-4725-B522-754E0A02E282}.Debug|Any CPU.Build.0 = Debug|Any CPU - {ABC9F4B9-2452-4725-B522-754E0A02E282}.Debug|x64.ActiveCfg = Debug|x64 - {ABC9F4B9-2452-4725-B522-754E0A02E282}.Debug|x64.Build.0 = Debug|x64 - {ABC9F4B9-2452-4725-B522-754E0A02E282}.Debug|x86.ActiveCfg = Debug|x86 - {ABC9F4B9-2452-4725-B522-754E0A02E282}.Debug|x86.Build.0 = Debug|x86 + {ABC9F4B9-2452-4725-B522-754E0A02E282}.Debug|x64.ActiveCfg = Debug|Any CPU + {ABC9F4B9-2452-4725-B522-754E0A02E282}.Debug|x64.Build.0 = Debug|Any CPU + {ABC9F4B9-2452-4725-B522-754E0A02E282}.Debug|x86.ActiveCfg = Debug|Any CPU + {ABC9F4B9-2452-4725-B522-754E0A02E282}.Debug|x86.Build.0 = Debug|Any CPU {ABC9F4B9-2452-4725-B522-754E0A02E282}.Release|Any CPU.ActiveCfg = Release|Any CPU {ABC9F4B9-2452-4725-B522-754E0A02E282}.Release|Any CPU.Build.0 = Release|Any CPU - {ABC9F4B9-2452-4725-B522-754E0A02E282}.Release|x64.ActiveCfg = Release|x64 - {ABC9F4B9-2452-4725-B522-754E0A02E282}.Release|x64.Build.0 = Release|x64 - {ABC9F4B9-2452-4725-B522-754E0A02E282}.Release|x86.ActiveCfg = Release|x86 - {ABC9F4B9-2452-4725-B522-754E0A02E282}.Release|x86.Build.0 = Release|x86 + {ABC9F4B9-2452-4725-B522-754E0A02E282}.Release|x64.ActiveCfg = Release|Any CPU + {ABC9F4B9-2452-4725-B522-754E0A02E282}.Release|x64.Build.0 = Release|Any CPU + {ABC9F4B9-2452-4725-B522-754E0A02E282}.Release|x86.ActiveCfg = Release|Any CPU + {ABC9F4B9-2452-4725-B522-754E0A02E282}.Release|x86.Build.0 = Release|Any CPU {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Debug|x64.ActiveCfg = Debug|x64 - {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Debug|x64.Build.0 = Debug|x64 - {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Debug|x86.ActiveCfg = Debug|x86 - {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Debug|x86.Build.0 = Debug|x86 + {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Debug|x64.ActiveCfg = Debug|Any CPU + {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Debug|x64.Build.0 = Debug|Any CPU + {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Debug|x86.ActiveCfg = Debug|Any CPU + {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Debug|x86.Build.0 = Debug|Any CPU {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Release|Any CPU.ActiveCfg = Release|Any CPU {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Release|Any CPU.Build.0 = Release|Any CPU - {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Release|x64.ActiveCfg = Release|x64 - {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Release|x64.Build.0 = Release|x64 - {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Release|x86.ActiveCfg = Release|x86 - {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Release|x86.Build.0 = Release|x86 + {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Release|x64.ActiveCfg = Release|Any CPU + {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Release|x64.Build.0 = Release|Any CPU + {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Release|x86.ActiveCfg = Release|Any CPU + {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Release|x86.Build.0 = Release|Any CPU {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Debug|x64.ActiveCfg = Debug|x64 - {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Debug|x64.Build.0 = Debug|x64 - {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Debug|x86.ActiveCfg = Debug|x86 - {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Debug|x86.Build.0 = Debug|x86 + {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Debug|x64.ActiveCfg = Debug|Any CPU + {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Debug|x64.Build.0 = Debug|Any CPU + {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Debug|x86.ActiveCfg = Debug|Any CPU + {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Debug|x86.Build.0 = Debug|Any CPU {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Release|Any CPU.ActiveCfg = Release|Any CPU {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Release|Any CPU.Build.0 = Release|Any CPU - {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Release|x64.ActiveCfg = Release|x64 - {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Release|x64.Build.0 = Release|x64 - {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Release|x86.ActiveCfg = Release|x86 - {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Release|x86.Build.0 = Release|x86 + {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Release|x64.ActiveCfg = Release|Any CPU + {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Release|x64.Build.0 = Release|Any CPU + {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Release|x86.ActiveCfg = Release|Any CPU + {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Release|x86.Build.0 = Release|Any CPU {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Debug|x64.ActiveCfg = Debug|x64 - {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Debug|x64.Build.0 = Debug|x64 - {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Debug|x86.ActiveCfg = Debug|x86 - {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Debug|x86.Build.0 = Debug|x86 + {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Debug|x64.ActiveCfg = Debug|Any CPU + {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Debug|x64.Build.0 = Debug|Any CPU + {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Debug|x86.ActiveCfg = Debug|Any CPU + {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Debug|x86.Build.0 = Debug|Any CPU {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Release|Any CPU.ActiveCfg = Release|Any CPU {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Release|Any CPU.Build.0 = Release|Any CPU - {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Release|x64.ActiveCfg = Release|x64 - {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Release|x64.Build.0 = Release|x64 - {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Release|x86.ActiveCfg = Release|x86 - {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Release|x86.Build.0 = Release|x86 + {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Release|x64.ActiveCfg = Release|Any CPU + {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Release|x64.Build.0 = Release|Any CPU + {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Release|x86.ActiveCfg = Release|Any CPU + {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Discord.Net.Rest/Extensions/EntityExtensions.cs b/src/Discord.Net.Rest/Extensions/EntityExtensions.cs index 7a9643674..f59b8f7a3 100644 --- a/src/Discord.Net.Rest/Extensions/EntityExtensions.cs +++ b/src/Discord.Net.Rest/Extensions/EntityExtensions.cs @@ -24,6 +24,7 @@ namespace Discord.Rest } public static API.Embed ToModel(this Embed entity) { + if (entity == null) return null; var model = new API.Embed { Type = entity.Type, From 3fb21e06c23dc1b86e3a6274f72358b5a59acb2b Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 18 Mar 2017 21:38:28 -0300 Subject: [PATCH 044/243] Fixed RunMode.Sync running Async. Added ThrowOnError option. --- src/Discord.Net.Commands/CommandService.cs | 3 ++- src/Discord.Net.Commands/CommandServiceConfig.cs | 6 ++++++ src/Discord.Net.Commands/Info/CommandInfo.cs | 5 +++-- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index e8db0100d..91472b9eb 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -24,7 +24,7 @@ namespace Discord.Commands private readonly HashSet _moduleDefs; private readonly CommandMap _map; - internal readonly bool _caseSensitive; + internal readonly bool _caseSensitive, _throwOnError; internal readonly char _separatorChar; internal readonly RunMode _defaultRunMode; internal readonly Logger _cmdLogger; @@ -38,6 +38,7 @@ namespace Discord.Commands public CommandService(CommandServiceConfig config) { _caseSensitive = config.CaseSensitiveCommands; + _throwOnError = config.ThrowOnError; _separatorChar = config.SeparatorChar; _defaultRunMode = config.DefaultRunMode; if (_defaultRunMode == RunMode.Default) diff --git a/src/Discord.Net.Commands/CommandServiceConfig.cs b/src/Discord.Net.Commands/CommandServiceConfig.cs index a94e433f5..b0925e28d 100644 --- a/src/Discord.Net.Commands/CommandServiceConfig.cs +++ b/src/Discord.Net.Commands/CommandServiceConfig.cs @@ -11,5 +11,11 @@ /// Gets or sets the minimum log level severity that will be sent to the Log event. public LogSeverity LogLevel { get; set; } = LogSeverity.Info; + + /// + /// Gets or sets whether RunMode.Sync commands should push exceptions up to the caller. + /// If false or an RunMode.Async command, exceptions are only reported in the Log event. + /// + public bool ThrowOnError { get; set; } = true; } } diff --git a/src/Discord.Net.Commands/Info/CommandInfo.cs b/src/Discord.Net.Commands/Info/CommandInfo.cs index 26b6163ab..d0bf25a4b 100644 --- a/src/Discord.Net.Commands/Info/CommandInfo.cs +++ b/src/Discord.Net.Commands/Info/CommandInfo.cs @@ -140,7 +140,7 @@ namespace Discord.Commands switch (RunMode) { case RunMode.Sync: //Always sync - var t1 = ExecuteAsyncInternal(context, args, map); + await ExecuteAsyncInternal(context, args, map).ConfigureAwait(false); break; case RunMode.Async: //Always async var t2 = Task.Run(async () => @@ -168,7 +168,8 @@ namespace Discord.Commands { ex = new CommandException(this, context, ex); await Module.Service._cmdLogger.ErrorAsync(ex).ConfigureAwait(false); - throw; + if (Module.Service._throwOnError) + throw; } await Module.Service._cmdLogger.VerboseAsync($"Executed {GetLogText(context)}").ConfigureAwait(false); } From 0c8d6435192378d2c527f0b9ca4a7d90cfaaff5a Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 18 Mar 2017 23:22:39 -0300 Subject: [PATCH 045/243] Minor doc/exception edits --- src/Discord.Net.Commands/CommandServiceConfig.cs | 7 ++----- src/Discord.Net.Commands/Dependencies/DependencyMap.cs | 4 +--- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/Discord.Net.Commands/CommandServiceConfig.cs b/src/Discord.Net.Commands/CommandServiceConfig.cs index b0925e28d..5dcd50cd8 100644 --- a/src/Discord.Net.Commands/CommandServiceConfig.cs +++ b/src/Discord.Net.Commands/CommandServiceConfig.cs @@ -12,10 +12,7 @@ /// Gets or sets the minimum log level severity that will be sent to the Log event. public LogSeverity LogLevel { get; set; } = LogSeverity.Info; - /// - /// Gets or sets whether RunMode.Sync commands should push exceptions up to the caller. - /// If false or an RunMode.Async command, exceptions are only reported in the Log event. - /// + /// Gets or sets whether RunMode.Sync commands should push exceptions up to the caller. public bool ThrowOnError { get; set; } = true; } -} +} \ No newline at end of file diff --git a/src/Discord.Net.Commands/Dependencies/DependencyMap.cs b/src/Discord.Net.Commands/Dependencies/DependencyMap.cs index c24b9db91..5b4f44fb9 100644 --- a/src/Discord.Net.Commands/Dependencies/DependencyMap.cs +++ b/src/Discord.Net.Commands/Dependencies/DependencyMap.cs @@ -50,9 +50,7 @@ namespace Discord.Commands public bool TryAddFactory(Func factory) where T : class { var type = typeof(T); - if (_typeBlacklist.Contains(type)) - throw new InvalidOperationException($"{type.FullName} is used internally and cannot be added as a dependency"); - if (map.ContainsKey(type)) + if (_typeBlacklist.Contains(type) || map.ContainsKey(type)) return false; map.Add(type, factory); return true; From 8d435e994b156085ca000120a2f0c61d634f4852 Mon Sep 17 00:00:00 2001 From: RogueException Date: Mon, 20 Mar 2017 23:46:11 -0300 Subject: [PATCH 046/243] Filter null roles in SocketGuildUser --- src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs index e92ebb0b1..387a96f0a 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs @@ -34,7 +34,8 @@ namespace Discord.WebSocket public bool IsDeafened => VoiceState?.IsDeafened ?? false; public bool IsMuted => VoiceState?.IsMuted ?? false; public DateTimeOffset? JoinedAt => DateTimeUtils.FromTicks(_joinedAtTicks); - public IEnumerable Roles => _roleIds.Select(id => Guild.GetRole(id)).ToReadOnlyCollection(() => _roleIds.Count()); + public IEnumerable Roles + => _roleIds.Select(id => Guild.GetRole(id)).Where(x => x != null).ToReadOnlyCollection(() => _roleIds.Length); public SocketVoiceChannel VoiceChannel => VoiceState?.VoiceChannel; public string VoiceSessionId => VoiceState?.VoiceSessionId ?? ""; public SocketVoiceState? VoiceState => Guild.GetVoiceState(Id); From 20f7ba431fc3c6c02bf35c6c7381b33aac4e053c Mon Sep 17 00:00:00 2001 From: RogueException Date: Mon, 20 Mar 2017 23:48:41 -0300 Subject: [PATCH 047/243] Cleaned up and fixed several reorder issues. --- .../Channels/BulkGuildChannelProperties.cs | 20 ------------------- .../Channels/ReorderChannelProperties.cs | 16 +++++++++++++++ .../Entities/Guilds/IGuild.cs | 4 ++-- .../Entities/Roles/BulkRoleProperties.cs | 15 -------------- .../Entities/Roles/ReorderRoleProperties.cs | 16 +++++++++++++++ .../API/Rest/ModifyGuildChannelsParams.cs | 4 ++-- .../API/Rest/ModifyGuildRoleParams.cs | 2 -- .../API/Rest/ModifyGuildRolesParams.cs | 5 ++++- src/Discord.Net.Rest/DiscordRestApiClient.cs | 15 ++------------ .../Entities/Guilds/GuildHelper.cs | 17 +++++----------- .../Entities/Guilds/RestGuild.cs | 8 ++++---- .../Entities/Roles/RoleHelper.cs | 14 ++++++++++--- .../Entities/Guilds/SocketGuild.cs | 8 ++++---- 13 files changed, 66 insertions(+), 78 deletions(-) delete mode 100644 src/Discord.Net.Core/Entities/Channels/BulkGuildChannelProperties.cs create mode 100644 src/Discord.Net.Core/Entities/Channels/ReorderChannelProperties.cs delete mode 100644 src/Discord.Net.Core/Entities/Roles/BulkRoleProperties.cs create mode 100644 src/Discord.Net.Core/Entities/Roles/ReorderRoleProperties.cs diff --git a/src/Discord.Net.Core/Entities/Channels/BulkGuildChannelProperties.cs b/src/Discord.Net.Core/Entities/Channels/BulkGuildChannelProperties.cs deleted file mode 100644 index 2358b2e2e..000000000 --- a/src/Discord.Net.Core/Entities/Channels/BulkGuildChannelProperties.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Discord -{ - public class BulkGuildChannelProperties - { - /// - /// The id of the channel to apply this position to. - /// - public ulong Id { get; set; } - /// - /// The new zero-based position of this channel. - /// - public int Position { get; set; } - - public BulkGuildChannelProperties(ulong id, int position) - { - Id = id; - Position = position; - } - } -} diff --git a/src/Discord.Net.Core/Entities/Channels/ReorderChannelProperties.cs b/src/Discord.Net.Core/Entities/Channels/ReorderChannelProperties.cs new file mode 100644 index 000000000..31f814334 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Channels/ReorderChannelProperties.cs @@ -0,0 +1,16 @@ +namespace Discord +{ + public class ReorderChannelProperties + { + /// The id of the channel to apply this position to. + public ulong Id { get; } + /// The new zero-based position of this channel. + public int Position { get; } + + public ReorderChannelProperties(ulong id, int position) + { + Id = id; + Position = position; + } + } +} diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs index 2ce9b48d0..b3367fab5 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs @@ -56,9 +56,9 @@ namespace Discord /// Modifies this guild's embed. Task ModifyEmbedAsync(Action func, RequestOptions options = null); /// Bulk modifies the channels of this guild. - Task ModifyChannelsAsync(IEnumerable args, RequestOptions options = null); + Task ReorderChannelsAsync(IEnumerable args, RequestOptions options = null); /// Bulk modifies the roles of this guild. - Task ModifyRolesAsync(IEnumerable args, RequestOptions options = null); + Task ReorderRolesAsync(IEnumerable args, RequestOptions options = null); /// Leaves this guild. If you are the owner, use Delete instead. Task LeaveAsync(RequestOptions options = null); diff --git a/src/Discord.Net.Core/Entities/Roles/BulkRoleProperties.cs b/src/Discord.Net.Core/Entities/Roles/BulkRoleProperties.cs deleted file mode 100644 index eacb6689d..000000000 --- a/src/Discord.Net.Core/Entities/Roles/BulkRoleProperties.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Discord -{ - public class BulkRoleProperties : RoleProperties - { - /// - /// The id of the role to be edited - /// - public ulong Id { get; } - - public BulkRoleProperties(ulong id) - { - Id = id; - } - } -} diff --git a/src/Discord.Net.Core/Entities/Roles/ReorderRoleProperties.cs b/src/Discord.Net.Core/Entities/Roles/ReorderRoleProperties.cs new file mode 100644 index 000000000..0c8afa24c --- /dev/null +++ b/src/Discord.Net.Core/Entities/Roles/ReorderRoleProperties.cs @@ -0,0 +1,16 @@ +namespace Discord +{ + public class ReorderRoleProperties + { + /// The id of the role to be edited + public ulong Id { get; } + /// The new zero-based position of the role. + public int Position { get; } + + public ReorderRoleProperties(ulong id, int pos) + { + Id = id; + Position = pos; + } + } +} diff --git a/src/Discord.Net.Rest/API/Rest/ModifyGuildChannelsParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyGuildChannelsParams.cs index 2bbb58ea6..f97fbda0b 100644 --- a/src/Discord.Net.Rest/API/Rest/ModifyGuildChannelsParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyGuildChannelsParams.cs @@ -7,9 +7,9 @@ namespace Discord.API.Rest internal class ModifyGuildChannelsParams { [JsonProperty("id")] - public ulong Id { get; set; } + public ulong Id { get; } [JsonProperty("position")] - public int Position { get; set; } + public int Position { get; } public ModifyGuildChannelsParams(ulong id, int position) { diff --git a/src/Discord.Net.Rest/API/Rest/ModifyGuildRoleParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyGuildRoleParams.cs index c3c20706b..287e1cafe 100644 --- a/src/Discord.Net.Rest/API/Rest/ModifyGuildRoleParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyGuildRoleParams.cs @@ -10,8 +10,6 @@ namespace Discord.API.Rest public Optional Name { get; set; } [JsonProperty("permissions")] public Optional Permissions { get; set; } - [JsonProperty("position")] - public Optional Position { get; set; } [JsonProperty("color")] public Optional Color { get; set; } [JsonProperty("hoist")] diff --git a/src/Discord.Net.Rest/API/Rest/ModifyGuildRolesParams.cs b/src/Discord.Net.Rest/API/Rest/ModifyGuildRolesParams.cs index 38c3fb646..0e816a260 100644 --- a/src/Discord.Net.Rest/API/Rest/ModifyGuildRolesParams.cs +++ b/src/Discord.Net.Rest/API/Rest/ModifyGuildRolesParams.cs @@ -8,10 +8,13 @@ namespace Discord.API.Rest { [JsonProperty("id")] public ulong Id { get; } + [JsonProperty("position")] + public int Position { get; } - public ModifyGuildRolesParams(ulong id) + public ModifyGuildRolesParams(ulong id, int position) { Id = id; + Position = position; } } } diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index 2e404003f..afa4af9e6 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -8,7 +8,6 @@ using Newtonsoft.Json; using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Collections.Immutable; using System.Diagnostics; using System.Globalization; using System.IO; @@ -1049,7 +1048,6 @@ namespace Discord.API Preconditions.NotNull(args, nameof(args)); Preconditions.AtLeast(args.Color, 0, nameof(args.Color)); Preconditions.NotNullOrEmpty(args.Name, nameof(args.Name)); - Preconditions.AtLeast(args.Position, 0, nameof(args.Position)); options = RequestOptions.CreateOrClone(options); var ids = new BucketIds(guildId: guildId); @@ -1061,17 +1059,8 @@ namespace Discord.API Preconditions.NotNull(args, nameof(args)); options = RequestOptions.CreateOrClone(options); - var roles = args.ToImmutableArray(); - switch (roles.Length) - { - case 0: - return ImmutableArray.Create(); - case 1: - return ImmutableArray.Create(await ModifyGuildRoleAsync(guildId, roles[0].Id, roles[0]).ConfigureAwait(false)); - default: - var ids = new BucketIds(guildId: guildId); - return await SendJsonAsync>("PATCH", () => $"guilds/{guildId}/roles", args, ids, options: options).ConfigureAwait(false); - } + var ids = new BucketIds(guildId: guildId); + return await SendJsonAsync>("PATCH", () => $"guilds/{guildId}/roles", args, ids, options: options).ConfigureAwait(false); } //Users diff --git a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs index 195ae27d0..98303cea6 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs @@ -75,23 +75,16 @@ namespace Discord.Rest return await client.ApiClient.ModifyGuildEmbedAsync(guild.Id, apiArgs, options).ConfigureAwait(false); } - public static async Task ModifyChannelsAsync(IGuild guild, BaseDiscordClient client, - IEnumerable args, RequestOptions options) + public static async Task ReorderChannelsAsync(IGuild guild, BaseDiscordClient client, + IEnumerable args, RequestOptions options) { var apiArgs = args.Select(x => new API.Rest.ModifyGuildChannelsParams(x.Id, x.Position)); await client.ApiClient.ModifyGuildChannelsAsync(guild.Id, apiArgs, options).ConfigureAwait(false); } - public static async Task> ModifyRolesAsync(IGuild guild, BaseDiscordClient client, - IEnumerable args, RequestOptions options) + public static async Task> ReorderRolesAsync(IGuild guild, BaseDiscordClient client, + IEnumerable args, RequestOptions options) { - var apiArgs = args.Select(x => new API.Rest.ModifyGuildRolesParams(x.Id) - { - Color = x.Color.IsSpecified ? x.Color.Value.RawValue : Optional.Create(), - Hoist = x.Hoist, - Name = x.Name, - Permissions = x.Permissions.IsSpecified ? x.Permissions.Value.RawValue : Optional.Create(), - Position = x.Position - }); + var apiArgs = args.Select(x => new API.Rest.ModifyGuildRolesParams(x.Id, x.Position)); return await client.ApiClient.ModifyGuildRolesAsync(guild.Id, apiArgs, options).ConfigureAwait(false); } public static async Task LeaveAsync(IGuild guild, BaseDiscordClient client, diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs index 0622df6ce..e4e970487 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -114,14 +114,14 @@ namespace Discord.Rest var model = await GuildHelper.ModifyEmbedAsync(this, Discord, func, options).ConfigureAwait(false); Update(model); } - public async Task ModifyChannelsAsync(IEnumerable args, RequestOptions options = null) + public async Task ReorderChannelsAsync(IEnumerable args, RequestOptions options = null) { var arr = args.ToArray(); - await GuildHelper.ModifyChannelsAsync(this, Discord, arr, options); + await GuildHelper.ReorderChannelsAsync(this, Discord, arr, options); } - public async Task ModifyRolesAsync(IEnumerable args, RequestOptions options = null) + public async Task ReorderRolesAsync(IEnumerable args, RequestOptions options = null) { - var models = await GuildHelper.ModifyRolesAsync(this, Discord, args, options).ConfigureAwait(false); + var models = await GuildHelper.ReorderRolesAsync(this, Discord, args, options).ConfigureAwait(false); foreach (var model in models) { var role = GetRole(model.Id); diff --git a/src/Discord.Net.Rest/Entities/Roles/RoleHelper.cs b/src/Discord.Net.Rest/Entities/Roles/RoleHelper.cs index 0081351f0..d570f078b 100644 --- a/src/Discord.Net.Rest/Entities/Roles/RoleHelper.cs +++ b/src/Discord.Net.Rest/Entities/Roles/RoleHelper.cs @@ -1,6 +1,7 @@ using System; using System.Threading.Tasks; using Model = Discord.API.Role; +using BulkParams = Discord.API.Rest.ModifyGuildRolesParams; namespace Discord.Rest { @@ -23,10 +24,17 @@ namespace Discord.Rest Hoist = args.Hoist, Mentionable = args.Mentionable, Name = args.Name, - Permissions = args.Permissions.IsSpecified ? args.Permissions.Value.RawValue : Optional.Create(), - Position = args.Position + Permissions = args.Permissions.IsSpecified ? args.Permissions.Value.RawValue : Optional.Create() }; - return await client.ApiClient.ModifyGuildRoleAsync(role.Guild.Id, role.Id, apiArgs, options).ConfigureAwait(false); + var model = await client.ApiClient.ModifyGuildRoleAsync(role.Guild.Id, role.Id, apiArgs, options).ConfigureAwait(false); + + if (args.Position.IsSpecified) + { + var bulkArgs = new[] { new BulkParams(role.Id, args.Position.Value) }; + await client.ApiClient.ModifyGuildRolesAsync(role.Guild.Id, bulkArgs, options).ConfigureAwait(false); + model.Position = args.Position.Value; + } + return model; } } } diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index be63f4da2..fb5d785c9 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -277,10 +277,10 @@ namespace Discord.WebSocket => GuildHelper.ModifyAsync(this, Discord, func, options); public Task ModifyEmbedAsync(Action func, RequestOptions options = null) => GuildHelper.ModifyEmbedAsync(this, Discord, func, options); - public Task ModifyChannelsAsync(IEnumerable args, RequestOptions options = null) - => GuildHelper.ModifyChannelsAsync(this, Discord, args, options); - public Task ModifyRolesAsync(IEnumerable args, RequestOptions options = null) - => GuildHelper.ModifyRolesAsync(this, Discord, args, options); + public Task ReorderChannelsAsync(IEnumerable args, RequestOptions options = null) + => GuildHelper.ReorderChannelsAsync(this, Discord, args, options); + public Task ReorderRolesAsync(IEnumerable args, RequestOptions options = null) + => GuildHelper.ReorderRolesAsync(this, Discord, args, options); public Task LeaveAsync(RequestOptions options = null) => GuildHelper.LeaveAsync(this, Discord, options); From 3a45e9ec87da6dccd021b44170a1df2aeabebf3d Mon Sep 17 00:00:00 2001 From: RogueException Date: Mon, 20 Mar 2017 23:49:17 -0300 Subject: [PATCH 048/243] Support InvalidSession(true) --- src/Discord.Net.WebSocket/ConnectionManager.cs | 6 ++++++ src/Discord.Net.WebSocket/DiscordSocketClient.cs | 6 +++++- test/Discord.Net.Tests/Net/CachedRestClient.cs | 2 +- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net.WebSocket/ConnectionManager.cs b/src/Discord.Net.WebSocket/ConnectionManager.cs index 72926e2e3..8e40beab4 100644 --- a/src/Discord.Net.WebSocket/ConnectionManager.cs +++ b/src/Discord.Net.WebSocket/ConnectionManager.cs @@ -193,6 +193,12 @@ namespace Discord _reconnectCancelToken?.Cancel(); Error(ex); } + public void Reconnect() + { + _readyPromise.TrySetCanceled(); + _connectionPromise.TrySetCanceled(); + _connectionCancelToken?.Cancel(); + } private async Task AcquireConnectionLock() { while (true) diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index 4f2f70321..9fb172612 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -433,7 +433,11 @@ namespace Discord.WebSocket _sessionId = null; _lastSeq = 0; - await ApiClient.SendIdentifyAsync(shardID: ShardId, totalShards: TotalShards).ConfigureAwait(false); + bool retry = (bool)payload; + if (retry) + _connection.Reconnect(); //TODO: Untested + else + await ApiClient.SendIdentifyAsync(shardID: ShardId, totalShards: TotalShards).ConfigureAwait(false); } break; case GatewayOpCode.Reconnect: diff --git a/test/Discord.Net.Tests/Net/CachedRestClient.cs b/test/Discord.Net.Tests/Net/CachedRestClient.cs index 324510688..f4b3bb279 100644 --- a/test/Discord.Net.Tests/Net/CachedRestClient.cs +++ b/test/Discord.Net.Tests/Net/CachedRestClient.cs @@ -5,11 +5,11 @@ using System; using System.Collections.Generic; using System.IO; using System.Net; +using System.Reactive.Concurrency; using System.Reactive.Linq; using System.Threading; using System.Threading.Tasks; using Splat; -using System.Reactive.Concurrency; namespace Discord.Net { From 22e6b0f386e82e6f65571a76cd2f18ea6b1b761c Mon Sep 17 00:00:00 2001 From: RogueException Date: Wed, 22 Mar 2017 04:34:17 -0300 Subject: [PATCH 049/243] Fixed RpcChannelSummary accessibilities --- src/Discord.Net.Rpc/Entities/Channels/RpcChannelSummary.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcChannelSummary.cs b/src/Discord.Net.Rpc/Entities/Channels/RpcChannelSummary.cs index 72679ac58..c35437e26 100644 --- a/src/Discord.Net.Rpc/Entities/Channels/RpcChannelSummary.cs +++ b/src/Discord.Net.Rpc/Entities/Channels/RpcChannelSummary.cs @@ -7,8 +7,8 @@ namespace Discord.Rpc public class RpcChannelSummary { public ulong Id { get; } - public string Name { get; set; } - public ChannelType Type { get; set; } + public string Name { get; private set; } + public ChannelType Type { get; private set; } internal RpcChannelSummary(ulong id) { From 32cf7ba5e1ce8e17d542c10fa2a0e272e1c3a892 Mon Sep 17 00:00:00 2001 From: RogueException Date: Wed, 22 Mar 2017 05:22:11 -0300 Subject: [PATCH 050/243] A few datetime fixes --- src/Discord.Net.Core/Utils/DateTimeUtils.cs | 2 +- src/Discord.Net.Core/Utils/Preconditions.cs | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Discord.Net.Core/Utils/DateTimeUtils.cs b/src/Discord.Net.Core/Utils/DateTimeUtils.cs index fc9ef4b7b..99cc9387f 100644 --- a/src/Discord.Net.Core/Utils/DateTimeUtils.cs +++ b/src/Discord.Net.Core/Utils/DateTimeUtils.cs @@ -13,7 +13,7 @@ namespace Discord public static DateTimeOffset FromSnowflake(ulong value) => FromUnixMilliseconds((long)((value >> 22) + 1420070400000UL)); public static ulong ToSnowflake(DateTimeOffset value) - => (ulong)(ToUnixMilliseconds(value) - 1420070400000L) << 22; + => ((ulong)ToUnixMilliseconds(value) - 1420070400000UL) << 22; public static DateTimeOffset FromTicks(long ticks) => new DateTimeOffset(ticks, TimeSpan.Zero); diff --git a/src/Discord.Net.Core/Utils/Preconditions.cs b/src/Discord.Net.Core/Utils/Preconditions.cs index 16046c2a9..faa35e653 100644 --- a/src/Discord.Net.Core/Utils/Preconditions.cs +++ b/src/Discord.Net.Core/Utils/Preconditions.cs @@ -185,9 +185,12 @@ namespace Discord // Bulk Delete public static void YoungerThanTwoWeeks(ulong[] collection, string name) { - var minimum = DateTimeUtils.ToSnowflake(DateTimeOffset.Now.Subtract(TimeSpan.FromMilliseconds(1209540000))); + var minimum = DateTimeUtils.ToSnowflake(DateTimeOffset.UtcNow.Subtract(TimeSpan.FromDays(14))); for (var i = 0; i < collection.Length; i++) - if (collection[i] <= minimum) throw new ArgumentOutOfRangeException(name, "Messages must be younger than two weeks to delete."); + { + if (collection[i] <= minimum) + throw new ArgumentOutOfRangeException(name, "Messages must be younger than two weeks old."); + } } } } From 13b9b15cf085b4b68324bb0b2f8d7ad6fd6ab2e8 Mon Sep 17 00:00:00 2001 From: RogueException Date: Wed, 22 Mar 2017 05:35:19 -0300 Subject: [PATCH 051/243] Fixed DateTimeUtils on .Net Standard 1.1 --- src/Discord.Net.Core/Utils/DateTimeUtils.cs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/Discord.Net.Core/Utils/DateTimeUtils.cs b/src/Discord.Net.Core/Utils/DateTimeUtils.cs index 99cc9387f..ab285b56d 100644 --- a/src/Discord.Net.Core/Utils/DateTimeUtils.cs +++ b/src/Discord.Net.Core/Utils/DateTimeUtils.cs @@ -2,12 +2,13 @@ namespace Discord { + //Source: https://github.com/dotnet/coreclr/blob/master/src/mscorlib/src/System/DateTimeOffset.cs internal static class DateTimeUtils { #if !NETSTANDARD1_3 - //https://github.com/dotnet/coreclr/blob/master/src/mscorlib/src/System/DateTimeOffset.cs - private const long UnixEpochTicks = 621355968000000000; - private const long UnixEpochSeconds = 62135596800; + private const long UnixEpochTicks = 621_355_968_000_000_000; + private const long UnixEpochSeconds = 62_135_596_800; + private const long UnixEpochMilliseconds = 62_135_596_800_000; #endif public static DateTimeOffset FromSnowflake(ulong value) @@ -29,12 +30,12 @@ namespace Discord return new DateTimeOffset(ticks, TimeSpan.Zero); #endif } - public static DateTimeOffset FromUnixMilliseconds(long seconds) + public static DateTimeOffset FromUnixMilliseconds(long milliseconds) { #if NETSTANDARD1_3 - return DateTimeOffset.FromUnixTimeMilliseconds(seconds); + return DateTimeOffset.FromUnixTimeMilliseconds(milliseconds); #else - long ticks = seconds * TimeSpan.TicksPerMillisecond + UnixEpochTicks; + long ticks = milliseconds * TimeSpan.TicksPerMillisecond + UnixEpochTicks; return new DateTimeOffset(ticks, TimeSpan.Zero); #endif } @@ -53,8 +54,8 @@ namespace Discord #if NETSTANDARD1_3 return dto.ToUnixTimeMilliseconds(); #else - long seconds = dto.UtcDateTime.Ticks / TimeSpan.TicksPerMillisecond; - return seconds - UnixEpochSeconds; + long milliseconds = dto.UtcDateTime.Ticks / TimeSpan.TicksPerMillisecond; + return milliseconds - UnixEpochMilliseconds; #endif } } From 35d7a0cec89398a69fd5d3472fe6980f327f5d00 Mon Sep 17 00:00:00 2001 From: RogueException Date: Wed, 22 Mar 2017 06:08:58 -0300 Subject: [PATCH 052/243] Ensure UploadFile is always a seekable stream. --- src/Discord.Net.Rest/DiscordRestApiClient.cs | 9 ++------- src/Discord.Net.Rest/Net/DefaultRestClient.cs | 9 ++++++++- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index afa4af9e6..09f5a5767 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -494,13 +494,8 @@ namespace Discord.API if (args.Content.GetValueOrDefault(null) == null) args.Content = ""; - else if (args.Content.IsSpecified) - { - if (args.Content.Value == null) - 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)); - } + else 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)); var ids = new BucketIds(channelId: channelId); return await SendMultipartAsync("POST", () => $"channels/{channelId}/messages", args.ToDictionary(), ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); diff --git a/src/Discord.Net.Rest/Net/DefaultRestClient.cs b/src/Discord.Net.Rest/Net/DefaultRestClient.cs index 39b94294f..2381a9976 100644 --- a/src/Discord.Net.Rest/Net/DefaultRestClient.cs +++ b/src/Discord.Net.Rest/Net/DefaultRestClient.cs @@ -101,7 +101,14 @@ namespace Discord.Net.Rest if (p.Value is MultipartFile) { var fileValue = (MultipartFile)p.Value; - content.Add(new StreamContent(fileValue.Stream), p.Key, fileValue.Filename); + var stream = fileValue.Stream; + if (!stream.CanSeek) + { + var memoryStream = new MemoryStream(); + await stream.CopyToAsync(memoryStream).ConfigureAwait(false); + stream = memoryStream; + } + content.Add(new StreamContent(stream), p.Key, fileValue.Filename); continue; } From ca18eb0eb4a2c68d1ffd608bc80039dddd2bdc60 Mon Sep 17 00:00:00 2001 From: Finite Reality Date: Thu, 23 Mar 2017 14:03:06 +0000 Subject: [PATCH 053/243] Replace TryGetValue call with TryRemove call (#586) Resolves #584 --- src/Discord.Net.Commands/CommandService.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index 91472b9eb..c0c20f80f 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -159,8 +159,7 @@ namespace Discord.Commands try { ModuleInfo module; - _typedModuleDefs.TryGetValue(typeof(T), out module); - if (module == default(ModuleInfo)) + if (!_typedModuleDefs.TryRemove(typeof(T), out module)) return false; return RemoveModuleInternal(module); From df129dd76687d08b73c029dbefcb2aa6fbb4a83c Mon Sep 17 00:00:00 2001 From: RogueException Date: Thu, 23 Mar 2017 11:38:03 -0300 Subject: [PATCH 054/243] Actually populate SocketSimpleUser's fields --- .../Entities/Users/SocketSimpleUser.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketSimpleUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketSimpleUser.cs index 1ecb5e578..0d2198888 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketSimpleUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketSimpleUser.cs @@ -29,6 +29,14 @@ namespace Discord.WebSocket internal override void Update(ClientState state, PresenceModel model) { + if (model.User.Avatar.IsSpecified) + AvatarId = model.User.Avatar.Value; + if (model.User.Discriminator.IsSpecified) + DiscriminatorValue = ushort.Parse(model.User.Discriminator.Value); + if (model.User.Bot.IsSpecified) + IsBot = model.User.Bot.Value; + if (model.User.Username.IsSpecified) + Username = model.User.Username.Value; } internal new SocketSimpleUser Clone() => MemberwiseClone() as SocketSimpleUser; From bbd45a6f4b5908c514d40bffa1c1fa126909c4db Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 25 Mar 2017 11:19:31 -0300 Subject: [PATCH 055/243] Fixed CommandException --- src/Discord.Net.Commands/CommandException.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net.Commands/CommandException.cs b/src/Discord.Net.Commands/CommandException.cs index 9a582d17b..d5300841a 100644 --- a/src/Discord.Net.Commands/CommandException.cs +++ b/src/Discord.Net.Commands/CommandException.cs @@ -5,11 +5,13 @@ namespace Discord.Commands public class CommandException : Exception { public CommandInfo Command { get; } - public ICommandContext Content { get; } + public ICommandContext Context { get; } public CommandException(CommandInfo command, ICommandContext context, Exception ex) : base($"Error occurred executing {command.GetLogText(context)}.", ex) { + Command = command; + Context = context; } } } From 2b4a1249f4a92055d1bd6c093e214633cea70e07 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Sat, 25 Mar 2017 17:08:45 -0400 Subject: [PATCH 056/243] Restrict DependencyMap#Get to reference types It's impossible to add non-reference types to the map, so why allow pulling them out of it. --- src/Discord.Net.Commands/Dependencies/DependencyMap.cs | 4 ++-- src/Discord.Net.Commands/Dependencies/IDependencyMap.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Discord.Net.Commands/Dependencies/DependencyMap.cs b/src/Discord.Net.Commands/Dependencies/DependencyMap.cs index 5b4f44fb9..55092961a 100644 --- a/src/Discord.Net.Commands/Dependencies/DependencyMap.cs +++ b/src/Discord.Net.Commands/Dependencies/DependencyMap.cs @@ -57,7 +57,7 @@ namespace Discord.Commands } /// - public T Get() + public T Get() where T : class { return (T)Get(typeof(T)); } @@ -72,7 +72,7 @@ namespace Discord.Commands } /// - public bool TryGet(out T result) + public bool TryGet(out T result) where T : class { object untypedResult; if (TryGet(typeof(T), out untypedResult)) diff --git a/src/Discord.Net.Commands/Dependencies/IDependencyMap.cs b/src/Discord.Net.Commands/Dependencies/IDependencyMap.cs index a55a9e4c5..fa76709b6 100644 --- a/src/Discord.Net.Commands/Dependencies/IDependencyMap.cs +++ b/src/Discord.Net.Commands/Dependencies/IDependencyMap.cs @@ -63,14 +63,14 @@ namespace Discord.Commands /// /// The type of service. /// An instance of this service. - T Get(); + T Get() where T : class; /// /// Try to pull an object from the map. /// /// The type of service. /// The instance of this service. /// Whether or not this object could be found in the map. - bool TryGet(out T result); + bool TryGet(out T result) where T : class; /// /// Pull an object from the map. From e7401eda68d8c946c2f6f8f872829b9d5a55540c Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 25 Mar 2017 19:25:00 -0300 Subject: [PATCH 057/243] Fixed internal nullref on voicestate change --- .../Entities/Guilds/SocketGuild.cs | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index fb5d785c9..7de5eda77 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -430,13 +430,13 @@ namespace Discord.WebSocket var after = SocketVoiceState.Create(voiceChannel, model); _voiceStates[model.UserId] = after; - if (before.VoiceChannel?.Id != after.VoiceChannel?.Id) + if (_audioClient != null && before.VoiceChannel?.Id != after.VoiceChannel?.Id) { if (model.UserId == CurrentUser.Id) RepopulateAudioStreams(); else { - _audioClient?.RemoveInputStream(model.UserId); //User changed channels, end their stream + _audioClient.RemoveInputStream(model.UserId); //User changed channels, end their stream if (CurrentUser.VoiceChannel != null && after.VoiceChannel?.Id == CurrentUser.VoiceChannel?.Id) _audioClient.CreateInputStream(model.UserId); } @@ -575,16 +575,13 @@ namespace Discord.WebSocket internal void RepopulateAudioStreams() { - if (_audioClient != null) + _audioClient.ClearInputStreams(); //We changed channels, end all current streams + if (CurrentUser.VoiceChannel != null) { - _audioClient.ClearInputStreams(); //We changed channels, end all current streams - if (CurrentUser.VoiceChannel != null) + foreach (var pair in _voiceStates) { - foreach (var pair in _voiceStates) - { - if (pair.Value.VoiceChannel?.Id == CurrentUser.VoiceChannel?.Id) - _audioClient.CreateInputStream(pair.Key); - } + if (pair.Value.VoiceChannel?.Id == CurrentUser.VoiceChannel?.Id) + _audioClient.CreateInputStream(pair.Key); } } } From bf0be82d1516e8f344b7cc838386e4faa831316f Mon Sep 17 00:00:00 2001 From: RogueException Date: Thu, 30 Mar 2017 23:29:30 -0300 Subject: [PATCH 058/243] Added IWebhookUser and MessageSource --- .../Entities/Messages/IMessage.cs | 6 +- .../Entities/Messages/MessageSource.cs | 10 ++ .../Permissions/ChannelPermissions.cs | 39 ++++---- .../Entities/Permissions/GuildPermissions.cs | 10 +- src/Discord.Net.Core/Entities/Users/IUser.cs | 4 +- .../Entities/Users/IWebhookUser.cs | 8 ++ src/Discord.Net.Core/Utils/AsyncEvent.cs | 7 +- src/Discord.Net.Core/Utils/Optional.cs | 4 + src/Discord.Net.Core/Utils/Permissions.cs | 4 + .../Entities/Channels/ChannelHelper.cs | 10 +- .../Entities/Messages/MessageHelper.cs | 13 ++- .../Entities/Messages/RestMessage.cs | 6 +- .../Entities/Messages/RestSystemMessage.cs | 2 +- .../Entities/Messages/RestUserMessage.cs | 12 +-- .../Entities/Users/RestUser.cs | 14 ++- .../Entities/Users/RestWebhookUser.cs | 83 ++++++++++++++++ .../Entities/Messages/RpcMessage.cs | 4 +- .../Entities/Messages/RpcSystemMessage.cs | 4 +- .../Entities/Messages/RpcUserMessage.cs | 7 +- src/Discord.Net.Rpc/Entities/Users/RpcUser.cs | 14 ++- .../Entities/Users/RpcWebhookUser.cs | 25 +++++ .../DiscordSocketClient.cs | 23 +++-- .../Entities/Messages/SocketMessage.cs | 6 +- .../Entities/Messages/SocketSystemMessage.cs | 2 +- .../Entities/Messages/SocketUserMessage.cs | 18 ++-- .../Entities/Users/SocketGlobalUser.cs | 3 +- .../Entities/Users/SocketGroupUser.cs | 2 + .../Entities/Users/SocketGuildUser.cs | 1 + .../Entities/Users/SocketSelfUser.cs | 2 + ...cketSimpleUser.cs => SocketUnknownUser.cs} | 18 ++-- .../Entities/Users/SocketUser.cs | 6 +- .../Entities/Users/SocketWebhookUser.cs | 98 +++++++++++++++++++ 32 files changed, 370 insertions(+), 95 deletions(-) create mode 100644 src/Discord.Net.Core/Entities/Messages/MessageSource.cs create mode 100644 src/Discord.Net.Core/Entities/Users/IWebhookUser.cs create mode 100644 src/Discord.Net.Rest/Entities/Users/RestWebhookUser.cs create mode 100644 src/Discord.Net.Rpc/Entities/Users/RpcWebhookUser.cs rename src/Discord.Net.WebSocket/Entities/Users/{SocketSimpleUser.cs => SocketUnknownUser.cs} (70%) create mode 100644 src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs diff --git a/src/Discord.Net.Core/Entities/Messages/IMessage.cs b/src/Discord.Net.Core/Entities/Messages/IMessage.cs index 6bb44368b..4266f893a 100644 --- a/src/Discord.Net.Core/Entities/Messages/IMessage.cs +++ b/src/Discord.Net.Core/Entities/Messages/IMessage.cs @@ -7,12 +7,12 @@ namespace Discord { /// Gets the type of this system message. MessageType Type { get; } + /// Gets the source of this message. + MessageSource Source { get; } /// Returns true if this message was sent as a text-to-speech message. bool IsTTS { get; } /// Returns true if this message was added to its channel's pinned messages. bool IsPinned { get; } - /// Returns true if this message was created using a webhook. - bool IsWebhook { get; } /// Returns the content for this message. string Content { get; } /// Gets the time this message was sent. @@ -24,8 +24,6 @@ namespace Discord IMessageChannel Channel { get; } /// Gets the author of this message. IUser Author { get; } - /// Gets the id of the webhook used to created this message, if any. - ulong? WebhookId { get; } /// Returns all attachments included in this message. IReadOnlyCollection Attachments { get; } diff --git a/src/Discord.Net.Core/Entities/Messages/MessageSource.cs b/src/Discord.Net.Core/Entities/Messages/MessageSource.cs new file mode 100644 index 000000000..1cb2f8b94 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Messages/MessageSource.cs @@ -0,0 +1,10 @@ +namespace Discord +{ + public enum MessageSource + { + System, + User, + Bot, + Webhook + } +} diff --git a/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs b/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs index 2824a1426..054b80119 100644 --- a/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs +++ b/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs @@ -7,22 +7,24 @@ namespace Discord [DebuggerDisplay("{DebuggerDisplay,nq}")] public struct ChannelPermissions { - //TODO: C#7 Candidate for binary literals - private static ChannelPermissions _allDM { get; } = new ChannelPermissions(Convert.ToUInt64("00000000000001011100110000000000", 2)); - private static ChannelPermissions _allVoice { get; } = new ChannelPermissions(Convert.ToUInt64("00010011111100000000000000010001", 2)); - private static ChannelPermissions _allText { get; } = new ChannelPermissions(Convert.ToUInt64("00010000000001111111110001010001", 2)); - private static ChannelPermissions _allGroup { get; } = new ChannelPermissions(Convert.ToUInt64("00000000000001111110110000000000", 2)); - /// Gets a blank ChannelPermissions that grants no permissions. - public static ChannelPermissions None { get; } = new ChannelPermissions(); + public static readonly ChannelPermissions None = new ChannelPermissions(); + /// Gets a ChannelPermissions that grants all permissions for text channels. + public static readonly ChannelPermissions Text = new ChannelPermissions(0b00100_0000000_1111111110001_010001); + /// Gets a ChannelPermissions that grants all permissions for voice channels. + public static readonly ChannelPermissions Voice = new ChannelPermissions(0b00100_1111110_0000000000000_010001); + /// Gets a ChannelPermissions that grants all permissions for direct message channels. + public static readonly ChannelPermissions DM = new ChannelPermissions(0b00000_1000110_1011100110000_000000); + /// Gets a ChannelPermissions that grants all permissions for group channels. + public static readonly ChannelPermissions Group = new ChannelPermissions(0b00000_1000110_0001101100000_000000); /// Gets a ChannelPermissions that grants all permissions for a given channelType. public static ChannelPermissions All(IChannel channel) { //TODO: C#7 Candidate for typeswitch - if (channel is ITextChannel) return _allText; - if (channel is IVoiceChannel) return _allVoice; - if (channel is IDMChannel) return _allDM; - if (channel is IGroupChannel) return _allGroup; + if (channel is ITextChannel) return Text; + if (channel is IVoiceChannel) return Voice; + if (channel is IDMChannel) return DM; + if (channel is IGroupChannel) return Group; throw new ArgumentException("Unknown channel type", nameof(channel)); } @@ -77,7 +79,7 @@ namespace Discord /// Creates a new ChannelPermissions with the provided packed value. public ChannelPermissions(ulong rawValue) { RawValue = rawValue; } - private ChannelPermissions(ulong initialValue, bool? createInstantInvite = null, bool? manageChannel = null, + private ChannelPermissions(ulong initialValue, bool? createInstantInvite = null, bool? manageChannel = null, bool? addReactions = null, bool? readMessages = null, bool? sendMessages = null, bool? sendTTSMessages = null, bool? manageMessages = null, bool? embedLinks = null, bool? attachFiles = null, bool? readMessageHistory = null, bool? mentionEveryone = null, @@ -111,25 +113,26 @@ namespace Discord } /// Creates a new ChannelPermissions with the provided permissions. - public ChannelPermissions(bool createInstantInvite = false, bool manageChannel = false, + public ChannelPermissions(bool createInstantInvite = false, bool manageChannel = false, bool addReactions = false, bool readMessages = false, bool sendMessages = false, bool sendTTSMessages = false, bool manageMessages = false, bool embedLinks = false, bool attachFiles = false, bool readMessageHistory = false, bool mentionEveryone = false, bool useExternalEmojis = false, bool connect = false, bool speak = false, bool muteMembers = false, bool deafenMembers = false, bool moveMembers = false, bool useVoiceActivation = false, bool managePermissions = false, bool manageWebhooks = false) - : this(0, createInstantInvite, manageChannel, addReactions, readMessages, sendMessages, sendTTSMessages, manageMessages, - embedLinks, attachFiles, readMessageHistory, mentionEveryone, useExternalEmojis, connect, - speak, muteMembers, deafenMembers, moveMembers, useVoiceActivation, managePermissions, manageWebhooks) { } + : this(0, createInstantInvite, manageChannel, addReactions, readMessages, sendMessages, sendTTSMessages, manageMessages, + embedLinks, attachFiles, readMessageHistory, mentionEveryone, useExternalEmojis, connect, + speak, muteMembers, deafenMembers, moveMembers, useVoiceActivation, managePermissions, manageWebhooks) + { } /// Creates a new ChannelPermissions from this one, changing the provided non-null permissions. - public ChannelPermissions Modify(bool? createInstantInvite = null, bool? manageChannel = null, + public ChannelPermissions Modify(bool? createInstantInvite = null, bool? manageChannel = null, bool? addReactions = null, bool? readMessages = null, bool? sendMessages = null, bool? sendTTSMessages = null, bool? manageMessages = null, bool? embedLinks = null, bool? attachFiles = null, bool? readMessageHistory = null, bool? mentionEveryone = null, bool useExternalEmojis = false, bool? connect = null, bool? speak = null, bool? muteMembers = null, bool? deafenMembers = null, bool? moveMembers = null, bool? useVoiceActivation = null, bool? managePermissions = null, bool? manageWebhooks = null) => new ChannelPermissions(RawValue, createInstantInvite, manageChannel, addReactions, readMessages, sendMessages, sendTTSMessages, manageMessages, - embedLinks, attachFiles, readMessageHistory, mentionEveryone, useExternalEmojis, connect, + embedLinks, attachFiles, readMessageHistory, mentionEveryone, useExternalEmojis, connect, speak, muteMembers, deafenMembers, moveMembers, useVoiceActivation, managePermissions, manageWebhooks); public bool Has(ChannelPermission permission) => Permissions.GetValue(RawValue, permission); diff --git a/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs b/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs index e7461915c..c5f1efab0 100644 --- a/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs +++ b/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Diagnostics; namespace Discord @@ -9,9 +8,10 @@ namespace Discord { /// Gets a blank GuildPermissions that grants no permissions. public static readonly GuildPermissions None = new GuildPermissions(); - /// Gets a GuildPermissions that grants all permissions. - //TODO: C#7 Candidate for binary literals - public static readonly GuildPermissions All = new GuildPermissions(Convert.ToUInt64("01111111111100111111110001111111", 2)); + /// Gets a GuildPermissions that grants all guild permissions for webhook users. + public static readonly GuildPermissions Webhook = new GuildPermissions(0b00000_0000000_0001101100000_000000); + /// Gets a GuildPermissions that grants all guild permissions. + public static readonly GuildPermissions All = new GuildPermissions(0b11111_1111110_0111111110001_111111); /// Gets a packed value representing all the permissions in this GuildPermissions. public ulong RawValue { get; } diff --git a/src/Discord.Net.Core/Entities/Users/IUser.cs b/src/Discord.Net.Core/Entities/Users/IUser.cs index 62060da22..45d8862f1 100644 --- a/src/Discord.Net.Core/Entities/Users/IUser.cs +++ b/src/Discord.Net.Core/Entities/Users/IUser.cs @@ -12,8 +12,10 @@ namespace Discord string Discriminator { get; } /// Gets the per-username unique id for this user. ushort DiscriminatorValue { get; } - /// Returns true if this user is a bot account. + /// Returns true if this user is a bot user. bool IsBot { get; } + /// Returns true if this user is a webhook user. + bool IsWebhook { get; } /// Gets the username for this user. string Username { get; } diff --git a/src/Discord.Net.Core/Entities/Users/IWebhookUser.cs b/src/Discord.Net.Core/Entities/Users/IWebhookUser.cs new file mode 100644 index 000000000..8f4d42187 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Users/IWebhookUser.cs @@ -0,0 +1,8 @@ +namespace Discord +{ + //TODO: Add webhook endpoints + public interface IWebhookUser : IGuildUser + { + ulong WebhookId { get; } + } +} diff --git a/src/Discord.Net.Core/Utils/AsyncEvent.cs b/src/Discord.Net.Core/Utils/AsyncEvent.cs index a7fdeddf2..12a1fba9c 100644 --- a/src/Discord.Net.Core/Utils/AsyncEvent.cs +++ b/src/Discord.Net.Core/Utils/AsyncEvent.cs @@ -37,11 +37,8 @@ namespace Discord public static async Task InvokeAsync(this AsyncEvent> eventHandler) { var subscribers = eventHandler.Subscriptions; - if (subscribers.Count > 0) - { - for (int i = 0; i < subscribers.Count; i++) - await subscribers[i].Invoke().ConfigureAwait(false); - } + for (int i = 0; i < subscribers.Count; i++) + await subscribers[i].Invoke().ConfigureAwait(false); } public static async Task InvokeAsync(this AsyncEvent> eventHandler, T arg) { diff --git a/src/Discord.Net.Core/Utils/Optional.cs b/src/Discord.Net.Core/Utils/Optional.cs index e2d55cf7f..df927b7ea 100644 --- a/src/Discord.Net.Core/Utils/Optional.cs +++ b/src/Discord.Net.Core/Utils/Optional.cs @@ -51,5 +51,9 @@ namespace Discord { public static Optional Create() => Optional.Unspecified; public static Optional Create(T value) => new Optional(value); + + public static T? ToNullable(this Optional val) + where T : struct + => val.IsSpecified ? val.Value : (T?)null; } } diff --git a/src/Discord.Net.Core/Utils/Permissions.cs b/src/Discord.Net.Core/Utils/Permissions.cs index a99c64094..b69b103e1 100644 --- a/src/Discord.Net.Core/Utils/Permissions.cs +++ b/src/Discord.Net.Core/Utils/Permissions.cs @@ -86,12 +86,16 @@ namespace Discord [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void UnsetBit(ref ulong value, byte bit) => value &= ~(1U << bit); + public static ChannelPermissions ToChannelPerms(IGuildChannel channel, ulong guildPermissions) + => new ChannelPermissions(guildPermissions & ChannelPermissions.All(channel).RawValue); public static ulong ResolveGuild(IGuild guild, IGuildUser user) { ulong resolvedPermissions = 0; if (user.Id == guild.OwnerId) resolvedPermissions = GuildPermissions.All.RawValue; //Owners always have all permissions + else if (user.IsWebhook) + resolvedPermissions = GuildPermissions.Webhook.RawValue; else { foreach (var roleId in user.RoleIds) diff --git a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs index 07bdfe0eb..efcadac0d 100644 --- a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs +++ b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs @@ -91,7 +91,7 @@ namespace Discord.Rest var guildId = (channel as IGuildChannel)?.GuildId; var guild = guildId != null ? await (client as IDiscordClient).GetGuildAsync(guildId.Value, CacheMode.CacheOnly).ConfigureAwait(false) : null; var model = await client.ApiClient.GetChannelMessageAsync(channel.Id, id, options).ConfigureAwait(false); - var author = GetAuthor(client, guild, model.Author.Value); + var author = GetAuthor(client, guild, model.Author.Value, model.WebhookId.ToNullable()); return RestMessage.Create(client, channel, author, model); } public static IAsyncEnumerable> GetMessagesAsync(IMessageChannel channel, BaseDiscordClient client, @@ -119,7 +119,7 @@ namespace Discord.Rest var builder = ImmutableArray.CreateBuilder(); foreach (var model in models) { - var author = GetAuthor(client, guild, model.Author.Value); + var author = GetAuthor(client, guild, model.Author.Value, model.WebhookId.ToNullable()); builder.Add(RestMessage.Create(client, channel, author, model)); } return builder.ToImmutable(); @@ -147,7 +147,7 @@ namespace Discord.Rest var builder = ImmutableArray.CreateBuilder(); foreach (var model in models) { - var author = GetAuthor(client, guild, model.Author.Value); + var author = GetAuthor(client, guild, model.Author.Value, model.WebhookId.ToNullable()); builder.Add(RestMessage.Create(client, channel, author, model)); } return builder.ToImmutable(); @@ -264,13 +264,13 @@ namespace Discord.Rest => new TypingNotifier(client, channel, options); //Helpers - private static IUser GetAuthor(BaseDiscordClient client, IGuild guild, UserModel model) + private static IUser GetAuthor(BaseDiscordClient client, IGuild guild, UserModel model, ulong? webhookId) { IUser author = null; if (guild != null) author = guild.GetUserAsync(model.Id, CacheMode.CacheOnly).Result; if (author == null) - author = RestUser.Create(client, model); + author = RestUser.Create(client, guild, model, webhookId); return author; } } diff --git a/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs b/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs index 2c6f67477..f347563ad 100644 --- a/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs +++ b/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs @@ -67,7 +67,7 @@ namespace Discord.Rest await client.ApiClient.RemovePinAsync(msg.Channel.Id, msg.Id, options).ConfigureAwait(false); } - public static ImmutableArray ParseTags(string text, IMessageChannel channel, IGuild guild, ImmutableArray userMentions) + public static ImmutableArray ParseTags(string text, IMessageChannel channel, IGuild guild, IReadOnlyCollection userMentions) { var tags = ImmutableArray.CreateBuilder(); @@ -156,5 +156,16 @@ namespace Discord.Rest .Where(x => x != null) .ToImmutableArray(); } + + public static MessageSource GetSource(Model msg) + { + if (msg.Type != MessageType.Default) + return MessageSource.System; + else if (msg.WebhookId.IsSpecified) + return MessageSource.Webhook; + else if (msg.Author.GetValueOrDefault()?.Bot.GetValueOrDefault(false) == true) + return MessageSource.Bot; + return MessageSource.User; + } } } diff --git a/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs b/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs index b50edf03b..bdd4800c1 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs @@ -13,6 +13,7 @@ namespace Discord.Rest public IMessageChannel Channel { get; } public IUser Author { get; } + public MessageSource Source { get; } public string Content { get; private set; } @@ -26,16 +27,15 @@ namespace Discord.Rest public virtual IReadOnlyCollection MentionedRoleIds => ImmutableArray.Create(); public virtual IReadOnlyCollection MentionedUsers => ImmutableArray.Create(); public virtual IReadOnlyCollection Tags => ImmutableArray.Create(); - public virtual ulong? WebhookId => null; - public bool IsWebhook => WebhookId != null; public DateTimeOffset Timestamp => DateTimeUtils.FromTicks(_timestampTicks); - internal RestMessage(BaseDiscordClient discord, ulong id, IMessageChannel channel, IUser author) + internal RestMessage(BaseDiscordClient discord, ulong id, IMessageChannel channel, IUser author, MessageSource source) : base(discord, id) { Channel = channel; Author = author; + Source = source; } internal static RestMessage Create(BaseDiscordClient discord, IMessageChannel channel, IUser author, Model model) { diff --git a/src/Discord.Net.Rest/Entities/Messages/RestSystemMessage.cs b/src/Discord.Net.Rest/Entities/Messages/RestSystemMessage.cs index a5ced8c8f..b9dda08ae 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestSystemMessage.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestSystemMessage.cs @@ -9,7 +9,7 @@ namespace Discord.Rest public MessageType Type { get; private set; } internal RestSystemMessage(BaseDiscordClient discord, ulong id, IMessageChannel channel, IUser author) - : base(discord, id, channel, author) + : base(discord, id, channel, author, MessageSource.System) { } internal new static RestSystemMessage Create(BaseDiscordClient discord, IMessageChannel channel, IUser author, Model model) diff --git a/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs b/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs index a9197188e..00ab0c299 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs @@ -13,7 +13,6 @@ namespace Discord.Rest { private bool _isMentioningEveryone, _isTTS, _isPinned; private long? _editedTimestampTicks; - private ulong? _webhookId; private ImmutableArray _attachments; private ImmutableArray _embeds; private ImmutableArray _tags; @@ -21,7 +20,6 @@ namespace Discord.Rest public override bool IsTTS => _isTTS; public override bool IsPinned => _isPinned; - public override ulong? WebhookId => _webhookId; public override DateTimeOffset? EditedTimestamp => DateTimeUtils.FromTicks(_editedTimestampTicks); public override IReadOnlyCollection Attachments => _attachments; public override IReadOnlyCollection Embeds => _embeds; @@ -31,13 +29,13 @@ namespace Discord.Rest public override IReadOnlyCollection Tags => _tags; public IReadOnlyDictionary Reactions => _reactions.ToDictionary(x => x.Emoji, x => new ReactionMetadata { ReactionCount = x.Count, IsMe = x.Me }); - internal RestUserMessage(BaseDiscordClient discord, ulong id, IMessageChannel channel, IUser author) - : base(discord, id, channel, author) + internal RestUserMessage(BaseDiscordClient discord, ulong id, IMessageChannel channel, IUser author, MessageSource source) + : base(discord, id, channel, author, source) { } - internal new static RestUserMessage Create(BaseDiscordClient discord, IMessageChannel channel, IUser author, Model model) + internal static new RestUserMessage Create(BaseDiscordClient discord, IMessageChannel channel, IUser author, Model model) { - var entity = new RestUserMessage(discord, model.Id, channel, author); + var entity = new RestUserMessage(discord, model.Id, channel, author, MessageHelper.GetSource(model)); entity.Update(model); return entity; } @@ -54,8 +52,6 @@ namespace Discord.Rest _editedTimestampTicks = model.EditedTimestamp.Value?.UtcTicks; if (model.MentionEveryone.IsSpecified) _isMentioningEveryone = model.MentionEveryone.Value; - if (model.WebhookId.IsSpecified) - _webhookId = model.WebhookId.Value; if (model.Attachments.IsSpecified) { diff --git a/src/Discord.Net.Rest/Entities/Users/RestUser.cs b/src/Discord.Net.Rest/Entities/Users/RestUser.cs index 0acfe3ddf..e9b3b39ea 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestUser.cs @@ -13,21 +13,26 @@ namespace Discord.Rest public ushort DiscriminatorValue { get; private set; } public string AvatarId { get; private set; } - public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) - => CDN.GetUserAvatarUrl(Id, AvatarId, size, format); public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); public string Discriminator => DiscriminatorValue.ToString("D4"); public string Mention => MentionUtils.MentionUser(Id); public virtual Game? Game => null; public virtual UserStatus Status => UserStatus.Offline; + public virtual bool IsWebhook => false; internal RestUser(BaseDiscordClient discord, ulong id) : base(discord, id) { } internal static RestUser Create(BaseDiscordClient discord, Model model) + => Create(discord, null, model, null); + internal static RestUser Create(BaseDiscordClient discord, IGuild guild, Model model, ulong? webhookId) { - var entity = new RestUser(discord, model.Id); + RestUser entity; + if (webhookId.HasValue) + entity = new RestWebhookUser(discord, guild, model.Id, webhookId.Value); + else + entity = new RestUser(discord, model.Id); entity.Update(model); return entity; } @@ -52,6 +57,9 @@ namespace Discord.Rest public Task CreateDMChannelAsync(RequestOptions options = null) => UserHelper.CreateDMChannelAsync(this, Discord, options); + public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) + => CDN.GetUserAvatarUrl(Id, AvatarId, size, format); + public override string ToString() => $"{Username}#{Discriminator}"; private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")})"; diff --git a/src/Discord.Net.Rest/Entities/Users/RestWebhookUser.cs b/src/Discord.Net.Rest/Entities/Users/RestWebhookUser.cs new file mode 100644 index 000000000..ae794becc --- /dev/null +++ b/src/Discord.Net.Rest/Entities/Users/RestWebhookUser.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Threading.Tasks; +using Model = Discord.API.User; + +namespace Discord.Rest +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RestWebhookUser : RestUser, IWebhookUser + { + public ulong WebhookId { get; } + internal IGuild Guild { get; } + + public override bool IsWebhook => true; + public ulong GuildId => Guild.Id; + + internal RestWebhookUser(BaseDiscordClient discord, IGuild guild, ulong id, ulong webhookId) + : base(discord, id) + { + Guild = guild; + WebhookId = webhookId; + } + internal static RestWebhookUser Create(BaseDiscordClient discord, IGuild guild, Model model, ulong webhookId) + { + var entity = new RestWebhookUser(discord, guild, model.Id, webhookId); + entity.Update(model); + return entity; + } + + //IGuildUser + IGuild IGuildUser.Guild + { + get + { + if (Guild != null) + return Guild; + throw new InvalidOperationException("Unable to return this entity's parent unless it was fetched through that object."); + } + } + IReadOnlyCollection IGuildUser.RoleIds => ImmutableArray.Create(); + DateTimeOffset? IGuildUser.JoinedAt => null; + string IGuildUser.Nickname => null; + GuildPermissions IGuildUser.GuildPermissions => GuildPermissions.Webhook; + + ChannelPermissions IGuildUser.GetPermissions(IGuildChannel channel) => Permissions.ToChannelPerms(channel, GuildPermissions.Webhook.RawValue); + Task IGuildUser.KickAsync(RequestOptions options) + { + throw new NotSupportedException("Webhook users cannot be kicked."); + } + Task IGuildUser.ModifyAsync(Action func, RequestOptions options) + { + throw new NotSupportedException("Webhook users cannot be modified."); + } + + Task IGuildUser.AddRoleAsync(IRole role, RequestOptions options) + { + throw new NotSupportedException("Roles are not supported on webhook users."); + } + Task IGuildUser.AddRolesAsync(IEnumerable roles, RequestOptions options) + { + throw new NotSupportedException("Roles are not supported on webhook users."); + } + Task IGuildUser.RemoveRoleAsync(IRole role, RequestOptions options) + { + throw new NotSupportedException("Roles are not supported on webhook users."); + } + Task IGuildUser.RemoveRolesAsync(IEnumerable roles, RequestOptions options) + { + throw new NotSupportedException("Roles are not supported on webhook users."); + } + + //IVoiceState + bool IVoiceState.IsDeafened => false; + bool IVoiceState.IsMuted => false; + bool IVoiceState.IsSelfDeafened => false; + bool IVoiceState.IsSelfMuted => false; + bool IVoiceState.IsSuppressed => false; + IVoiceChannel IVoiceState.VoiceChannel => null; + string IVoiceState.VoiceSessionId => null; + } +} diff --git a/src/Discord.Net.Rpc/Entities/Messages/RpcMessage.cs b/src/Discord.Net.Rpc/Entities/Messages/RpcMessage.cs index b85071f2a..c77c06288 100644 --- a/src/Discord.Net.Rpc/Entities/Messages/RpcMessage.cs +++ b/src/Discord.Net.Rpc/Entities/Messages/RpcMessage.cs @@ -13,6 +13,7 @@ namespace Discord.Rpc public IMessageChannel Channel { get; } public RpcUser Author { get; } + public MessageSource Source { get; } public string Content { get; private set; } public Color AuthorColor { get; private set; } @@ -33,11 +34,12 @@ namespace Discord.Rpc public DateTimeOffset Timestamp => DateTimeUtils.FromTicks(_timestampTicks); - internal RpcMessage(DiscordRpcClient discord, ulong id, RestVirtualMessageChannel channel, RpcUser author) + internal RpcMessage(DiscordRpcClient discord, ulong id, RestVirtualMessageChannel channel, RpcUser author, MessageSource source) : base(discord, id) { Channel = channel; Author = author; + Source = source; } internal static RpcMessage Create(DiscordRpcClient discord, ulong channelId, Model model) { diff --git a/src/Discord.Net.Rpc/Entities/Messages/RpcSystemMessage.cs b/src/Discord.Net.Rpc/Entities/Messages/RpcSystemMessage.cs index e8c918bc6..39c6026a7 100644 --- a/src/Discord.Net.Rpc/Entities/Messages/RpcSystemMessage.cs +++ b/src/Discord.Net.Rpc/Entities/Messages/RpcSystemMessage.cs @@ -10,14 +10,14 @@ namespace Discord.Rpc public MessageType Type { get; private set; } internal RpcSystemMessage(DiscordRpcClient discord, ulong id, RestVirtualMessageChannel channel, RpcUser author) - : base(discord, id, channel, author) + : base(discord, id, channel, author, MessageSource.System) { } internal new static RpcSystemMessage Create(DiscordRpcClient discord, ulong channelId, Model model) { var entity = new RpcSystemMessage(discord, model.Id, RestVirtualMessageChannel.Create(discord, channelId), - RpcUser.Create(discord, model.Author.Value)); + RpcUser.Create(discord, model.Author.Value, model.WebhookId.ToNullable())); entity.Update(model); return entity; } diff --git a/src/Discord.Net.Rpc/Entities/Messages/RpcUserMessage.cs b/src/Discord.Net.Rpc/Entities/Messages/RpcUserMessage.cs index 23d277dd9..cdcff4a07 100644 --- a/src/Discord.Net.Rpc/Entities/Messages/RpcUserMessage.cs +++ b/src/Discord.Net.Rpc/Entities/Messages/RpcUserMessage.cs @@ -31,15 +31,16 @@ namespace Discord.Rpc public override IReadOnlyCollection Tags => _tags; public IReadOnlyDictionary Reactions => ImmutableDictionary.Create(); - internal RpcUserMessage(DiscordRpcClient discord, ulong id, RestVirtualMessageChannel channel, RpcUser author) - : base(discord, id, channel, author) + internal RpcUserMessage(DiscordRpcClient discord, ulong id, RestVirtualMessageChannel channel, RpcUser author, MessageSource source) + : base(discord, id, channel, author, source) { } internal new static RpcUserMessage Create(DiscordRpcClient discord, ulong channelId, Model model) { var entity = new RpcUserMessage(discord, model.Id, RestVirtualMessageChannel.Create(discord, channelId), - RpcUser.Create(discord, model.Author.Value)); + RpcUser.Create(discord, model.Author.Value, model.WebhookId.ToNullable()), + MessageHelper.GetSource(model)); entity.Update(model); return entity; } diff --git a/src/Discord.Net.Rpc/Entities/Users/RpcUser.cs b/src/Discord.Net.Rpc/Entities/Users/RpcUser.cs index daf299e2a..cf21928bb 100644 --- a/src/Discord.Net.Rpc/Entities/Users/RpcUser.cs +++ b/src/Discord.Net.Rpc/Entities/Users/RpcUser.cs @@ -14,11 +14,10 @@ namespace Discord.Rpc public ushort DiscriminatorValue { get; private set; } public string AvatarId { get; private set; } - public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) - => CDN.GetUserAvatarUrl(Id, AvatarId, size, format); public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); public string Discriminator => DiscriminatorValue.ToString("D4"); public string Mention => MentionUtils.MentionUser(Id); + public virtual bool IsWebhook => false; public virtual Game? Game => null; public virtual UserStatus Status => UserStatus.Offline; @@ -27,8 +26,14 @@ namespace Discord.Rpc { } internal static RpcUser Create(DiscordRpcClient discord, Model model) + => Create(discord, model, null); + internal static RpcUser Create(DiscordRpcClient discord, Model model, ulong? webhookId) { - var entity = new RpcUser(discord, model.Id); + RpcUser entity; + if (webhookId.HasValue) + entity = new RpcWebhookUser(discord, model.Id, webhookId.Value); + else + entity = new RpcUser(discord, model.Id); entity.Update(model); return entity; } @@ -47,6 +52,9 @@ namespace Discord.Rpc public Task CreateDMChannelAsync(RequestOptions options = null) => UserHelper.CreateDMChannelAsync(this, Discord, options); + public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) + => CDN.GetUserAvatarUrl(Id, AvatarId, size, format); + public override string ToString() => $"{Username}#{Discriminator}"; private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")})"; diff --git a/src/Discord.Net.Rpc/Entities/Users/RpcWebhookUser.cs b/src/Discord.Net.Rpc/Entities/Users/RpcWebhookUser.cs new file mode 100644 index 000000000..9ea4312c2 --- /dev/null +++ b/src/Discord.Net.Rpc/Entities/Users/RpcWebhookUser.cs @@ -0,0 +1,25 @@ +using System.Diagnostics; +using Model = Discord.API.User; + +namespace Discord.Rpc +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RpcWebhookUser : RpcUser + { + public ulong WebhookId { get; } + + public override bool IsWebhook => true; + + internal RpcWebhookUser(DiscordRpcClient discord, ulong id, ulong webhookId) + : base(discord, id) + { + WebhookId = webhookId; + } + internal static RpcWebhookUser Create(DiscordRpcClient discord, Model model, ulong webhookId) + { + var entity = new RpcWebhookUser(discord, model.Id, webhookId); + entity.Update(model); + return entity; + } + } +} diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index 9fb172612..093339028 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -1045,8 +1045,11 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Ignored GUILD_BAN_ADD, guild is not synced yet.").ConfigureAwait(false); return; } - - await _userBannedEvent.InvokeAsync(SocketSimpleUser.Create(this, State, data.User), guild).ConfigureAwait(false); + + SocketUser user = guild.GetUser(data.User.Id); + if (user == null) + user = SocketUnknownUser.Create(this, State, data.User); + await _userBannedEvent.InvokeAsync(user, guild).ConfigureAwait(false); } else { @@ -1071,7 +1074,7 @@ namespace Discord.WebSocket SocketUser user = State.GetUser(data.User.Id); if (user == null) - user = SocketSimpleUser.Create(this, State, data.User); + user = SocketUnknownUser.Create(this, State, data.User); await _userUnbannedEvent.InvokeAsync(user, guild).ConfigureAwait(false); } else @@ -1098,8 +1101,16 @@ namespace Discord.WebSocket return; } - var author = (guild != null ? guild.GetUser(data.Author.Value.Id) : (channel as SocketChannel).GetUser(data.Author.Value.Id)) ?? - SocketSimpleUser.Create(this, State, data.Author.Value); + SocketUser author; + if (guild != null) + { + if (data.WebhookId.IsSpecified) + author = SocketWebhookUser.Create(guild, State, data.Author.Value, data.WebhookId.Value); + else + author = guild.GetUser(data.Author.Value.Id); + } + else + author = (channel as SocketChannel).GetUser(data.Author.Value.Id); if (author != null) { @@ -1153,7 +1164,7 @@ namespace Discord.WebSocket else author = (channel as SocketChannel).GetUser(data.Author.Value.Id); if (author == null) - author = SocketSimpleUser.Create(this, State, data.Author.Value); + author = SocketUnknownUser.Create(this, State, data.Author.Value); after = SocketMessage.Create(this, State, author, channel, data); } diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs index 0b09d2d22..2d63665de 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs @@ -14,6 +14,7 @@ namespace Discord.WebSocket public SocketUser Author { get; } public ISocketMessageChannel Channel { get; } + public MessageSource Source { get; } public string Content { get; private set; } @@ -27,16 +28,15 @@ namespace Discord.WebSocket public virtual IReadOnlyCollection MentionedRoles => ImmutableArray.Create(); public virtual IReadOnlyCollection MentionedUsers => ImmutableArray.Create(); public virtual IReadOnlyCollection Tags => ImmutableArray.Create(); - public virtual ulong? WebhookId => null; - public bool IsWebhook => WebhookId != null; public DateTimeOffset Timestamp => DateTimeUtils.FromTicks(_timestampTicks); - internal SocketMessage(DiscordSocketClient discord, ulong id, ISocketMessageChannel channel, SocketUser author) + internal SocketMessage(DiscordSocketClient discord, ulong id, ISocketMessageChannel channel, SocketUser author, MessageSource source) : base(discord, id) { Channel = channel; Author = author; + Source = source; } internal static SocketMessage Create(DiscordSocketClient discord, ClientState state, SocketUser author, ISocketMessageChannel channel, Model model) { diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketSystemMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketSystemMessage.cs index 50cdb964b..e6c67159f 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketSystemMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketSystemMessage.cs @@ -9,7 +9,7 @@ namespace Discord.WebSocket public MessageType Type { get; private set; } internal SocketSystemMessage(DiscordSocketClient discord, ulong id, ISocketMessageChannel channel, SocketUser author) - : base(discord, id, channel, author) + : base(discord, id, channel, author, MessageSource.System) { } internal new static SocketSystemMessage Create(DiscordSocketClient discord, ClientState state, SocketUser author, ISocketMessageChannel channel, Model model) diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs index 93085234e..8b9acf118 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs @@ -14,7 +14,6 @@ namespace Discord.WebSocket { private bool _isMentioningEveryone, _isTTS, _isPinned; private long? _editedTimestampTicks; - private ulong? _webhookId; private ImmutableArray _attachments; private ImmutableArray _embeds; private ImmutableArray _tags; @@ -22,7 +21,6 @@ namespace Discord.WebSocket public override bool IsTTS => _isTTS; public override bool IsPinned => _isPinned; - public override ulong? WebhookId => _webhookId; public override DateTimeOffset? EditedTimestamp => DateTimeUtils.FromTicks(_editedTimestampTicks); public override IReadOnlyCollection Attachments => _attachments; public override IReadOnlyCollection Embeds => _embeds; @@ -32,13 +30,13 @@ namespace Discord.WebSocket public override IReadOnlyCollection MentionedUsers => MessageHelper.FilterTagsByValue(TagType.UserMention, _tags); public IReadOnlyDictionary Reactions => _reactions.GroupBy(r => r.Emoji).ToDictionary(x => x.Key, x => new ReactionMetadata { ReactionCount = x.Count(), IsMe = x.Any(y => y.UserId == Discord.CurrentUser.Id) }); - internal SocketUserMessage(DiscordSocketClient discord, ulong id, ISocketMessageChannel channel, SocketUser author) - : base(discord, id, channel, author) + internal SocketUserMessage(DiscordSocketClient discord, ulong id, ISocketMessageChannel channel, SocketUser author, MessageSource source) + : base(discord, id, channel, author, source) { } - internal new static SocketUserMessage Create(DiscordSocketClient discord, ClientState state, SocketUser author, ISocketMessageChannel channel, Model model) + internal static new SocketUserMessage Create(DiscordSocketClient discord, ClientState state, SocketUser author, ISocketMessageChannel channel, Model model) { - var entity = new SocketUserMessage(discord, model.Id, channel, author); + var entity = new SocketUserMessage(discord, model.Id, channel, author, MessageHelper.GetSource(model)); entity.Update(state, model); return entity; } @@ -55,8 +53,6 @@ namespace Discord.WebSocket _editedTimestampTicks = model.EditedTimestamp.Value?.UtcTicks; if (model.MentionEveryone.IsSpecified) _isMentioningEveryone = model.MentionEveryone.Value; - if (model.WebhookId.IsSpecified) - _webhookId = model.WebhookId.Value; if (model.Attachments.IsSpecified) { @@ -86,18 +82,18 @@ namespace Discord.WebSocket _embeds = ImmutableArray.Create(); } - ImmutableArray mentions = ImmutableArray.Create(); + IReadOnlyCollection mentions = ImmutableArray.Create(); //Is passed to ParseTags to get real mention collection if (model.UserMentions.IsSpecified) { var value = model.UserMentions.Value; if (value.Length > 0) { - var newMentions = ImmutableArray.CreateBuilder(value.Length); + var newMentions = ImmutableArray.CreateBuilder(value.Length); for (int i = 0; i < value.Length; i++) { var val = value[i]; if (val.Object != null) - newMentions.Add(SocketSimpleUser.Create(Discord, Discord.State, val.Object)); + newMentions.Add(SocketUnknownUser.Create(Discord, state, val.Object)); } mentions = newMentions.ToImmutable(); } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs index 4870937a1..496ca7073 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs @@ -11,9 +11,10 @@ namespace Discord.WebSocket public override ushort DiscriminatorValue { get; internal set; } public override string AvatarId { get; internal set; } public SocketDMChannel DMChannel { get; internal set; } + internal override SocketPresence Presence { get; set; } + public override bool IsWebhook => false; internal override SocketGlobalUser GlobalUser => this; - internal override SocketPresence Presence { get; set; } private readonly object _lockObj = new object(); private ushort _references; diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGroupUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGroupUser.cs index 694d0ccb9..8d1b360e3 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGroupUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGroupUser.cs @@ -15,6 +15,8 @@ namespace Discord.WebSocket public override string AvatarId { get { return GlobalUser.AvatarId; } internal set { GlobalUser.AvatarId = value; } } internal override SocketPresence Presence { get { return GlobalUser.Presence; } set { GlobalUser.Presence = value; } } + public override bool IsWebhook => false; + internal SocketGroupUser(SocketGroupChannel channel, SocketGlobalUser globalUser) : base(channel.Discord, globalUser.Id) { diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs index 387a96f0a..d20fd0d33 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs @@ -28,6 +28,7 @@ namespace Discord.WebSocket public GuildPermissions GuildPermissions => new GuildPermissions(Permissions.ResolveGuild(Guild, this)); internal override SocketPresence Presence { get { return GlobalUser.Presence; } set { GlobalUser.Presence = value; } } + public override bool IsWebhook => false; public bool IsSelfDeafened => VoiceState?.IsSelfDeafened ?? false; public bool IsSelfMuted => VoiceState?.IsSelfMuted ?? false; public bool IsSuppressed => VoiceState?.IsSuppressed ?? false; diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs index 0f6d4e4f1..c0e483a56 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs @@ -20,6 +20,8 @@ namespace Discord.WebSocket public override string AvatarId { get { return GlobalUser.AvatarId; } internal set { GlobalUser.AvatarId = value; } } internal override SocketPresence Presence { get { return GlobalUser.Presence; } set { GlobalUser.Presence = value; } } + public override bool IsWebhook => false; + internal SocketSelfUser(DiscordSocketClient discord, SocketGlobalUser globalUser) : base(discord, globalUser.Id) { diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketSimpleUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs similarity index 70% rename from src/Discord.Net.WebSocket/Entities/Users/SocketSimpleUser.cs rename to src/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs index 0d2198888..57ff81433 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketSimpleUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs @@ -6,23 +6,25 @@ using PresenceModel = Discord.API.Presence; namespace Discord.WebSocket { [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public class SocketSimpleUser : SocketUser + public class SocketUnknownUser : SocketUser { - public override bool IsBot { get; internal set; } public override string Username { get; internal set; } public override ushort DiscriminatorValue { get; internal set; } public override string AvatarId { get; internal set; } - internal override SocketPresence Presence { get { return new SocketPresence(UserStatus.Offline, null); } set { } } + public override bool IsBot { get; internal set; } - internal override SocketGlobalUser GlobalUser { get { throw new NotSupportedException(); } } + public override bool IsWebhook => false; + + internal override SocketPresence Presence { get { return new SocketPresence(UserStatus.Offline, null); } set { } } + internal override SocketGlobalUser GlobalUser { get { throw new NotSupportedException(); } } - internal SocketSimpleUser(DiscordSocketClient discord, ulong id) + internal SocketUnknownUser(DiscordSocketClient discord, ulong id) : base(discord, id) { } - internal static SocketSimpleUser Create(DiscordSocketClient discord, ClientState state, Model model) + internal static SocketUnknownUser Create(DiscordSocketClient discord, ClientState state, Model model) { - var entity = new SocketSimpleUser(discord, model.Id); + var entity = new SocketUnknownUser(discord, model.Id); entity.Update(state, model); return entity; } @@ -39,6 +41,6 @@ namespace Discord.WebSocket Username = model.User.Username.Value; } - internal new SocketSimpleUser Clone() => MemberwiseClone() as SocketSimpleUser; + internal new SocketUnknownUser Clone() => MemberwiseClone() as SocketUnknownUser; } } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs index 5c73e3b6a..6e97d4b31 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs @@ -12,11 +12,10 @@ namespace Discord.WebSocket public abstract string Username { get; internal set; } public abstract ushort DiscriminatorValue { get; internal set; } public abstract string AvatarId { get; internal set; } + public abstract bool IsWebhook { get; } internal abstract SocketGlobalUser GlobalUser { get; } internal abstract SocketPresence Presence { get; set; } - public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) - => CDN.GetUserAvatarUrl(Id, AvatarId, size, format); public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); public string Discriminator => DiscriminatorValue.ToString("D4"); public string Mention => MentionUtils.MentionUser(Id); @@ -47,6 +46,9 @@ namespace Discord.WebSocket public Task CreateDMChannelAsync(RequestOptions options = null) => UserHelper.CreateDMChannelAsync(this, Discord, options); + public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) + => CDN.GetUserAvatarUrl(Id, AvatarId, size, format); + public override string ToString() => $"{Username}#{Discriminator}"; private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")})"; internal SocketUser Clone() => MemberwiseClone() as SocketUser; diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs new file mode 100644 index 000000000..1193eca8f --- /dev/null +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Threading.Tasks; +using Model = Discord.API.User; +using PresenceModel = Discord.API.Presence; + +namespace Discord.WebSocket +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class SocketWebhookUser : SocketUser, IWebhookUser + { + public SocketGuild Guild { get; } + public ulong WebhookId { get; } + + public override string Username { get; internal set; } + public override ushort DiscriminatorValue { get; internal set; } + public override string AvatarId { get; internal set; } + public override bool IsBot { get; internal set; } + + public override bool IsWebhook => true; + + internal override SocketPresence Presence { get { return new SocketPresence(UserStatus.Offline, null); } set { } } + internal override SocketGlobalUser GlobalUser { get { throw new NotSupportedException(); } } + + internal SocketWebhookUser(SocketGuild guild, ulong id, ulong webhookId) + : base(guild.Discord, id) + { + WebhookId = webhookId; + } + internal static SocketWebhookUser Create(SocketGuild guild, ClientState state, Model model, ulong webhookId) + { + var entity = new SocketWebhookUser(guild, model.Id, webhookId); + entity.Update(state, model); + return entity; + } + + internal override void Update(ClientState state, PresenceModel model) + { + if (model.User.Avatar.IsSpecified) + AvatarId = model.User.Avatar.Value; + if (model.User.Discriminator.IsSpecified) + DiscriminatorValue = ushort.Parse(model.User.Discriminator.Value); + if (model.User.Bot.IsSpecified) + IsBot = model.User.Bot.Value; + if (model.User.Username.IsSpecified) + Username = model.User.Username.Value; + } + + internal new SocketWebhookUser Clone() => MemberwiseClone() as SocketWebhookUser; + + + //IGuildUser + IGuild IGuildUser.Guild => Guild; + ulong IGuildUser.GuildId => Guild.Id; + IReadOnlyCollection IGuildUser.RoleIds => ImmutableArray.Create(); + DateTimeOffset? IGuildUser.JoinedAt => null; + string IGuildUser.Nickname => null; + GuildPermissions IGuildUser.GuildPermissions => GuildPermissions.Webhook; + + ChannelPermissions IGuildUser.GetPermissions(IGuildChannel channel) => Permissions.ToChannelPerms(channel, GuildPermissions.Webhook.RawValue); + Task IGuildUser.KickAsync(RequestOptions options) + { + throw new NotSupportedException("Webhook users cannot be kicked."); + } + Task IGuildUser.ModifyAsync(Action func, RequestOptions options) + { + throw new NotSupportedException("Webhook users cannot be modified."); + } + + Task IGuildUser.AddRoleAsync(IRole role, RequestOptions options) + { + throw new NotSupportedException("Roles are not supported on webhook users."); + } + Task IGuildUser.AddRolesAsync(IEnumerable roles, RequestOptions options) + { + throw new NotSupportedException("Roles are not supported on webhook users."); + } + Task IGuildUser.RemoveRoleAsync(IRole role, RequestOptions options) + { + throw new NotSupportedException("Roles are not supported on webhook users."); + } + Task IGuildUser.RemoveRolesAsync(IEnumerable roles, RequestOptions options) + { + throw new NotSupportedException("Roles are not supported on webhook users."); + } + + //IVoiceState + bool IVoiceState.IsDeafened => false; + bool IVoiceState.IsMuted => false; + bool IVoiceState.IsSelfDeafened => false; + bool IVoiceState.IsSelfMuted => false; + bool IVoiceState.IsSuppressed => false; + IVoiceChannel IVoiceState.VoiceChannel => null; + string IVoiceState.VoiceSessionId => null; + } +} From 13d488f43bb90cb5ccc232256f85c5a3a07a5f02 Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 31 Mar 2017 00:35:20 -0300 Subject: [PATCH 059/243] Added slow/deadlocked event handler detection --- src/Discord.Net.Core/Utils/AsyncEvent.cs | 1 + .../DiscordSocketClient.cs | 154 ++++++++++++++---- .../DiscordSocketConfig.cs | 2 + 3 files changed, 121 insertions(+), 36 deletions(-) diff --git a/src/Discord.Net.Core/Utils/AsyncEvent.cs b/src/Discord.Net.Core/Utils/AsyncEvent.cs index 12a1fba9c..731489dea 100644 --- a/src/Discord.Net.Core/Utils/AsyncEvent.cs +++ b/src/Discord.Net.Core/Utils/AsyncEvent.cs @@ -11,6 +11,7 @@ namespace Discord private readonly object _subLock = new object(); internal ImmutableArray _subscriptions; + public bool HasSubscribers => _subscriptions.Length != 0; public IReadOnlyList Subscriptions => _subscriptions; public AsyncEvent() diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index 093339028..76e943ce4 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -21,6 +21,8 @@ namespace Discord.WebSocket { public partial class DiscordSocketClient : BaseDiscordClient, IDiscordClient { + private const int HandlerTimeoutMillis = 3000; + private readonly ConcurrentQueue _largeGuilds; private readonly JsonSerializer _serializer; private readonly SemaphoreSlim _connectionGroupLock; @@ -57,6 +59,7 @@ namespace Discord.WebSocket internal UdpSocketProvider UdpSocketProvider { get; private set; } internal WebSocketProvider WebSocketProvider { get; private set; } internal bool AlwaysDownloadUsers { get; private set; } + internal bool EnableHandlerTimeouts { get; private set; } internal new DiscordSocketApiClient ApiClient => base.ApiClient as DiscordSocketApiClient; public new SocketSelfUser CurrentUser { get { return base.CurrentUser as SocketSelfUser; } private set { base.CurrentUser = value; } } @@ -83,6 +86,7 @@ namespace Discord.WebSocket UdpSocketProvider = config.UdpSocketProvider; WebSocketProvider = config.WebSocketProvider; AlwaysDownloadUsers = config.AlwaysDownloadUsers; + EnableHandlerTimeouts = config.EnableHandlerTimeouts; State = new ClientState(0, 0); _heartbeatTimes = new ConcurrentQueue(); @@ -90,8 +94,8 @@ namespace Discord.WebSocket _gatewayLogger = LogManager.CreateLogger(ShardId == 0 && TotalShards == 1 ? "Gateway" : $"Shard #{ShardId}"); _connection = new ConnectionManager(_stateLock, _gatewayLogger, config.ConnectionTimeout, OnConnectingAsync, OnDisconnectingAsync, x => ApiClient.Disconnected += x); - _connection.Connected += () => _connectedEvent.InvokeAsync(); - _connection.Disconnected += (ex, recon) => _disconnectedEvent.InvokeAsync(ex); + _connection.Connected += () => TimedInvokeAsync(_connectedEvent, nameof(Connected)); + _connection.Disconnected += (ex, recon) => TimedInvokeAsync(_disconnectedEvent, nameof(Disconnected), ex); _nextAudioId = 1; _connectionGroupLock = groupLock; @@ -422,7 +426,7 @@ namespace Discord.WebSocket int before = Latency; Latency = latency; - await _latencyUpdatedEvent.InvokeAsync(before, latency).ConfigureAwait(false); + await TimedInvokeAsync(_latencyUpdatedEvent, nameof(LatencyUpdated), before, latency).ConfigureAwait(false); } } break; @@ -500,7 +504,7 @@ namespace Discord.WebSocket else if (_connection.CancelToken.IsCancellationRequested) return; - await _readyEvent.InvokeAsync().ConfigureAwait(false); + await TimedInvokeAsync(_readyEvent, nameof(Ready)).ConfigureAwait(false); await _gatewayLogger.InfoAsync("Ready").ConfigureAwait(false); }); var _ = _connection.CompleteAsync(); @@ -559,7 +563,7 @@ namespace Discord.WebSocket { if (ApiClient.AuthTokenType == TokenType.User) await SyncGuildsAsync().ConfigureAwait(false); - await _joinedGuildEvent.InvokeAsync(guild).ConfigureAwait(false); + await TimedInvokeAsync(_joinedGuildEvent, nameof(JoinedGuild), guild).ConfigureAwait(false); } else { @@ -579,7 +583,7 @@ namespace Discord.WebSocket { var before = guild.Clone(); guild.Update(State, data); - await _guildUpdatedEvent.InvokeAsync(before, guild).ConfigureAwait(false); + await TimedInvokeAsync(_guildUpdatedEvent, nameof(GuildUpdated), before, guild).ConfigureAwait(false); } else { @@ -598,7 +602,7 @@ namespace Discord.WebSocket { var before = guild.Clone(); guild.Update(State, data); - await _guildUpdatedEvent.InvokeAsync(before, guild).ConfigureAwait(false); + await TimedInvokeAsync(_guildUpdatedEvent, nameof(GuildUpdated), before, guild).ConfigureAwait(false); } else { @@ -620,7 +624,7 @@ namespace Discord.WebSocket _unavailableGuilds--; _lastGuildAvailableTime = Environment.TickCount; await GuildAvailableAsync(guild).ConfigureAwait(false); - await _guildUpdatedEvent.InvokeAsync(before, guild).ConfigureAwait(false); + await TimedInvokeAsync(_guildUpdatedEvent, nameof(GuildUpdated), before, guild).ConfigureAwait(false); } else { @@ -657,7 +661,7 @@ namespace Discord.WebSocket if (guild != null) { await GuildUnavailableAsync(guild).ConfigureAwait(false); - await _leftGuildEvent.InvokeAsync(guild).ConfigureAwait(false); + await TimedInvokeAsync(_leftGuildEvent, nameof(LeftGuild), guild).ConfigureAwait(false); } else { @@ -698,7 +702,7 @@ namespace Discord.WebSocket channel = AddPrivateChannel(data, State) as SocketChannel; if (channel != null) - await _channelCreatedEvent.InvokeAsync(channel).ConfigureAwait(false); + await TimedInvokeAsync(_channelCreatedEvent, nameof(ChannelCreated), channel).ConfigureAwait(false); } break; case "CHANNEL_UPDATE": @@ -718,7 +722,7 @@ namespace Discord.WebSocket return; } - await _channelUpdatedEvent.InvokeAsync(before, channel).ConfigureAwait(false); + await TimedInvokeAsync(_channelUpdatedEvent, nameof(ChannelUpdated), before, channel).ConfigureAwait(false); } else { @@ -756,7 +760,7 @@ namespace Discord.WebSocket channel = RemovePrivateChannel(data.Id) as SocketChannel; if (channel != null) - await _channelDestroyedEvent.InvokeAsync(channel).ConfigureAwait(false); + await TimedInvokeAsync(_channelDestroyedEvent, nameof(ChannelDestroyed), channel).ConfigureAwait(false); else { await _gatewayLogger.WarningAsync("CHANNEL_DELETE referenced an unknown channel.").ConfigureAwait(false); @@ -783,7 +787,7 @@ namespace Discord.WebSocket return; } - await _userJoinedEvent.InvokeAsync(user).ConfigureAwait(false); + await TimedInvokeAsync(_userJoinedEvent, nameof(UserJoined), user).ConfigureAwait(false); } else { @@ -812,7 +816,7 @@ namespace Discord.WebSocket { var before = user.Clone(); user.Update(State, data); - await _guildMemberUpdatedEvent.InvokeAsync(before, user).ConfigureAwait(false); + await TimedInvokeAsync(_guildMemberUpdatedEvent, nameof(GuildMemberUpdated), before, user).ConfigureAwait(false); } else { @@ -851,7 +855,7 @@ namespace Discord.WebSocket } if (user != null) - await _userLeftEvent.InvokeAsync(user).ConfigureAwait(false); + await TimedInvokeAsync(_userLeftEvent, nameof(UserLeft), user).ConfigureAwait(false); else { if (!guild.HasAllMembers) @@ -885,7 +889,7 @@ namespace Discord.WebSocket if (guild.DownloadedMemberCount >= guild.MemberCount) //Finished downloading for there { guild.CompleteDownloadUsers(); - await _guildMembersDownloadedEvent.InvokeAsync(guild).ConfigureAwait(false); + await TimedInvokeAsync(_guildMembersDownloadedEvent, nameof(GuildMembersDownloaded), guild).ConfigureAwait(false); } } else @@ -904,7 +908,7 @@ namespace Discord.WebSocket if (channel != null) { var user = channel.AddUser(data.User); - await _recipientAddedEvent.InvokeAsync(user).ConfigureAwait(false); + await TimedInvokeAsync(_recipientAddedEvent, nameof(RecipientAdded), user).ConfigureAwait(false); } else { @@ -923,7 +927,7 @@ namespace Discord.WebSocket { var user = channel.RemoveUser(data.User.Id); if (user != null) - await _recipientRemovedEvent.InvokeAsync(user).ConfigureAwait(false); + await TimedInvokeAsync(_recipientRemovedEvent, nameof(RecipientRemoved), user).ConfigureAwait(false); else { await _gatewayLogger.WarningAsync("CHANNEL_RECIPIENT_REMOVE referenced an unknown user.").ConfigureAwait(false); @@ -954,7 +958,7 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Ignored GUILD_ROLE_CREATE, guild is not synced yet.").ConfigureAwait(false); return; } - await _roleCreatedEvent.InvokeAsync(role).ConfigureAwait(false); + await TimedInvokeAsync(_roleCreatedEvent, nameof(RoleCreated), role).ConfigureAwait(false); } else { @@ -983,7 +987,7 @@ namespace Discord.WebSocket return; } - await _roleUpdatedEvent.InvokeAsync(before, role).ConfigureAwait(false); + await TimedInvokeAsync(_roleUpdatedEvent, nameof(RoleUpdated), before, role).ConfigureAwait(false); } else { @@ -1015,7 +1019,7 @@ namespace Discord.WebSocket return; } - await _roleDeletedEvent.InvokeAsync(role).ConfigureAwait(false); + await TimedInvokeAsync(_roleDeletedEvent, nameof(RoleDeleted), role).ConfigureAwait(false); } else { @@ -1049,7 +1053,7 @@ namespace Discord.WebSocket SocketUser user = guild.GetUser(data.User.Id); if (user == null) user = SocketUnknownUser.Create(this, State, data.User); - await _userBannedEvent.InvokeAsync(user, guild).ConfigureAwait(false); + await TimedInvokeAsync(_userBannedEvent, nameof(UserBanned), user, guild).ConfigureAwait(false); } else { @@ -1075,7 +1079,7 @@ namespace Discord.WebSocket SocketUser user = State.GetUser(data.User.Id); if (user == null) user = SocketUnknownUser.Create(this, State, data.User); - await _userUnbannedEvent.InvokeAsync(user, guild).ConfigureAwait(false); + await TimedInvokeAsync(_userUnbannedEvent, nameof(UserUnbanned), user, guild).ConfigureAwait(false); } else { @@ -1116,7 +1120,7 @@ namespace Discord.WebSocket { var msg = SocketMessage.Create(this, State, author, channel, data); SocketChannelHelper.AddMessage(channel, this, msg); - await _messageReceivedEvent.InvokeAsync(msg).ConfigureAwait(false); + await TimedInvokeAsync(_messageReceivedEvent, nameof(MessageReceived), msg).ConfigureAwait(false); } else { @@ -1170,7 +1174,7 @@ namespace Discord.WebSocket } var cacheableBefore = new Cacheable(before, data.Id, isCached , async () => await channel.GetMessageAsync(data.Id)); - await _messageUpdatedEvent.InvokeAsync(cacheableBefore, after, channel).ConfigureAwait(false); + await TimedInvokeAsync(_messageUpdatedEvent, nameof(MessageUpdated), cacheableBefore, after, channel).ConfigureAwait(false); } else { @@ -1197,7 +1201,7 @@ namespace Discord.WebSocket bool isCached = msg != null; var cacheable = new Cacheable(msg, data.Id, isCached, async () => await channel.GetMessageAsync(data.Id)); - await _messageDeletedEvent.InvokeAsync(cacheable, channel).ConfigureAwait(false); + await TimedInvokeAsync(_messageDeletedEvent, nameof(MessageDeleted), cacheable, channel).ConfigureAwait(false); } else { @@ -1222,7 +1226,7 @@ namespace Discord.WebSocket cachedMsg?.AddReaction(reaction); - await _reactionAddedEvent.InvokeAsync(cacheable, channel, reaction).ConfigureAwait(false); + await TimedInvokeAsync(_reactionAddedEvent, nameof(ReactionAdded), cacheable, channel, reaction).ConfigureAwait(false); } else { @@ -1247,7 +1251,7 @@ namespace Discord.WebSocket cachedMsg?.RemoveReaction(reaction); - await _reactionRemovedEvent.InvokeAsync(cacheable, channel, reaction).ConfigureAwait(false); + await TimedInvokeAsync(_reactionRemovedEvent, nameof(ReactionRemoved), cacheable, channel, reaction).ConfigureAwait(false); } else { @@ -1270,7 +1274,7 @@ namespace Discord.WebSocket cachedMsg?.ClearReactions(); - await _reactionsClearedEvent.InvokeAsync(cacheable, channel).ConfigureAwait(false); + await TimedInvokeAsync(_reactionsClearedEvent, nameof(ReactionsCleared), cacheable, channel).ConfigureAwait(false); } else { @@ -1298,7 +1302,7 @@ namespace Discord.WebSocket var msg = SocketChannelHelper.RemoveMessage(channel, this, id); bool isCached = msg != null; var cacheable = new Cacheable(msg, id, isCached, async () => await channel.GetMessageAsync(id)); - await _messageDeletedEvent.InvokeAsync(cacheable, channel).ConfigureAwait(false); + await TimedInvokeAsync(_messageDeletedEvent, nameof(MessageDeleted), cacheable, channel).ConfigureAwait(false); } } else @@ -1339,7 +1343,7 @@ namespace Discord.WebSocket var before = globalUser.Clone(); globalUser.Update(State, data); - await _userUpdatedEvent.InvokeAsync(before, globalUser).ConfigureAwait(false); + await TimedInvokeAsync(_userUpdatedEvent, nameof(UserUpdated), before, globalUser).ConfigureAwait(false); } break; case "TYPING_START": @@ -1358,7 +1362,7 @@ namespace Discord.WebSocket var user = (channel as SocketChannel).GetUser(data.UserId); if (user != null) - await _userIsTypingEvent.InvokeAsync(user, channel).ConfigureAwait(false); + await TimedInvokeAsync(_userIsTypingEvent, nameof(UserIsTyping), user, channel).ConfigureAwait(false); } } break; @@ -1373,7 +1377,7 @@ namespace Discord.WebSocket { var before = CurrentUser.Clone(); CurrentUser.Update(State, data); - await _selfUpdatedEvent.InvokeAsync(before, CurrentUser).ConfigureAwait(false); + await TimedInvokeAsync(_selfUpdatedEvent, nameof(CurrentUserUpdated), before, CurrentUser).ConfigureAwait(false); } else { @@ -1449,7 +1453,7 @@ namespace Discord.WebSocket } if (user != null) - await _userVoiceStateUpdatedEvent.InvokeAsync(user, before, after).ConfigureAwait(false); + await TimedInvokeAsync(_userVoiceStateUpdatedEvent, nameof(UserVoiceStateUpdated), user, before, after).ConfigureAwait(false); else { await _gatewayLogger.WarningAsync("VOICE_STATE_UPDATE referenced an unknown user.").ConfigureAwait(false); @@ -1631,7 +1635,7 @@ namespace Discord.WebSocket if (!guild.IsConnected) { guild.IsConnected = true; - await _guildAvailableEvent.InvokeAsync(guild).ConfigureAwait(false); + await TimedInvokeAsync(_guildAvailableEvent, nameof(GuildAvailable), guild).ConfigureAwait(false); } } private async Task GuildUnavailableAsync(SocketGuild guild) @@ -1639,7 +1643,85 @@ namespace Discord.WebSocket if (guild.IsConnected) { guild.IsConnected = false; - await _guildUnavailableEvent.InvokeAsync(guild).ConfigureAwait(false); + await TimedInvokeAsync(_guildUnavailableEvent, nameof(GuildUnavailable), guild).ConfigureAwait(false); + } + } + + private async Task TimedInvokeAsync(AsyncEvent> eventHandler, string name) + { + if (eventHandler.HasSubscribers) + { + if (EnableHandlerTimeouts) + await TimeoutWrap(name, () => eventHandler.InvokeAsync()).ConfigureAwait(false); + else + await eventHandler.InvokeAsync().ConfigureAwait(false); + } + } + private async Task TimedInvokeAsync(AsyncEvent> eventHandler, string name, T arg) + { + if (eventHandler.HasSubscribers) + { + if (EnableHandlerTimeouts) + await TimeoutWrap(name, () => eventHandler.InvokeAsync(arg)).ConfigureAwait(false); + else + await eventHandler.InvokeAsync(arg).ConfigureAwait(false); + } + } + private async Task TimedInvokeAsync(AsyncEvent> eventHandler, string name, T1 arg1, T2 arg2) + { + if (eventHandler.HasSubscribers) + { + if (EnableHandlerTimeouts) + await TimeoutWrap(name, () => eventHandler.InvokeAsync(arg1, arg2)).ConfigureAwait(false); + else + await eventHandler.InvokeAsync(arg1, arg2).ConfigureAwait(false); + } + } + private async Task TimedInvokeAsync(AsyncEvent> eventHandler, string name, T1 arg1, T2 arg2, T3 arg3) + { + if (eventHandler.HasSubscribers) + { + if (EnableHandlerTimeouts) + await TimeoutWrap(name, () => eventHandler.InvokeAsync(arg1, arg2, arg3)).ConfigureAwait(false); + else + await eventHandler.InvokeAsync(arg1, arg2, arg3).ConfigureAwait(false); + } + } + private async Task TimedInvokeAsync(AsyncEvent> eventHandler, string name, T1 arg1, T2 arg2, T3 arg3, T4 arg4) + { + if (eventHandler.HasSubscribers) + { + if (EnableHandlerTimeouts) + await TimeoutWrap(name, () => eventHandler.InvokeAsync(arg1, arg2, arg3, arg4)).ConfigureAwait(false); + else + await eventHandler.InvokeAsync(arg1, arg2, arg3, arg4).ConfigureAwait(false); + } + } + private async Task TimedInvokeAsync(AsyncEvent> eventHandler, string name, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) + { + if (eventHandler.HasSubscribers) + { + if (EnableHandlerTimeouts) + await TimeoutWrap(name, () => eventHandler.InvokeAsync(arg1, arg2, arg3, arg4, arg5)).ConfigureAwait(false); + else + await eventHandler.InvokeAsync(arg1, arg2, arg3, arg4, arg5).ConfigureAwait(false); + } + } + private async Task TimeoutWrap(string name, Func action) + { + try + { + var timeoutTask = Task.Delay(HandlerTimeoutMillis); + var handlersTask = action(); + if (await Task.WhenAny(timeoutTask, handlersTask).ConfigureAwait(false) == timeoutTask) + { + await _gatewayLogger.WarningAsync($"A {name} handler is blocking the gateway task.").ConfigureAwait(false); + await handlersTask.ConfigureAwait(false); //Ensure the handler completes + } + } + catch (Exception ex) + { + await _gatewayLogger.WarningAsync($"A {name} handler has thrown an unhandled exception.", ex).ConfigureAwait(false); } } diff --git a/src/Discord.Net.WebSocket/DiscordSocketConfig.cs b/src/Discord.Net.WebSocket/DiscordSocketConfig.cs index 9ef030d72..add42ce80 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketConfig.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketConfig.cs @@ -33,6 +33,8 @@ namespace Discord.WebSocket /// Gets or sets whether or not all users should be downloaded as guilds come available. public bool AlwaysDownloadUsers { get; set; } = false; + /// Gets or sets whether or not warnings should be logged if an event handler is taking too long to execute. + public bool EnableHandlerTimeouts { get; set; } = true; public DiscordSocketConfig() { From 5a1beeeb663bc1355a989f3358a27ed78b3080dd Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 31 Mar 2017 01:31:38 -0300 Subject: [PATCH 060/243] Fixed DiscordShardedClient CurrentUser and RecipientRemoved --- .../DiscordShardedClient.cs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/Discord.Net.WebSocket/DiscordShardedClient.cs b/src/Discord.Net.WebSocket/DiscordShardedClient.cs index cb1f32fab..e65de38b5 100644 --- a/src/Discord.Net.WebSocket/DiscordShardedClient.cs +++ b/src/Discord.Net.WebSocket/DiscordShardedClient.cs @@ -65,7 +65,7 @@ namespace Discord.WebSocket var newConfig = config.Clone(); newConfig.ShardId = _shardIds[i]; _shards[i] = new DiscordSocketClient(newConfig, _connectionGroupLock, i != 0 ? _shards[0] : null); - RegisterEvents(_shards[i]); + RegisterEvents(_shards[i], i == 0); } } } @@ -87,7 +87,7 @@ namespace Discord.WebSocket newConfig.ShardId = _shardIds[i]; newConfig.TotalShards = _totalShards; _shards[i] = new DiscordSocketClient(newConfig, _connectionGroupLock, i != 0 ? _shards[0] : null); - RegisterEvents(_shards[i]); + RegisterEvents(_shards[i], i == 0); } } @@ -256,7 +256,7 @@ namespace Discord.WebSocket await _shards[i].SetGameAsync(name, streamUrl, streamType).ConfigureAwait(false); } - private void RegisterEvents(DiscordSocketClient client) + private void RegisterEvents(DiscordSocketClient client, bool isPrimary) { client.Log += (msg) => _logEvent.InvokeAsync(msg); client.LoggedOut += () => @@ -269,6 +269,14 @@ namespace Discord.WebSocket } return Task.Delay(0); }; + if (isPrimary) + { + client.Ready += () => + { + CurrentUser = client.CurrentUser; + return Task.Delay(0); + }; + } client.ChannelCreated += (channel) => _channelCreatedEvent.InvokeAsync(channel); client.ChannelDestroyed += (channel) => _channelDestroyedEvent.InvokeAsync(channel); @@ -302,7 +310,7 @@ namespace Discord.WebSocket client.CurrentUserUpdated += (oldUser, newUser) => _selfUpdatedEvent.InvokeAsync(oldUser, newUser); client.UserIsTyping += (oldUser, newUser) => _userIsTypingEvent.InvokeAsync(oldUser, newUser); client.RecipientAdded += (user) => _recipientAddedEvent.InvokeAsync(user); - client.RecipientAdded += (user) => _recipientRemovedEvent.InvokeAsync(user); + client.RecipientRemoved += (user) => _recipientRemovedEvent.InvokeAsync(user); } //IDiscordClient From 5aa92f8954af85d531dfc389927ec4c9e1c82ce2 Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 31 Mar 2017 01:47:42 -0300 Subject: [PATCH 061/243] Reset position when uploading file from temp stream --- src/Discord.Net.Rest/Net/DefaultRestClient.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Discord.Net.Rest/Net/DefaultRestClient.cs b/src/Discord.Net.Rest/Net/DefaultRestClient.cs index 2381a9976..3b21307e1 100644 --- a/src/Discord.Net.Rest/Net/DefaultRestClient.cs +++ b/src/Discord.Net.Rest/Net/DefaultRestClient.cs @@ -106,6 +106,7 @@ namespace Discord.Net.Rest { var memoryStream = new MemoryStream(); await stream.CopyToAsync(memoryStream).ConfigureAwait(false); + memoryStream.Position = 0; stream = memoryStream; } content.Add(new StreamContent(stream), p.Key, fileValue.Filename); From 5e94b9702462e55859cc2df4c84f4753f21a6382 Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 31 Mar 2017 03:01:49 -0300 Subject: [PATCH 062/243] Added RequestOptions to RestClient methods. Added guild summary paging. --- src/Discord.Net.Core/DiscordConfig.cs | 1 + src/Discord.Net.Core/IDiscordClient.cs | 28 ++--- .../API/Rest/GetGuildSummariesParams.cs | 9 ++ src/Discord.Net.Rest/BaseDiscordClient.cs | 28 ++--- src/Discord.Net.Rest/ClientHelper.cs | 98 +++++++++------ src/Discord.Net.Rest/DiscordRestApiClient.cs | 12 +- src/Discord.Net.Rest/DiscordRestClient.cs | 119 +++++++++--------- src/Discord.Net.Rpc/DiscordRpcClient.cs | 2 +- .../DiscordShardedClient.cs | 30 ++--- .../DiscordSocketClient.cs | 36 +++--- 10 files changed, 204 insertions(+), 159 deletions(-) create mode 100644 src/Discord.Net.Rest/API/Rest/GetGuildSummariesParams.cs diff --git a/src/Discord.Net.Core/DiscordConfig.cs b/src/Discord.Net.Core/DiscordConfig.cs index b1e075e5b..2a11a6edb 100644 --- a/src/Discord.Net.Core/DiscordConfig.cs +++ b/src/Discord.Net.Core/DiscordConfig.cs @@ -18,6 +18,7 @@ namespace Discord public const int MaxMessageSize = 2000; public const int MaxMessagesPerBatch = 100; public const int MaxUsersPerBatch = 1000; + public const int MaxGuildsPerBatch = 100; /// Gets or sets how a request should act in the case of an error, by default. public RetryMode DefaultRetryMode { get; set; } = RetryMode.AlwaysRetry; diff --git a/src/Discord.Net.Core/IDiscordClient.cs b/src/Discord.Net.Core/IDiscordClient.cs index c434ccd7b..23e8e9c5b 100644 --- a/src/Discord.Net.Core/IDiscordClient.cs +++ b/src/Discord.Net.Core/IDiscordClient.cs @@ -14,25 +14,25 @@ namespace Discord Task StartAsync(); Task StopAsync(); - Task GetApplicationInfoAsync(); + Task GetApplicationInfoAsync(RequestOptions options = null); - Task GetChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload); - Task> GetPrivateChannelsAsync(CacheMode mode = CacheMode.AllowDownload); - Task> GetDMChannelsAsync(CacheMode mode = CacheMode.AllowDownload); - Task> GetGroupChannelsAsync(CacheMode mode = CacheMode.AllowDownload); + Task GetChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + Task> GetPrivateChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + Task> GetDMChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + Task> GetGroupChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); - Task> GetConnectionsAsync(); + Task> GetConnectionsAsync(RequestOptions options = null); - Task GetGuildAsync(ulong id, CacheMode mode = CacheMode.AllowDownload); - Task> GetGuildsAsync(CacheMode mode = CacheMode.AllowDownload); - Task CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon = null); + Task GetGuildAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + Task> GetGuildsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + Task CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon = null, RequestOptions options = null); - Task GetInviteAsync(string inviteId); + Task GetInviteAsync(string inviteId, RequestOptions options = null); - Task GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload); - Task GetUserAsync(string username, string discriminator); + Task GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + Task GetUserAsync(string username, string discriminator, RequestOptions options = null); - Task> GetVoiceRegionsAsync(); - Task GetVoiceRegionAsync(string id); + Task> GetVoiceRegionsAsync(RequestOptions options = null); + Task GetVoiceRegionAsync(string id, RequestOptions options = null); } } diff --git a/src/Discord.Net.Rest/API/Rest/GetGuildSummariesParams.cs b/src/Discord.Net.Rest/API/Rest/GetGuildSummariesParams.cs new file mode 100644 index 000000000..f770ef398 --- /dev/null +++ b/src/Discord.Net.Rest/API/Rest/GetGuildSummariesParams.cs @@ -0,0 +1,9 @@ +#pragma warning disable CS1591 +namespace Discord.API.Rest +{ + internal class GetGuildSummariesParams + { + public Optional Limit { get; set; } + public Optional AfterGuildId { get; set; } + } +} diff --git a/src/Discord.Net.Rest/BaseDiscordClient.cs b/src/Discord.Net.Rest/BaseDiscordClient.cs index df4f180b2..ed12ff383 100644 --- a/src/Discord.Net.Rest/BaseDiscordClient.cs +++ b/src/Discord.Net.Rest/BaseDiscordClient.cs @@ -127,37 +127,37 @@ namespace Discord.Rest ConnectionState IDiscordClient.ConnectionState => ConnectionState.Disconnected; ISelfUser IDiscordClient.CurrentUser => CurrentUser; - Task IDiscordClient.GetApplicationInfoAsync() { throw new NotSupportedException(); } + Task IDiscordClient.GetApplicationInfoAsync(RequestOptions options) { throw new NotSupportedException(); } - Task IDiscordClient.GetChannelAsync(ulong id, CacheMode mode) + Task IDiscordClient.GetChannelAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(null); - Task> IDiscordClient.GetPrivateChannelsAsync(CacheMode mode) + Task> IDiscordClient.GetPrivateChannelsAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(ImmutableArray.Create()); - Task> IDiscordClient.GetDMChannelsAsync(CacheMode mode) + Task> IDiscordClient.GetDMChannelsAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(ImmutableArray.Create()); - Task> IDiscordClient.GetGroupChannelsAsync(CacheMode mode) + Task> IDiscordClient.GetGroupChannelsAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(ImmutableArray.Create()); - Task> IDiscordClient.GetConnectionsAsync() + Task> IDiscordClient.GetConnectionsAsync(RequestOptions options) => Task.FromResult>(ImmutableArray.Create()); - Task IDiscordClient.GetInviteAsync(string inviteId) + Task IDiscordClient.GetInviteAsync(string inviteId, RequestOptions options) => Task.FromResult(null); - Task IDiscordClient.GetGuildAsync(ulong id, CacheMode mode) + Task IDiscordClient.GetGuildAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(null); - Task> IDiscordClient.GetGuildsAsync(CacheMode mode) + Task> IDiscordClient.GetGuildsAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(ImmutableArray.Create()); - Task IDiscordClient.CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon) { throw new NotSupportedException(); } + Task IDiscordClient.CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon, RequestOptions options) { throw new NotSupportedException(); } - Task IDiscordClient.GetUserAsync(ulong id, CacheMode mode) + Task IDiscordClient.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(null); - Task IDiscordClient.GetUserAsync(string username, string discriminator) + Task IDiscordClient.GetUserAsync(string username, string discriminator, RequestOptions options) => Task.FromResult(null); - Task> IDiscordClient.GetVoiceRegionsAsync() + Task> IDiscordClient.GetVoiceRegionsAsync(RequestOptions options) => Task.FromResult>(ImmutableArray.Create()); - Task IDiscordClient.GetVoiceRegionAsync(string id) + Task IDiscordClient.GetVoiceRegionAsync(string id, RequestOptions options) => Task.FromResult(null); Task IDiscordClient.StartAsync() diff --git a/src/Discord.Net.Rest/ClientHelper.cs b/src/Discord.Net.Rest/ClientHelper.cs index 456362be6..a1fedb113 100644 --- a/src/Discord.Net.Rest/ClientHelper.cs +++ b/src/Discord.Net.Rest/ClientHelper.cs @@ -10,80 +10,104 @@ namespace Discord.Rest internal static class ClientHelper { //Applications - public static async Task GetApplicationInfoAsync(BaseDiscordClient client) + public static async Task GetApplicationInfoAsync(BaseDiscordClient client, RequestOptions options) { - var model = await client.ApiClient.GetMyApplicationAsync().ConfigureAwait(false); + var model = await client.ApiClient.GetMyApplicationAsync(options).ConfigureAwait(false); return RestApplication.Create(client, model); } public static async Task GetChannelAsync(BaseDiscordClient client, - ulong id) + ulong id, RequestOptions options) { - var model = await client.ApiClient.GetChannelAsync(id).ConfigureAwait(false); + var model = await client.ApiClient.GetChannelAsync(id, options).ConfigureAwait(false); if (model != null) return RestChannel.Create(client, model); return null; } - public static async Task> GetPrivateChannelsAsync(BaseDiscordClient client) + public static async Task> GetPrivateChannelsAsync(BaseDiscordClient client, RequestOptions options) { - var models = await client.ApiClient.GetMyPrivateChannelsAsync().ConfigureAwait(false); + var models = await client.ApiClient.GetMyPrivateChannelsAsync(options).ConfigureAwait(false); return models.Select(x => RestChannel.CreatePrivate(client, x)).ToImmutableArray(); } - public static async Task> GetDMChannelsAsync(BaseDiscordClient client) + public static async Task> GetDMChannelsAsync(BaseDiscordClient client, RequestOptions options) { - var models = await client.ApiClient.GetMyPrivateChannelsAsync().ConfigureAwait(false); + var models = await client.ApiClient.GetMyPrivateChannelsAsync(options).ConfigureAwait(false); return models .Where(x => x.Type == ChannelType.DM) .Select(x => RestDMChannel.Create(client, x)).ToImmutableArray(); } - public static async Task> GetGroupChannelsAsync(BaseDiscordClient client) + public static async Task> GetGroupChannelsAsync(BaseDiscordClient client, RequestOptions options) { - var models = await client.ApiClient.GetMyPrivateChannelsAsync().ConfigureAwait(false); + var models = await client.ApiClient.GetMyPrivateChannelsAsync(options).ConfigureAwait(false); return models .Where(x => x.Type == ChannelType.Group) .Select(x => RestGroupChannel.Create(client, x)).ToImmutableArray(); } - public static async Task> GetConnectionsAsync(BaseDiscordClient client) + public static async Task> GetConnectionsAsync(BaseDiscordClient client, RequestOptions options) { - var models = await client.ApiClient.GetMyConnectionsAsync().ConfigureAwait(false); + var models = await client.ApiClient.GetMyConnectionsAsync(options).ConfigureAwait(false); return models.Select(x => RestConnection.Create(x)).ToImmutableArray(); } public static async Task GetInviteAsync(BaseDiscordClient client, - string inviteId) + string inviteId, RequestOptions options) { - var model = await client.ApiClient.GetInviteAsync(inviteId).ConfigureAwait(false); + var model = await client.ApiClient.GetInviteAsync(inviteId, options).ConfigureAwait(false); if (model != null) return RestInvite.Create(client, null, null, model); return null; } public static async Task GetGuildAsync(BaseDiscordClient client, - ulong id) + ulong id, RequestOptions options) { - var model = await client.ApiClient.GetGuildAsync(id).ConfigureAwait(false); + var model = await client.ApiClient.GetGuildAsync(id, options).ConfigureAwait(false); if (model != null) return RestGuild.Create(client, model); return null; } public static async Task GetGuildEmbedAsync(BaseDiscordClient client, - ulong id) + ulong id, RequestOptions options) { - var model = await client.ApiClient.GetGuildEmbedAsync(id).ConfigureAwait(false); + var model = await client.ApiClient.GetGuildEmbedAsync(id, options).ConfigureAwait(false); if (model != null) return RestGuildEmbed.Create(model); return null; } - public static async Task> GetGuildSummariesAsync(BaseDiscordClient client) - { - var models = await client.ApiClient.GetMyGuildsAsync().ConfigureAwait(false); - return models.Select(x => RestUserGuild.Create(client, x)).ToImmutableArray(); - } - public static async Task> GetGuildsAsync(BaseDiscordClient client) - { - var summaryModels = await client.ApiClient.GetMyGuildsAsync().ConfigureAwait(false); - var guilds = ImmutableArray.CreateBuilder(summaryModels.Count); + public static IAsyncEnumerable> GetGuildSummariesAsync(BaseDiscordClient client, + ulong? fromGuildId, int? limit, RequestOptions options) + { + return new PagedAsyncEnumerable( + DiscordConfig.MaxUsersPerBatch, + async (info, ct) => + { + var args = new GetGuildSummariesParams + { + Limit = info.PageSize + }; + if (info.Position != null) + args.AfterGuildId = info.Position.Value; + var models = await client.ApiClient.GetMyGuildsAsync(args, options).ConfigureAwait(false); + return models + .Select(x => RestUserGuild.Create(client, x)) + .ToImmutableArray(); + }, + nextPage: (info, lastPage) => + { + if (lastPage.Count != DiscordConfig.MaxMessagesPerBatch) + return false; + info.Position = lastPage.Max(x => x.Id); + return true; + }, + start: fromGuildId, + count: limit + ); + } + public static async Task> GetGuildsAsync(BaseDiscordClient client, RequestOptions options) + { + var summaryModels = await GetGuildSummariesAsync(client, null, null, options).Flatten(); + var guilds = ImmutableArray.CreateBuilder(); foreach (var summaryModel in summaryModels) { var guildModel = await client.ApiClient.GetGuildAsync(summaryModel.Id).ConfigureAwait(false); @@ -93,39 +117,39 @@ namespace Discord.Rest return guilds.ToImmutable(); } public static async Task CreateGuildAsync(BaseDiscordClient client, - string name, IVoiceRegion region, Stream jpegIcon = null) + string name, IVoiceRegion region, Stream jpegIcon, RequestOptions options) { var args = new CreateGuildParams(name, region.Id); - var model = await client.ApiClient.CreateGuildAsync(args).ConfigureAwait(false); + var model = await client.ApiClient.CreateGuildAsync(args, options).ConfigureAwait(false); return RestGuild.Create(client, model); } public static async Task GetUserAsync(BaseDiscordClient client, - ulong id) + ulong id, RequestOptions options) { - var model = await client.ApiClient.GetUserAsync(id).ConfigureAwait(false); + var model = await client.ApiClient.GetUserAsync(id, options).ConfigureAwait(false); if (model != null) return RestUser.Create(client, model); return null; } public static async Task GetGuildUserAsync(BaseDiscordClient client, - ulong guildId, ulong id) + ulong guildId, ulong id, RequestOptions options) { - var model = await client.ApiClient.GetGuildMemberAsync(guildId, id).ConfigureAwait(false); + var model = await client.ApiClient.GetGuildMemberAsync(guildId, id, options).ConfigureAwait(false); if (model != null) return RestGuildUser.Create(client, new RestGuild(client, guildId), model); return null; } - public static async Task> GetVoiceRegionsAsync(BaseDiscordClient client) + public static async Task> GetVoiceRegionsAsync(BaseDiscordClient client, RequestOptions options) { - var models = await client.ApiClient.GetVoiceRegionsAsync().ConfigureAwait(false); + var models = await client.ApiClient.GetVoiceRegionsAsync(options).ConfigureAwait(false); return models.Select(x => RestVoiceRegion.Create(client, x)).ToImmutableArray(); } public static async Task GetVoiceRegionAsync(BaseDiscordClient client, - string id) + string id, RequestOptions options) { - var models = await client.ApiClient.GetVoiceRegionsAsync().ConfigureAwait(false); + var models = await client.ApiClient.GetVoiceRegionsAsync(options).ConfigureAwait(false); return models.Select(x => RestVoiceRegion.Create(client, x)).Where(x => x.Id == id).FirstOrDefault(); } } diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index 09f5a5767..d57443605 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -1087,10 +1087,18 @@ namespace Discord.API options = RequestOptions.CreateOrClone(options); return await SendAsync>("GET", () => "users/@me/channels", new BucketIds(), options: options).ConfigureAwait(false); } - public async Task> GetMyGuildsAsync(RequestOptions options = null) + public async Task> GetMyGuildsAsync(GetGuildSummariesParams args, RequestOptions options = null) { + Preconditions.NotNull(args, nameof(args)); + Preconditions.GreaterThan(args.Limit, 0, nameof(args.Limit)); + Preconditions.AtMost(args.Limit, DiscordConfig.MaxGuildsPerBatch, nameof(args.Limit)); + Preconditions.GreaterThan(args.AfterGuildId, 0, nameof(args.AfterGuildId)); options = RequestOptions.CreateOrClone(options); - return await SendAsync>("GET", () => "users/@me/guilds", new BucketIds(), options: options).ConfigureAwait(false); + + int limit = args.Limit.GetValueOrDefault(int.MaxValue); + ulong afterGuildId = args.AfterGuildId.GetValueOrDefault(0); + + return await SendAsync>("GET", () => $"users/@me/guilds?limit={limit}&after={afterGuildId}", new BucketIds(), options: options).ConfigureAwait(false); } public async Task GetMyApplicationAsync(RequestOptions options = null) { diff --git a/src/Discord.Net.Rest/DiscordRestClient.cs b/src/Discord.Net.Rest/DiscordRestClient.cs index 0ff1a4821..aa9937008 100644 --- a/src/Discord.Net.Rest/DiscordRestClient.cs +++ b/src/Discord.Net.Rest/DiscordRestClient.cs @@ -35,127 +35,130 @@ namespace Discord.Rest } /// - public async Task GetApplicationInfoAsync() + public async Task GetApplicationInfoAsync(RequestOptions options = null) { - return _applicationInfo ?? (_applicationInfo = await ClientHelper.GetApplicationInfoAsync(this)); + return _applicationInfo ?? (_applicationInfo = await ClientHelper.GetApplicationInfoAsync(this, options)); } /// - public Task GetChannelAsync(ulong id) - => ClientHelper.GetChannelAsync(this, id); + public Task GetChannelAsync(ulong id, RequestOptions options = null) + => ClientHelper.GetChannelAsync(this, id, options); /// - public Task> GetPrivateChannelsAsync() - => ClientHelper.GetPrivateChannelsAsync(this); - public Task> GetDMChannelsAsync() - => ClientHelper.GetDMChannelsAsync(this); - public Task> GetGroupChannelsAsync() - => ClientHelper.GetGroupChannelsAsync(this); + public Task> GetPrivateChannelsAsync(RequestOptions options = null) + => ClientHelper.GetPrivateChannelsAsync(this, options); + public Task> GetDMChannelsAsync(RequestOptions options = null) + => ClientHelper.GetDMChannelsAsync(this, options); + public Task> GetGroupChannelsAsync(RequestOptions options = null) + => ClientHelper.GetGroupChannelsAsync(this, options); /// - public Task> GetConnectionsAsync() - => ClientHelper.GetConnectionsAsync(this); + public Task> GetConnectionsAsync(RequestOptions options = null) + => ClientHelper.GetConnectionsAsync(this, options); /// - public Task GetInviteAsync(string inviteId) - => ClientHelper.GetInviteAsync(this, inviteId); + public Task GetInviteAsync(string inviteId, RequestOptions options = null) + => ClientHelper.GetInviteAsync(this, inviteId, options); /// - public Task GetGuildAsync(ulong id) - => ClientHelper.GetGuildAsync(this, id); + public Task GetGuildAsync(ulong id, RequestOptions options = null) + => ClientHelper.GetGuildAsync(this, id, options); /// - public Task GetGuildEmbedAsync(ulong id) - => ClientHelper.GetGuildEmbedAsync(this, id); + public Task GetGuildEmbedAsync(ulong id, RequestOptions options = null) + => ClientHelper.GetGuildEmbedAsync(this, id, options); /// - public Task> GetGuildSummariesAsync() - => ClientHelper.GetGuildSummariesAsync(this); + public IAsyncEnumerable> GetGuildSummariesAsync(RequestOptions options = null) + => ClientHelper.GetGuildSummariesAsync(this, null, null, options); /// - public Task> GetGuildsAsync() - => ClientHelper.GetGuildsAsync(this); + public IAsyncEnumerable> GetGuildSummariesAsync(ulong fromGuildId, int limit, RequestOptions options = null) + => ClientHelper.GetGuildSummariesAsync(this, fromGuildId, limit, options); /// - public Task CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon = null) - => ClientHelper.CreateGuildAsync(this, name, region, jpegIcon); + public Task> GetGuildsAsync(RequestOptions options = null) + => ClientHelper.GetGuildsAsync(this, options); + /// + public Task CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon = null, RequestOptions options = null) + => ClientHelper.CreateGuildAsync(this, name, region, jpegIcon, options); /// - public Task GetUserAsync(ulong id) - => ClientHelper.GetUserAsync(this, id); + public Task GetUserAsync(ulong id, RequestOptions options = null) + => ClientHelper.GetUserAsync(this, id, options); /// - public Task GetGuildUserAsync(ulong guildId, ulong id) - => ClientHelper.GetGuildUserAsync(this, guildId, id); + public Task GetGuildUserAsync(ulong guildId, ulong id, RequestOptions options = null) + => ClientHelper.GetGuildUserAsync(this, guildId, id, options); /// - public Task> GetVoiceRegionsAsync() - => ClientHelper.GetVoiceRegionsAsync(this); + public Task> GetVoiceRegionsAsync(RequestOptions options = null) + => ClientHelper.GetVoiceRegionsAsync(this, options); /// - public Task GetVoiceRegionAsync(string id) - => ClientHelper.GetVoiceRegionAsync(this, id); + public Task GetVoiceRegionAsync(string id, RequestOptions options = null) + => ClientHelper.GetVoiceRegionAsync(this, id, options); //IDiscordClient - async Task IDiscordClient.GetApplicationInfoAsync() - => await GetApplicationInfoAsync().ConfigureAwait(false); + async Task IDiscordClient.GetApplicationInfoAsync(RequestOptions options) + => await GetApplicationInfoAsync(options).ConfigureAwait(false); - async Task IDiscordClient.GetChannelAsync(ulong id, CacheMode mode) + async Task IDiscordClient.GetChannelAsync(ulong id, CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) - return await GetChannelAsync(id).ConfigureAwait(false); + return await GetChannelAsync(id, options).ConfigureAwait(false); else return null; } - async Task> IDiscordClient.GetPrivateChannelsAsync(CacheMode mode) + async Task> IDiscordClient.GetPrivateChannelsAsync(CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) - return await GetPrivateChannelsAsync().ConfigureAwait(false); + return await GetPrivateChannelsAsync(options).ConfigureAwait(false); else return ImmutableArray.Create(); } - async Task> IDiscordClient.GetDMChannelsAsync(CacheMode mode) + async Task> IDiscordClient.GetDMChannelsAsync(CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) - return await GetDMChannelsAsync().ConfigureAwait(false); + return await GetDMChannelsAsync(options).ConfigureAwait(false); else return ImmutableArray.Create(); } - async Task> IDiscordClient.GetGroupChannelsAsync(CacheMode mode) + async Task> IDiscordClient.GetGroupChannelsAsync(CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) - return await GetGroupChannelsAsync().ConfigureAwait(false); + return await GetGroupChannelsAsync(options).ConfigureAwait(false); else return ImmutableArray.Create(); } - async Task> IDiscordClient.GetConnectionsAsync() - => await GetConnectionsAsync().ConfigureAwait(false); + async Task> IDiscordClient.GetConnectionsAsync(RequestOptions options) + => await GetConnectionsAsync(options).ConfigureAwait(false); - async Task IDiscordClient.GetInviteAsync(string inviteId) - => await GetInviteAsync(inviteId).ConfigureAwait(false); + async Task IDiscordClient.GetInviteAsync(string inviteId, RequestOptions options) + => await GetInviteAsync(inviteId, options).ConfigureAwait(false); - async Task IDiscordClient.GetGuildAsync(ulong id, CacheMode mode) + async Task IDiscordClient.GetGuildAsync(ulong id, CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) - return await GetGuildAsync(id).ConfigureAwait(false); + return await GetGuildAsync(id, options).ConfigureAwait(false); else return null; } - async Task> IDiscordClient.GetGuildsAsync(CacheMode mode) + async Task> IDiscordClient.GetGuildsAsync(CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) - return await GetGuildsAsync().ConfigureAwait(false); + return await GetGuildsAsync(options).ConfigureAwait(false); else return ImmutableArray.Create(); } - async Task IDiscordClient.CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon) - => await CreateGuildAsync(name, region, jpegIcon).ConfigureAwait(false); + async Task IDiscordClient.CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon, RequestOptions options) + => await CreateGuildAsync(name, region, jpegIcon, options).ConfigureAwait(false); - async Task IDiscordClient.GetUserAsync(ulong id, CacheMode mode) + async Task IDiscordClient.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) - return await GetUserAsync(id).ConfigureAwait(false); + return await GetUserAsync(id, options).ConfigureAwait(false); else return null; } - async Task> IDiscordClient.GetVoiceRegionsAsync() - => await GetVoiceRegionsAsync().ConfigureAwait(false); - async Task IDiscordClient.GetVoiceRegionAsync(string id) - => await GetVoiceRegionAsync(id).ConfigureAwait(false); + async Task> IDiscordClient.GetVoiceRegionsAsync(RequestOptions options) + => await GetVoiceRegionsAsync(options).ConfigureAwait(false); + async Task IDiscordClient.GetVoiceRegionAsync(string id, RequestOptions options) + => await GetVoiceRegionAsync(id, options).ConfigureAwait(false); } } diff --git a/src/Discord.Net.Rpc/DiscordRpcClient.cs b/src/Discord.Net.Rpc/DiscordRpcClient.cs index 01d641204..9c77fc919 100644 --- a/src/Discord.Net.Rpc/DiscordRpcClient.cs +++ b/src/Discord.Net.Rpc/DiscordRpcClient.cs @@ -468,7 +468,7 @@ namespace Discord.Rpc //IDiscordClient ConnectionState IDiscordClient.ConnectionState => _connection.State; - Task IDiscordClient.GetApplicationInfoAsync() => Task.FromResult(ApplicationInfo); + Task IDiscordClient.GetApplicationInfoAsync(RequestOptions options) => Task.FromResult(ApplicationInfo); async Task IDiscordClient.StartAsync() => await StartAsync().ConfigureAwait(false); diff --git a/src/Discord.Net.WebSocket/DiscordShardedClient.cs b/src/Discord.Net.WebSocket/DiscordShardedClient.cs index e65de38b5..874d34557 100644 --- a/src/Discord.Net.WebSocket/DiscordShardedClient.cs +++ b/src/Discord.Net.WebSocket/DiscordShardedClient.cs @@ -145,7 +145,7 @@ namespace Discord.WebSocket public SocketGuild GetGuild(ulong id) => GetShardFor(id).GetGuild(id); /// public Task CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon = null) - => ClientHelper.CreateGuildAsync(this, name, region, jpegIcon); + => ClientHelper.CreateGuildAsync(this, name, region, jpegIcon, new RequestOptions()); /// public SocketChannel GetChannel(ulong id) @@ -176,7 +176,7 @@ namespace Discord.WebSocket /// public Task> GetConnectionsAsync() - => ClientHelper.GetConnectionsAsync(this); + => ClientHelper.GetConnectionsAsync(this, new RequestOptions()); private IEnumerable GetGuilds() { @@ -196,7 +196,7 @@ namespace Discord.WebSocket /// public Task GetInviteAsync(string inviteId) - => ClientHelper.GetInviteAsync(this, inviteId); + => ClientHelper.GetInviteAsync(this, inviteId, new RequestOptions()); /// public SocketUser GetUser(ulong id) @@ -314,35 +314,35 @@ namespace Discord.WebSocket } //IDiscordClient - async Task IDiscordClient.GetApplicationInfoAsync() + async Task IDiscordClient.GetApplicationInfoAsync(RequestOptions options) => await GetApplicationInfoAsync().ConfigureAwait(false); - Task IDiscordClient.GetChannelAsync(ulong id, CacheMode mode) + Task IDiscordClient.GetChannelAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetChannel(id)); - Task> IDiscordClient.GetPrivateChannelsAsync(CacheMode mode) + Task> IDiscordClient.GetPrivateChannelsAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(PrivateChannels); - async Task> IDiscordClient.GetConnectionsAsync() + async Task> IDiscordClient.GetConnectionsAsync(RequestOptions options) => await GetConnectionsAsync().ConfigureAwait(false); - async Task IDiscordClient.GetInviteAsync(string inviteId) + async Task IDiscordClient.GetInviteAsync(string inviteId, RequestOptions options) => await GetInviteAsync(inviteId).ConfigureAwait(false); - Task IDiscordClient.GetGuildAsync(ulong id, CacheMode mode) + Task IDiscordClient.GetGuildAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetGuild(id)); - Task> IDiscordClient.GetGuildsAsync(CacheMode mode) + Task> IDiscordClient.GetGuildsAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(Guilds); - async Task IDiscordClient.CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon) + async Task IDiscordClient.CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon, RequestOptions options) => await CreateGuildAsync(name, region, jpegIcon).ConfigureAwait(false); - Task IDiscordClient.GetUserAsync(ulong id, CacheMode mode) + Task IDiscordClient.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetUser(id)); - Task IDiscordClient.GetUserAsync(string username, string discriminator) + Task IDiscordClient.GetUserAsync(string username, string discriminator, RequestOptions options) => Task.FromResult(GetUser(username, discriminator)); - Task> IDiscordClient.GetVoiceRegionsAsync() + Task> IDiscordClient.GetVoiceRegionsAsync(RequestOptions options) => Task.FromResult>(VoiceRegions); - Task IDiscordClient.GetVoiceRegionAsync(string id) + Task IDiscordClient.GetVoiceRegionAsync(string id, RequestOptions options) => Task.FromResult(GetVoiceRegion(id)); } } diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index 76e943ce4..30828d88b 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -237,7 +237,7 @@ namespace Discord.WebSocket /// public async Task GetApplicationInfoAsync() { - return _applicationInfo ?? (_applicationInfo = await ClientHelper.GetApplicationInfoAsync(this)); + return _applicationInfo ?? (_applicationInfo = await ClientHelper.GetApplicationInfoAsync(this, new RequestOptions())); } /// @@ -247,7 +247,7 @@ namespace Discord.WebSocket } /// public Task CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon = null) - => ClientHelper.CreateGuildAsync(this, name, region, jpegIcon); + => ClientHelper.CreateGuildAsync(this, name, region, jpegIcon, new RequestOptions()); /// public SocketChannel GetChannel(ulong id) @@ -257,11 +257,11 @@ namespace Discord.WebSocket /// public Task> GetConnectionsAsync() - => ClientHelper.GetConnectionsAsync(this); + => ClientHelper.GetConnectionsAsync(this, new RequestOptions()); /// public Task GetInviteAsync(string inviteId) - => ClientHelper.GetInviteAsync(this, inviteId); + => ClientHelper.GetInviteAsync(this, inviteId, new RequestOptions()); /// public SocketUser GetUser(ulong id) @@ -1726,39 +1726,39 @@ namespace Discord.WebSocket } //IDiscordClient - async Task IDiscordClient.GetApplicationInfoAsync() + async Task IDiscordClient.GetApplicationInfoAsync(RequestOptions options) => await GetApplicationInfoAsync().ConfigureAwait(false); - Task IDiscordClient.GetChannelAsync(ulong id, CacheMode mode) + Task IDiscordClient.GetChannelAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetChannel(id)); - Task> IDiscordClient.GetPrivateChannelsAsync(CacheMode mode) + Task> IDiscordClient.GetPrivateChannelsAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(PrivateChannels); - Task> IDiscordClient.GetDMChannelsAsync(CacheMode mode) + Task> IDiscordClient.GetDMChannelsAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(DMChannels); - Task> IDiscordClient.GetGroupChannelsAsync(CacheMode mode) + Task> IDiscordClient.GetGroupChannelsAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(GroupChannels); - async Task> IDiscordClient.GetConnectionsAsync() + async Task> IDiscordClient.GetConnectionsAsync(RequestOptions options) => await GetConnectionsAsync().ConfigureAwait(false); - async Task IDiscordClient.GetInviteAsync(string inviteId) + async Task IDiscordClient.GetInviteAsync(string inviteId, RequestOptions options) => await GetInviteAsync(inviteId).ConfigureAwait(false); - Task IDiscordClient.GetGuildAsync(ulong id, CacheMode mode) + Task IDiscordClient.GetGuildAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetGuild(id)); - Task> IDiscordClient.GetGuildsAsync(CacheMode mode) + Task> IDiscordClient.GetGuildsAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(Guilds); - async Task IDiscordClient.CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon) + async Task IDiscordClient.CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon, RequestOptions options) => await CreateGuildAsync(name, region, jpegIcon).ConfigureAwait(false); - Task IDiscordClient.GetUserAsync(ulong id, CacheMode mode) + Task IDiscordClient.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetUser(id)); - Task IDiscordClient.GetUserAsync(string username, string discriminator) + Task IDiscordClient.GetUserAsync(string username, string discriminator, RequestOptions options) => Task.FromResult(GetUser(username, discriminator)); - Task> IDiscordClient.GetVoiceRegionsAsync() + Task> IDiscordClient.GetVoiceRegionsAsync(RequestOptions options) => Task.FromResult>(VoiceRegions); - Task IDiscordClient.GetVoiceRegionAsync(string id) + Task IDiscordClient.GetVoiceRegionAsync(string id, RequestOptions options) => Task.FromResult(GetVoiceRegion(id)); async Task IDiscordClient.StartAsync() From 158ce0f9222dc87f8b8fd8f138c3814b581dcdbf Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 31 Mar 2017 03:59:12 -0300 Subject: [PATCH 063/243] Started adding IAudioClient incoming stream creation/destruction events --- src/Discord.Net.Core/Audio/IAudioClient.cs | 4 +- .../Entities/Channels/IAudioChannel.cs | 8 ++- .../Entities/Channels/IVoiceChannel.cs | 5 +- .../Entities/Channels/RestGroupChannel.cs | 6 +- .../Entities/Channels/RestVoiceChannel.cs | 4 +- .../Entities/Channels/RpcGroupChannel.cs | 6 +- .../Entities/Channels/RpcVoiceChannel.cs | 4 +- .../Audio/AudioClient.cs | 29 ++++++-- .../DiscordSocketClient.cs | 6 +- .../Entities/Channels/ISocketAudioChannel.cs | 1 - .../Entities/Channels/SocketGroupChannel.cs | 3 + .../Entities/Channels/SocketVoiceChannel.cs | 4 +- .../Entities/Guilds/SocketGuild.cs | 72 ++++++++++--------- 13 files changed, 94 insertions(+), 58 deletions(-) diff --git a/src/Discord.Net.Core/Audio/IAudioClient.cs b/src/Discord.Net.Core/Audio/IAudioClient.cs index bea44fcf4..149905654 100644 --- a/src/Discord.Net.Core/Audio/IAudioClient.cs +++ b/src/Discord.Net.Core/Audio/IAudioClient.cs @@ -8,7 +8,9 @@ namespace Discord.Audio event Func Connected; event Func Disconnected; event Func LatencyUpdated; - + event Func StreamCreated; + event Func StreamDestroyed; + /// Gets the current connection state of this client. ConnectionState ConnectionState { get; } /// Gets the estimated round-trip latency, in milliseconds, to the gateway server. diff --git a/src/Discord.Net.Core/Entities/Channels/IAudioChannel.cs b/src/Discord.Net.Core/Entities/Channels/IAudioChannel.cs index 6c9507299..afb81d92f 100644 --- a/src/Discord.Net.Core/Entities/Channels/IAudioChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IAudioChannel.cs @@ -1,6 +1,12 @@ -namespace Discord +using Discord.Audio; +using System; +using System.Threading.Tasks; + +namespace Discord { public interface IAudioChannel : IChannel { + /// Connects to this audio channel. + Task ConnectAsync(Action configAction = null); } } diff --git a/src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs b/src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs index 80c90e4bd..e2a2ad8eb 100644 --- a/src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs @@ -1,5 +1,4 @@ -using Discord.Audio; -using System; +using System; using System.Threading.Tasks; namespace Discord @@ -13,7 +12,5 @@ namespace Discord /// Modifies this voice channel. Task ModifyAsync(Action func, RequestOptions options = null); - /// Connects to this voice channel. - Task ConnectAsync(); } } \ No newline at end of file diff --git a/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs index a4b49b118..e3ba4e94b 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs @@ -1,4 +1,5 @@ -using System; +using Discord.Audio; +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; @@ -145,6 +146,9 @@ namespace Discord.Rest IDisposable IMessageChannel.EnterTypingState(RequestOptions options) => EnterTypingState(options); + //IAudioChannel + Task IAudioChannel.ConnectAsync(Action configAction) { throw new NotSupportedException(); } + //IChannel Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetUser(id)); diff --git a/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs index e5330f29e..300ebd08d 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs @@ -40,8 +40,8 @@ namespace Discord.Rest private string DebuggerDisplay => $"{Name} ({Id}, Voice)"; - //IVoiceChannel - Task IVoiceChannel.ConnectAsync() { throw new NotSupportedException(); } + //IAudioChannel + Task IAudioChannel.ConnectAsync(Action configAction) { throw new NotSupportedException(); } //IGuildChannel Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs b/src/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs index 504bf8670..c365ad4ff 100644 --- a/src/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs +++ b/src/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs @@ -1,4 +1,5 @@ -using Discord.Rest; +using Discord.Audio; +using Discord.Rest; using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -112,6 +113,9 @@ namespace Discord.Rpc IDisposable IMessageChannel.EnterTypingState(RequestOptions options) => EnterTypingState(options); + //IAudioChannel + Task IAudioChannel.ConnectAsync(Action configAction) { throw new NotSupportedException(); } + //IChannel string IChannel.Name { get { throw new NotSupportedException(); } } diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcVoiceChannel.cs b/src/Discord.Net.Rpc/Entities/Channels/RpcVoiceChannel.cs index 3d5acfda9..067da6764 100644 --- a/src/Discord.Net.Rpc/Entities/Channels/RpcVoiceChannel.cs +++ b/src/Discord.Net.Rpc/Entities/Channels/RpcVoiceChannel.cs @@ -42,7 +42,7 @@ namespace Discord.Rpc private string DebuggerDisplay => $"{Name} ({Id}, Voice)"; - //IVoiceChannel - Task IVoiceChannel.ConnectAsync() { throw new NotSupportedException(); } + //IAudioChannel + Task IAudioChannel.ConnectAsync(Action configAction) { throw new NotSupportedException(); } } } diff --git a/src/Discord.Net.WebSocket/Audio/AudioClient.cs b/src/Discord.Net.WebSocket/Audio/AudioClient.cs index 226d8eb7f..bb5a62438 100644 --- a/src/Discord.Net.WebSocket/Audio/AudioClient.cs +++ b/src/Discord.Net.WebSocket/Audio/AudioClient.cs @@ -47,6 +47,18 @@ namespace Discord.Audio remove { _latencyUpdatedEvent.Remove(value); } } private readonly AsyncEvent> _latencyUpdatedEvent = new AsyncEvent>(); + public event Func StreamCreated + { + add { _streamCreated.Add(value); } + remove { _streamCreated.Remove(value); } + } + private readonly AsyncEvent> _streamCreated = new AsyncEvent>(); + public event Func StreamDestroyed + { + add { _streamDestroyed.Add(value); } + remove { _streamDestroyed.Remove(value); } + } + private readonly AsyncEvent> _streamDestroyed = new AsyncEvent>(); private readonly Logger _audioLogger; private readonly JsonSerializer _serializer; @@ -182,7 +194,7 @@ namespace Discord.Audio throw new ArgumentException("Value must be 120, 240, 480, 960, 1920 or 2880", nameof(samplesPerFrame)); } - internal void CreateInputStream(ulong userId) + internal async Task CreateInputStreamAsync(ulong userId) { //Assume Thread-safe if (!_streams.ContainsKey(userId)) @@ -190,6 +202,7 @@ namespace Discord.Audio var readerStream = new InputStream(); var writerStream = new OpusDecodeStream(new RTPReadStream(readerStream, _secretKey)); _streams.TryAdd(userId, new StreamPair(readerStream, writerStream)); + await _streamCreated.InvokeAsync(userId, readerStream); } } internal AudioInStream GetInputStream(ulong id) @@ -199,14 +212,18 @@ namespace Discord.Audio return streamPair.Reader; return null; } - internal void RemoveInputStream(ulong userId) + internal async Task RemoveInputStreamAsync(ulong userId) { - _streams.TryRemove(userId, out var ignored); + if (_streams.TryRemove(userId, out var ignored)) + await _streamDestroyed.InvokeAsync(userId).ConfigureAwait(false); } - internal void ClearInputStreams() + internal async Task ClearInputStreamsAsync() { - foreach (var pair in _streams.Values) - pair.Reader.Dispose(); + foreach (var pair in _streams) + { + pair.Value.Reader.Dispose(); + await _streamDestroyed.InvokeAsync(pair.Key).ConfigureAwait(false); + } _ssrcMap.Clear(); _streams.Clear(); } diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index 30828d88b..8307b4a36 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -1414,7 +1414,7 @@ namespace Discord.WebSocket if (data.ChannelId != null) { before = guild.GetVoiceState(data.UserId)?.Clone() ?? SocketVoiceState.Default; - after = guild.AddOrUpdateVoiceState(State, data); + after = await guild.AddOrUpdateVoiceStateAsync(State, data).ConfigureAwait(false); /*if (data.UserId == CurrentUser.Id) { var _ = guild.FinishJoinAudioChannel().ConfigureAwait(false); @@ -1471,7 +1471,7 @@ namespace Discord.WebSocket if (guild != null) { string endpoint = data.Endpoint.Substring(0, data.Endpoint.LastIndexOf(':')); - var _ = guild.FinishConnectAudio(_nextAudioId++, endpoint, data.Token).ConfigureAwait(false); + var _ = guild.FinishConnectAudio(endpoint, data.Token).ConfigureAwait(false); } else { @@ -1725,6 +1725,8 @@ namespace Discord.WebSocket } } + internal int GetAudioId() => _nextAudioId++; + //IDiscordClient async Task IDiscordClient.GetApplicationInfoAsync(RequestOptions options) => await GetApplicationInfoAsync().ConfigureAwait(false); diff --git a/src/Discord.Net.WebSocket/Entities/Channels/ISocketAudioChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/ISocketAudioChannel.cs index 7b9bf07f0..c15eaf17b 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/ISocketAudioChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/ISocketAudioChannel.cs @@ -5,6 +5,5 @@ namespace Discord.WebSocket { public interface ISocketAudioChannel : IAudioChannel { - Task ConnectAsync(); } } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs index ceba50a6e..e6c875e5a 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs @@ -212,6 +212,9 @@ namespace Discord.WebSocket IDisposable IMessageChannel.EnterTypingState(RequestOptions options) => EnterTypingState(options); + //IAudioChannel + Task IAudioChannel.ConnectAsync(Action configAction) { throw new NotSupportedException(); } + //IChannel Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetUser(id)); diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs index 9ec0da72a..e8a669845 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs @@ -40,9 +40,9 @@ namespace Discord.WebSocket public Task ModifyAsync(Action func, RequestOptions options = null) => ChannelHelper.ModifyAsync(this, Discord, func, options); - public async Task ConnectAsync() + public async Task ConnectAsync(Action configAction = null) { - return await Guild.ConnectAudioAsync(Id, false, false).ConfigureAwait(false); + return await Guild.ConnectAudioAsync(Id, false, false, configAction).ConfigureAwait(false); } public override SocketGuildUser GetUser(ulong id) diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index 7de5eda77..d240aac1e 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -423,7 +423,7 @@ namespace Discord.WebSocket } //Voice States - internal SocketVoiceState AddOrUpdateVoiceState(ClientState state, VoiceStateModel model) + internal async Task AddOrUpdateVoiceStateAsync(ClientState state, VoiceStateModel model) { var voiceChannel = state.GetChannel(model.ChannelId.Value) as SocketVoiceChannel; var before = GetVoiceState(model.UserId) ?? SocketVoiceState.Default; @@ -433,12 +433,12 @@ namespace Discord.WebSocket if (_audioClient != null && before.VoiceChannel?.Id != after.VoiceChannel?.Id) { if (model.UserId == CurrentUser.Id) - RepopulateAudioStreams(); + await RepopulateAudioStreamsAsync().ConfigureAwait(false); else { - _audioClient.RemoveInputStream(model.UserId); //User changed channels, end their stream + await _audioClient.RemoveInputStreamAsync(model.UserId).ConfigureAwait(false); //User changed channels, end their stream if (CurrentUser.VoiceChannel != null && after.VoiceChannel?.Id == CurrentUser.VoiceChannel?.Id) - _audioClient.CreateInputStream(model.UserId); + await _audioClient.CreateInputStreamAsync(model.UserId).ConfigureAwait(false); } } @@ -464,7 +464,7 @@ namespace Discord.WebSocket { return _audioClient?.GetInputStream(userId); } - internal async Task ConnectAudioAsync(ulong channelId, bool selfDeaf, bool selfMute) + internal async Task ConnectAudioAsync(ulong channelId, bool selfDeaf, bool selfMute, Action configAction) { selfDeaf = false; selfMute = false; @@ -477,6 +477,32 @@ namespace Discord.WebSocket await DisconnectAudioInternalAsync().ConfigureAwait(false); promise = new TaskCompletionSource(); _audioConnectPromise = promise; + + if (_audioClient == null) + { + var audioClient = new AudioClient(this, Discord.GetAudioId()); + audioClient.Disconnected += async ex => + { + if (!promise.Task.IsCompleted) + { + try { audioClient.Dispose(); } catch { } + _audioClient = null; + if (ex != null) + await promise.TrySetExceptionAsync(ex); + else + await promise.TrySetCanceledAsync(); + return; + } + }; + audioClient.Connected += () => + { + var _ = promise.TrySetResultAsync(_audioClient); + return Task.Delay(0); + }; + configAction?.Invoke(audioClient); + _audioClient = audioClient; + } + await Discord.ApiClient.SendVoiceStateUpdateAsync(Id, channelId, selfDeaf, selfMute).ConfigureAwait(false); } catch (Exception) @@ -523,7 +549,7 @@ namespace Discord.WebSocket await _audioClient.StopAsync().ConfigureAwait(false); _audioClient = null; } - internal async Task FinishConnectAudio(int id, string url, string token) + internal async Task FinishConnectAudio(string url, string token) { //TODO: Mem Leak: Disconnected/Connected handlers arent cleaned up var voiceState = GetVoiceState(Discord.CurrentUser.Id).Value; @@ -531,31 +557,7 @@ namespace Discord.WebSocket await _audioLock.WaitAsync().ConfigureAwait(false); try { - var promise = _audioConnectPromise; - if (_audioClient == null) - { - var audioClient = new AudioClient(this, id); - audioClient.Disconnected += async ex => - { - if (!promise.Task.IsCompleted) - { - try { audioClient.Dispose(); } catch { } - _audioClient = null; - if (ex != null) - await promise.TrySetExceptionAsync(ex); - else - await promise.TrySetCanceledAsync(); - return; - } - }; - _audioClient = audioClient; - RepopulateAudioStreams(); - } - _audioClient.Connected += () => - { - var _ = promise.TrySetResultAsync(_audioClient); - return Task.Delay(0); - }; + await RepopulateAudioStreamsAsync().ConfigureAwait(false); await _audioClient.StartAsync(url, Discord.CurrentUser.Id, voiceState.VoiceSessionId, token).ConfigureAwait(false); } catch (OperationCanceledException) @@ -573,15 +575,15 @@ namespace Discord.WebSocket } } - internal void RepopulateAudioStreams() + internal async Task RepopulateAudioStreamsAsync() { - _audioClient.ClearInputStreams(); //We changed channels, end all current streams + await _audioClient.ClearInputStreamsAsync().ConfigureAwait(false); //We changed channels, end all current streams if (CurrentUser.VoiceChannel != null) { foreach (var pair in _voiceStates) { - if (pair.Value.VoiceChannel?.Id == CurrentUser.VoiceChannel?.Id) - _audioClient.CreateInputStream(pair.Key); + if (pair.Value.VoiceChannel?.Id == CurrentUser.VoiceChannel?.Id && pair.Key != CurrentUser.Id) + await _audioClient.CreateInputStreamAsync(pair.Key).ConfigureAwait(false); } } } From 57013d5639f2bdfc13ee955f6b89dcd1c5612488 Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 31 Mar 2017 13:34:03 -0300 Subject: [PATCH 064/243] Don't crash if a rate limit header is unparsable --- src/Discord.Net.Rest/Net/RateLimitInfo.cs | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/Discord.Net.Rest/Net/RateLimitInfo.cs b/src/Discord.Net.Rest/Net/RateLimitInfo.cs index d8d168aec..79fe47dd1 100644 --- a/src/Discord.Net.Rest/Net/RateLimitInfo.cs +++ b/src/Discord.Net.Rest/Net/RateLimitInfo.cs @@ -15,14 +15,18 @@ namespace Discord.Net internal RateLimitInfo(Dictionary headers) { string temp; - IsGlobal = headers.TryGetValue("X-RateLimit-Global", out temp) ? bool.Parse(temp) : false; - Limit = headers.TryGetValue("X-RateLimit-Limit", out temp) ? int.Parse(temp) : (int?)null; - Remaining = headers.TryGetValue("X-RateLimit-Remaining", out temp) ? int.Parse(temp) : (int?)null; - Reset = headers.TryGetValue("X-RateLimit-Reset", out temp) ? - DateTimeUtils.FromUnixSeconds(int.Parse(temp)) : (DateTimeOffset?)null; - RetryAfter = headers.TryGetValue("Retry-After", out temp) ? int.Parse(temp) : (int?)null; - Lag = headers.TryGetValue("Date", out temp) ? - DateTimeOffset.UtcNow - DateTimeOffset.Parse(temp) : (TimeSpan?)null; + IsGlobal = headers.TryGetValue("X-RateLimit-Global", out temp) && + bool.TryParse(temp, out var isGlobal) ? isGlobal : false; + Limit = headers.TryGetValue("X-RateLimit-Limit", out temp) && + int.TryParse(temp, out var limit) ? limit : (int?)null; + Remaining = headers.TryGetValue("X-RateLimit-Remaining", out temp) && + int.TryParse(temp, out var remaining) ? remaining : (int?)null; + Reset = headers.TryGetValue("X-RateLimit-Reset", out temp) && + int.TryParse(temp, out var reset) ? DateTimeUtils.FromUnixSeconds(reset) : (DateTimeOffset?)null; + RetryAfter = headers.TryGetValue("Retry-After", out temp) && + int.TryParse(temp, out var retryAfter) ? retryAfter : (int?)null; + Lag = headers.TryGetValue("Date", out temp) && + DateTimeOffset.TryParse(temp, out var date) ? DateTimeOffset.UtcNow - date : (TimeSpan?)null; } } } From 7d1cae8ae8e0f11f7920af07210225dfd266caa2 Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 31 Mar 2017 14:34:10 -0300 Subject: [PATCH 065/243] Fixed tag parsing with incomplete tags --- src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs b/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs index f347563ad..56afb74ae 100644 --- a/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs +++ b/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs @@ -110,11 +110,12 @@ namespace Discord.Rest mentionedRole = guild.GetRole(id); tags.Add(new Tag(TagType.RoleMention, index, content.Length, id, mentionedRole)); } - else + else if (Emoji.TryParse(content, out var emoji)) + tags.Add(new Tag(TagType.Emoji, index, content.Length, emoji.Id ?? 0, emoji)); + else //Bad Tag { - Emoji emoji; - if (Emoji.TryParse(content, out emoji)) - tags.Add(new Tag(TagType.Emoji, index, content.Length, id, emoji)); + index = index + 1; + continue; } index = endIndex + 1; } From 8dfa0220c32c50fc2f593768876ddf85f1f2e0f2 Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 31 Mar 2017 15:18:35 -0300 Subject: [PATCH 066/243] Prevent overlapping tags --- src/Discord.Net.Core/Utils/MentionUtils.cs | 9 +++++++++ .../Entities/Messages/MessageHelper.cs | 16 ++++++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net.Core/Utils/MentionUtils.cs b/src/Discord.Net.Core/Utils/MentionUtils.cs index 4d9add8fd..60e065b62 100644 --- a/src/Discord.Net.Core/Utils/MentionUtils.cs +++ b/src/Discord.Net.Core/Utils/MentionUtils.cs @@ -256,6 +256,15 @@ namespace Discord if (mode != TagHandling.Remove) { Emoji emoji = (Emoji)tag.Value; + + //Remove if its name contains any bad chars (prevents a few tag exploits) + for (int i = 0; i < emoji.Name.Length; i++) + { + char c = emoji.Name[i]; + if (!char.IsLetterOrDigit(c) && c != '_' && c != '-') + return ""; + } + switch (mode) { case TagHandling.Name: diff --git a/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs b/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs index 56afb74ae..d872901fa 100644 --- a/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs +++ b/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs @@ -126,7 +126,8 @@ namespace Discord.Rest index = text.IndexOf("@everyone", index); if (index == -1) break; - tags.Add(new Tag(TagType.EveryoneMention, index, "@everyone".Length, 0, null)); + if (!TagOverlaps(tags, index)) + tags.Add(new Tag(TagType.EveryoneMention, index, "@everyone".Length, 0, null)); index++; } @@ -136,12 +137,23 @@ namespace Discord.Rest index = text.IndexOf("@here", index); if (index == -1) break; - tags.Add(new Tag(TagType.HereMention, index, "@here".Length, 0, null)); + if (!TagOverlaps(tags, index)) + tags.Add(new Tag(TagType.HereMention, index, "@here".Length, 0, null)); index++; } return tags.ToImmutable(); } + private static bool TagOverlaps(IReadOnlyList tags, int index) + { + for (int i = 0; i < tags.Count; i++) + { + var tag = tags[i]; + if (index >= tag.Index && index < tag.Index + tag.Length) + return true; + } + return false; + } public static ImmutableArray FilterTagsByKey(TagType type, ImmutableArray tags) { return tags From b5d817f1712163746a1e473f459c12b42c683755 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 1 Apr 2017 11:11:09 -0300 Subject: [PATCH 067/243] Cleaned up csprojs, added support for tag builds. --- Discord.Net.sln | 10 ++++---- Discord.Net.targets | 20 ++++++++++++++++ build.ps1 | 4 ++-- pack.ps1 | 21 ++++++++++------- .../Discord.Net.Analyzers.csproj | 22 ++++-------------- .../Discord.Net.Commands.csproj | 22 ++++-------------- src/Discord.Net.Core/Discord.Net.Core.csproj | 22 ++++-------------- .../Discord.Net.DebugTools.csproj | 20 +++------------- .../Discord.Net.Providers.UdpClient.csproj | 22 ++++-------------- .../Discord.Net.Providers.WS4Net.csproj | 23 ++++--------------- .../Discord.Net.Relay.csproj | 20 +++------------- src/Discord.Net.Rest/Discord.Net.Rest.csproj | 20 +++------------- src/Discord.Net.Rpc/Discord.Net.Rpc.csproj | 20 +++------------- .../Discord.Net.WebSocket.csproj | 22 ++++-------------- .../Discord.Net.Webhook.csproj | 20 +++------------- src/Discord.Net/Discord.Net.nuspec | 22 +++++++++--------- .../Discord.Net.Tests.csproj | 6 +++-- 17 files changed, 93 insertions(+), 223 deletions(-) create mode 100644 Discord.Net.targets diff --git a/Discord.Net.sln b/Discord.Net.sln index ac75f147e..1c8347293 100644 --- a/Discord.Net.sln +++ b/Discord.Net.sln @@ -24,11 +24,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Extensions", "Extensions", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Tests", "test\Discord.Net.Tests\Discord.Net.Tests.csproj", "{C38E5BC1-11CB-4101-8A38-5B40A1BC6433}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{F66D75C0-E304-46E0-9C3A-294F340DB37D}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Relay", "src\Discord.Net.Relay\Discord.Net.Relay.csproj", "{2705FCB3-68C9-4CEB-89CC-01F8EC80512B}" EndProject -Project("{13B669BE-BB05-4DDF-9536-439F39A36129}") = "Discord.Net.Relay", "src\Discord.Net.Relay\Discord.Net.Relay.csproj", "{2705FCB3-68C9-4CEB-89CC-01F8EC80512B}" -EndProject -Project("{13B669BE-BB05-4DDF-9536-439F39A36129}") = "Discord.Net.Webhook", "src\Discord.Net.Webhook\Discord.Net.Webhook.csproj", "{9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Webhook", "src\Discord.Net.Webhook\Discord.Net.Webhook.csproj", "{9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -171,7 +169,7 @@ Global {688FD1D8-7F01-4539-B2E9-F473C5D699C7} = {288C363D-A636-4EAE-9AC1-4698B641B26E} {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7} = {B0657AAE-DCC5-4FBF-8E5D-1FB578CF3012} {ABC9F4B9-2452-4725-B522-754E0A02E282} = {B0657AAE-DCC5-4FBF-8E5D-1FB578CF3012} - {2705FCB3-68C9-4CEB-89CC-01F8EC80512B} = {F66D75C0-E304-46E0-9C3A-294F340DB37D} - {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30} = {F66D75C0-E304-46E0-9C3A-294F340DB37D} + {2705FCB3-68C9-4CEB-89CC-01F8EC80512B} = {CC3D4B1C-9DE0-448B-8AE7-F3F1F3EC5C3A} + {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30} = {CC3D4B1C-9DE0-448B-8AE7-F3F1F3EC5C3A} EndGlobalSection EndGlobal diff --git a/Discord.Net.targets b/Discord.Net.targets new file mode 100644 index 000000000..867af36ec --- /dev/null +++ b/Discord.Net.targets @@ -0,0 +1,20 @@ + + + 1.0.0 + rc + $(VersionSuffix)-dev + $(VersionSuffix)-$(BuildNumber) + RogueException + discord;discordapp + https://github.com/RogueException/Discord.Net + http://opensource.org/licenses/MIT + git + git://github.com/RogueException/Discord.Net + true + + + $(NoWarn);CS1573;CS1591 + true + true + + \ No newline at end of file diff --git a/build.ps1 b/build.ps1 index 08508bbcf..1b960011a 100644 --- a/build.ps1 +++ b/build.ps1 @@ -1,4 +1,4 @@ -appveyor-retry dotnet restore Discord.Net.sln -v Minimal /p:BuildNumber="$Env:BUILD" +appveyor-retry dotnet restore Discord.Net.sln -v Minimal /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) } -dotnet build Discord.Net.sln -c "Release" /p:BuildNumber="$Env:BUILD" +dotnet build Discord.Net.sln -c "Release" /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) } \ No newline at end of file diff --git a/pack.ps1 b/pack.ps1 index 0f84ea309..c14f5402d 100644 --- a/pack.ps1 +++ b/pack.ps1 @@ -1,17 +1,22 @@ -dotnet pack "src\Discord.Net.Core\Discord.Net.Core.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" +dotnet pack "src\Discord.Net.Core\Discord.Net.Core.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) } -dotnet pack "src\Discord.Net.Rest\Discord.Net.Rest.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" +dotnet pack "src\Discord.Net.Rest\Discord.Net.Rest.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) } -dotnet pack "src\Discord.Net.WebSocket\Discord.Net.WebSocket.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" +dotnet pack "src\Discord.Net.WebSocket\Discord.Net.WebSocket.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) } -dotnet pack "src\Discord.Net.Rpc\Discord.Net.Rpc.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" +dotnet pack "src\Discord.Net.Rpc\Discord.Net.Rpc.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) } -dotnet pack "src\Discord.Net.Commands\Discord.Net.Commands.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" +dotnet pack "src\Discord.Net.Commands\Discord.Net.Commands.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) } -dotnet pack "src\Discord.Net.Providers.WS4Net\Discord.Net.Providers.WS4Net.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" +dotnet pack "src\Discord.Net.Providers.WS4Net\Discord.Net.Providers.WS4Net.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) } -dotnet pack "src\Discord.Net.Providers.UdpClient\Discord.Net.Providers.UdpClient.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" +dotnet pack "src\Discord.Net.Providers.UdpClient\Discord.Net.Providers.UdpClient.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) } -nuget pack src\Discord.Net\Discord.Net.nuspec -OutputDirectory "artifacts" -properties build="$Env:BUILD" +if ($Env:APPVEYOR_REPO_TAG -eq "true") { + nuget pack src\Discord.Net\Discord.Net.nuspec -OutputDirectory "artifacts" -properties suffix="-$Env:BUILD" +} +else { + nuget pack src\Discord.Net\Discord.Net.nuspec -OutputDirectory "artifacts" -properties suffix="" +} if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) } \ No newline at end of file diff --git a/src/Discord.Net.Analyzers/Discord.Net.Analyzers.csproj b/src/Discord.Net.Analyzers/Discord.Net.Analyzers.csproj index 0612e423f..129da98fe 100644 --- a/src/Discord.Net.Analyzers/Discord.Net.Analyzers.csproj +++ b/src/Discord.Net.Analyzers/Discord.Net.Analyzers.csproj @@ -1,20 +1,11 @@ + - 1.0.0 - rc-dev - rc-$(BuildNumber) - netstandard1.3 Discord.Net.Analyzers - RogueException - A Discord.Net extension adding compile-time analysis. - discord;discordapp - https://github.com/RogueException/Discord.Net - http://opensource.org/licenses/MIT - git - git://github.com/RogueException/Discord.Net Discord.Analyzers - portable-net45+win81 - true + A Discord.Net extension adding compile-time analysis. + netstandard1.3 + $(PackageTargetFallback);portable-net45+win81 @@ -24,9 +15,4 @@ all - - $(NoWarn);CS1573;CS1591 - true - true - \ No newline at end of file diff --git a/src/Discord.Net.Commands/Discord.Net.Commands.csproj b/src/Discord.Net.Commands/Discord.Net.Commands.csproj index 452b52f21..05853109a 100644 --- a/src/Discord.Net.Commands/Discord.Net.Commands.csproj +++ b/src/Discord.Net.Commands/Discord.Net.Commands.csproj @@ -1,26 +1,12 @@ - + + - 1.0.0 - rc-dev - rc-$(BuildNumber) - netstandard1.1;netstandard1.3 Discord.Net.Commands - RogueException - A Discord.Net extension adding support for bot commands. - discord;discordapp - https://github.com/RogueException/Discord.Net - http://opensource.org/licenses/MIT - git - git://github.com/RogueException/Discord.Net Discord.Commands - true + A Discord.Net extension adding support for bot commands. + netstandard1.1;netstandard1.3 - - $(NoWarn);CS1573;CS1591 - true - true - \ No newline at end of file diff --git a/src/Discord.Net.Core/Discord.Net.Core.csproj b/src/Discord.Net.Core/Discord.Net.Core.csproj index 9f75d7327..262ea9007 100644 --- a/src/Discord.Net.Core/Discord.Net.Core.csproj +++ b/src/Discord.Net.Core/Discord.Net.Core.csproj @@ -1,19 +1,10 @@ - + + - 1.0.0 - rc-dev - rc-$(BuildNumber) - netstandard1.1;netstandard1.3 Discord.Net.Core - RogueException - A .Net API wrapper and bot framework for Discord. - discord;discordapp - https://github.com/RogueException/Discord.Net - http://opensource.org/licenses/MIT - git - git://github.com/RogueException/Discord.Net Discord - true + A .Net API wrapper and bot framework for Discord. + netstandard1.1;netstandard1.3 @@ -28,9 +19,4 @@ - - $(NoWarn);CS1573;CS1591 - true - true - \ No newline at end of file diff --git a/src/Discord.Net.DebugTools/Discord.Net.DebugTools.csproj b/src/Discord.Net.DebugTools/Discord.Net.DebugTools.csproj index 829951d19..4d529c070 100644 --- a/src/Discord.Net.DebugTools/Discord.Net.DebugTools.csproj +++ b/src/Discord.Net.DebugTools/Discord.Net.DebugTools.csproj @@ -1,19 +1,10 @@ + - 1.0.0 - rc-dev - rc-$(BuildNumber) - netstandard1.6 Discord.Net.DebugTools - RogueException - A Discord.Net extension adding some helper classes for diagnosing issues. - discord;discordapp - https://github.com/RogueException/Discord.Net - http://opensource.org/licenses/MIT - git - git://github.com/RogueException/Discord.Net Discord - true + A Discord.Net extension adding some helper classes for diagnosing issues. + netstandard1.6 @@ -23,9 +14,4 @@ - - $(NoWarn);CS1573;CS1591 - true - true - \ No newline at end of file diff --git a/src/Discord.Net.Providers.UdpClient/Discord.Net.Providers.UdpClient.csproj b/src/Discord.Net.Providers.UdpClient/Discord.Net.Providers.UdpClient.csproj index 984cd8f9c..3a0a6612a 100644 --- a/src/Discord.Net.Providers.UdpClient/Discord.Net.Providers.UdpClient.csproj +++ b/src/Discord.Net.Providers.UdpClient/Discord.Net.Providers.UdpClient.csproj @@ -1,26 +1,12 @@ - + + - 1.0.0 - rc-dev - rc-$(BuildNumber) - net45 Discord.Net.Providers.UDPClient - RogueException - An optional UDP client provider for Discord.Net using System.Net.UdpClient - discord;discordapp - https://github.com/RogueException/Discord.Net - http://opensource.org/licenses/MIT - git - git://github.com/RogueException/Discord.Net Discord.Providers.UDPClient - true + An optional UDP client provider for Discord.Net using System.Net.UdpClient + net45 - - $(NoWarn);CS1573;CS1591 - true - true - \ No newline at end of file diff --git a/src/Discord.Net.Providers.WS4Net/Discord.Net.Providers.WS4Net.csproj b/src/Discord.Net.Providers.WS4Net/Discord.Net.Providers.WS4Net.csproj index 62adfc0b1..5e52a1e5e 100644 --- a/src/Discord.Net.Providers.WS4Net/Discord.Net.Providers.WS4Net.csproj +++ b/src/Discord.Net.Providers.WS4Net/Discord.Net.Providers.WS4Net.csproj @@ -1,20 +1,10 @@ - + + - 1.0.0 - rc-dev - rc-$(BuildNumber) - net45 - true Discord.Net.Providers.WS4Net - RogueException - An optional WebSocket client provider for Discord.Net using WebSocket4Net - discord;discordapp - https://github.com/RogueException/Discord.Net - http://opensource.org/licenses/MIT - git - git://github.com/RogueException/Discord.Net Discord.Providers.WS4Net - true + An optional WebSocket client provider for Discord.Net using WebSocket4Net + net45 @@ -22,9 +12,4 @@ - - $(NoWarn);CS1573;CS1591 - true - true - \ No newline at end of file diff --git a/src/Discord.Net.Relay/Discord.Net.Relay.csproj b/src/Discord.Net.Relay/Discord.Net.Relay.csproj index 8fee12d14..8942a9b28 100644 --- a/src/Discord.Net.Relay/Discord.Net.Relay.csproj +++ b/src/Discord.Net.Relay/Discord.Net.Relay.csproj @@ -1,19 +1,10 @@  + - 1.0.0 - rc-dev - rc-$(BuildNumber) - netstandard1.3 Discord.Net.Relay - RogueException - A core Discord.Net library containing the Relay server. - discord;discordapp - https://github.com/RogueException/Discord.Net - http://opensource.org/licenses/MIT - git - git://github.com/RogueException/Discord.Net Discord.Relay - true + A core Discord.Net library containing the Relay server. + netstandard1.3 @@ -24,9 +15,4 @@ - - $(NoWarn);CS1573;CS1591 - true - true - \ No newline at end of file diff --git a/src/Discord.Net.Rest/Discord.Net.Rest.csproj b/src/Discord.Net.Rest/Discord.Net.Rest.csproj index b7495f273..42583abf1 100644 --- a/src/Discord.Net.Rest/Discord.Net.Rest.csproj +++ b/src/Discord.Net.Rest/Discord.Net.Rest.csproj @@ -1,19 +1,10 @@  + - 1.0.0 - rc-dev - rc-$(BuildNumber) - netstandard1.1;netstandard1.3 Discord.Net.Rest - RogueException - A core Discord.Net library containing the REST client and models. - discord;discordapp - https://github.com/RogueException/Discord.Net - http://opensource.org/licenses/MIT - git - git://github.com/RogueException/Discord.Net Discord.Rest - true + A core Discord.Net library containing the REST client and models. + netstandard1.1;netstandard1.3 @@ -21,9 +12,4 @@ - - $(NoWarn);CS1573;CS1591 - true - true - \ No newline at end of file diff --git a/src/Discord.Net.Rpc/Discord.Net.Rpc.csproj b/src/Discord.Net.Rpc/Discord.Net.Rpc.csproj index 85c2bf4e0..22f932638 100644 --- a/src/Discord.Net.Rpc/Discord.Net.Rpc.csproj +++ b/src/Discord.Net.Rpc/Discord.Net.Rpc.csproj @@ -1,19 +1,10 @@  + - 1.0.0 - rc-dev - rc-$(BuildNumber) - netstandard1.1;netstandard1.3 Discord.Net.Rpc - RogueException - A core Discord.Net library containing the RPC client and models. - discord;discordapp - https://github.com/RogueException/Discord.Net - http://opensource.org/licenses/MIT - git - git://github.com/RogueException/Discord.Net Discord.Rpc - true + A core Discord.Net library containing the RPC client and models. + netstandard1.1;netstandard1.3 @@ -33,9 +24,4 @@ - - $(NoWarn);CS1573;CS1591 - true - true - \ No newline at end of file diff --git a/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj b/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj index b5dab98e5..b41cc84b4 100644 --- a/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj +++ b/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj @@ -1,20 +1,11 @@ - + + - 1.0.0 - rc-dev - rc-$(BuildNumber) - netstandard1.1;netstandard1.3 Discord.Net.WebSocket - RogueException - A core Discord.Net library containing the WebSocket client and models. - discord;discordapp - https://github.com/RogueException/Discord.Net - http://opensource.org/licenses/MIT - git - git://github.com/RogueException/Discord.Net Discord.WebSocket + A core Discord.Net library containing the WebSocket client and models. + netstandard1.1;netstandard1.3 true - true @@ -29,9 +20,4 @@ - - $(NoWarn);CS1573;CS1591 - true - true - \ No newline at end of file diff --git a/src/Discord.Net.Webhook/Discord.Net.Webhook.csproj b/src/Discord.Net.Webhook/Discord.Net.Webhook.csproj index 747586aea..d5072c18a 100644 --- a/src/Discord.Net.Webhook/Discord.Net.Webhook.csproj +++ b/src/Discord.Net.Webhook/Discord.Net.Webhook.csproj @@ -1,27 +1,13 @@  + - 1.0.0 - rc-dev - rc-$(BuildNumber) - netstandard1.1;netstandard1.3 Discord.Net.Webhook - RogueException - A core Discord.Net library containing the Webhook client and models. - discord;discordapp - https://github.com/RogueException/Discord.Net - http://opensource.org/licenses/MIT - git - git://github.com/RogueException/Discord.Net Discord.Webhook - true + A core Discord.Net library containing the Webhook client and models. + netstandard1.1;netstandard1.3 - - $(NoWarn);CS1573;CS1591 - true - true - \ No newline at end of file diff --git a/src/Discord.Net/Discord.Net.nuspec b/src/Discord.Net/Discord.Net.nuspec index 9966d9d23..d5537ec7c 100644 --- a/src/Discord.Net/Discord.Net.nuspec +++ b/src/Discord.Net/Discord.Net.nuspec @@ -2,7 +2,7 @@ Discord.Net - 1.0.0-rc-$build$ + 1.0.0-rc$suffix$ Discord.Net RogueException RogueException @@ -13,18 +13,18 @@ false - - - - - + + + + + - - - - - + + + + + diff --git a/test/Discord.Net.Tests/Discord.Net.Tests.csproj b/test/Discord.Net.Tests/Discord.Net.Tests.csproj index 986605eeb..998795a31 100644 --- a/test/Discord.Net.Tests/Discord.Net.Tests.csproj +++ b/test/Discord.Net.Tests/Discord.Net.Tests.csproj @@ -1,9 +1,10 @@ - + + Exe + Discord netcoreapp1.1 $(PackageTargetFallback);portable-net45+win8+wp8+wpa81 - Discord @@ -19,6 +20,7 @@ + From 30bb085a78821b74abd6fc18ddd20c30163afc2f Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 1 Apr 2017 11:17:44 -0300 Subject: [PATCH 068/243] Fixed tag metapackage logic, fixed test error --- pack.ps1 | 4 ++-- test/Discord.Net.Tests/Discord.Net.Tests.csproj | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/pack.ps1 b/pack.ps1 index c14f5402d..6a8f49315 100644 --- a/pack.ps1 +++ b/pack.ps1 @@ -14,9 +14,9 @@ dotnet pack "src\Discord.Net.Providers.UdpClient\Discord.Net.Providers.UdpClient if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) } if ($Env:APPVEYOR_REPO_TAG -eq "true") { - nuget pack src\Discord.Net\Discord.Net.nuspec -OutputDirectory "artifacts" -properties suffix="-$Env:BUILD" + nuget pack src\Discord.Net\Discord.Net.nuspec -OutputDirectory "artifacts" -properties suffix="" } else { - nuget pack src\Discord.Net\Discord.Net.nuspec -OutputDirectory "artifacts" -properties suffix="" + nuget pack src\Discord.Net\Discord.Net.nuspec -OutputDirectory "artifacts" -properties suffix="-$Env:BUILD" } if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) } \ No newline at end of file diff --git a/test/Discord.Net.Tests/Discord.Net.Tests.csproj b/test/Discord.Net.Tests/Discord.Net.Tests.csproj index 998795a31..75f6ec26d 100644 --- a/test/Discord.Net.Tests/Discord.Net.Tests.csproj +++ b/test/Discord.Net.Tests/Discord.Net.Tests.csproj @@ -1,5 +1,4 @@  - Exe Discord From 73dfb8c699e2d968893e7f87852c0217a453978a Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 1 Apr 2017 11:23:38 -0300 Subject: [PATCH 069/243] Added Discord.Net.Webhook to pack list --- pack.ps1 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pack.ps1 b/pack.ps1 index 6a8f49315..5ce51834c 100644 --- a/pack.ps1 +++ b/pack.ps1 @@ -8,6 +8,8 @@ dotnet pack "src\Discord.Net.Rpc\Discord.Net.Rpc.csproj" -c "Release" -o "../../ if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) } dotnet pack "src\Discord.Net.Commands\Discord.Net.Commands.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) } +dotnet pack "src\Discord.Net.Webhook\Discord.Net.Webhook.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" +if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) } dotnet pack "src\Discord.Net.Providers.WS4Net\Discord.Net.Providers.WS4Net.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) } dotnet pack "src\Discord.Net.Providers.UdpClient\Discord.Net.Providers.UdpClient.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" From 7242a85200c1b1702a0f3feb34c806186f7da443 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 1 Apr 2017 11:23:49 -0300 Subject: [PATCH 070/243] Fixed a couple small errors --- src/Discord.Net.WebSocket/Audio/Streams/InputStream.cs | 2 ++ src/Discord.Net.WebSocket/DiscordShardedClient.cs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net.WebSocket/Audio/Streams/InputStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/InputStream.cs index f10638bba..4014c7e1e 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/InputStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/InputStream.cs @@ -25,6 +25,8 @@ namespace Discord.Audio.Streams public override Task ReadFrameAsync(CancellationToken cancelToken) { + cancelToken.ThrowIfCancellationRequested(); + if (_frames.TryDequeue(out var frame)) return Task.FromResult(frame); return Task.FromResult(null); diff --git a/src/Discord.Net.WebSocket/DiscordShardedClient.cs b/src/Discord.Net.WebSocket/DiscordShardedClient.cs index 874d34557..1dc348c20 100644 --- a/src/Discord.Net.WebSocket/DiscordShardedClient.cs +++ b/src/Discord.Net.WebSocket/DiscordShardedClient.cs @@ -233,7 +233,7 @@ namespace Discord.WebSocket int id = _shardIds[i]; var arr = guilds.Where(x => GetShardIdFor(x) == id).ToArray(); if (arr.Length > 0) - await _shards[i].DownloadUsersAsync(arr); + await _shards[i].DownloadUsersAsync(arr).ConfigureAwait(false); } } From ce2b5da6dea1f8fe5a32371ff01569d5ed607871 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 1 Apr 2017 11:38:37 -0300 Subject: [PATCH 071/243] LogManager should never leak exceptions --- src/Discord.Net.Core/Logging/LogManager.cs | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/Discord.Net.Core/Logging/LogManager.cs b/src/Discord.Net.Core/Logging/LogManager.cs index 21f956b99..6f87d1229 100644 --- a/src/Discord.Net.Core/Logging/LogManager.cs +++ b/src/Discord.Net.Core/Logging/LogManager.cs @@ -19,19 +19,31 @@ namespace Discord.Logging public async Task LogAsync(LogSeverity severity, string source, Exception ex) { - if (severity <= Level) - await _messageEvent.InvokeAsync(new LogMessage(severity, source, null, ex)).ConfigureAwait(false); + try + { + if (severity <= Level) + await _messageEvent.InvokeAsync(new LogMessage(severity, source, null, ex)).ConfigureAwait(false); + } + catch { } } public async Task LogAsync(LogSeverity severity, string source, string message, Exception ex = null) { - if (severity <= Level) + try + { + if (severity <= Level) await _messageEvent.InvokeAsync(new LogMessage(severity, source, message, ex)).ConfigureAwait(false); + } + catch { } } #if NETSTANDARD1_3 public async Task LogAsync(LogSeverity severity, string source, FormattableString message, Exception ex = null) { - if (severity <= Level) - await _messageEvent.InvokeAsync(new LogMessage(severity, source, message.ToString(), ex)).ConfigureAwait(false); + try + { + if (severity <= Level) + await _messageEvent.InvokeAsync(new LogMessage(severity, source, message.ToString(), ex)).ConfigureAwait(false); + } + catch { } } #endif From 27d6f4159dac5ecec4d525fb29a32210a759f637 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 1 Apr 2017 11:42:19 -0300 Subject: [PATCH 072/243] Lowered latency updates to debug level --- src/Discord.Net.WebSocket/Audio/AudioClient.cs | 2 +- src/Discord.Net.WebSocket/DiscordSocketClient.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net.WebSocket/Audio/AudioClient.cs b/src/Discord.Net.WebSocket/Audio/AudioClient.cs index bb5a62438..3e11fa463 100644 --- a/src/Discord.Net.WebSocket/Audio/AudioClient.cs +++ b/src/Discord.Net.WebSocket/Audio/AudioClient.cs @@ -111,7 +111,7 @@ namespace Discord.Audio e.ErrorContext.Handled = true; }; - LatencyUpdated += async (old, val) => await _audioLogger.VerboseAsync($"Latency = {val} ms").ConfigureAwait(false); + LatencyUpdated += async (old, val) => await _audioLogger.DebugAsync($"Latency = {val} ms").ConfigureAwait(false); } internal async Task StartAsync(string url, ulong userId, string sessionId, string token) diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index 8307b4a36..849a0ad65 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -115,7 +115,7 @@ namespace Discord.WebSocket JoinedGuild += async g => await _gatewayLogger.InfoAsync($"Joined {g.Name}").ConfigureAwait(false); GuildAvailable += async g => await _gatewayLogger.VerboseAsync($"Connected to {g.Name}").ConfigureAwait(false); GuildUnavailable += async g => await _gatewayLogger.VerboseAsync($"Disconnected from {g.Name}").ConfigureAwait(false); - LatencyUpdated += async (old, val) => await _gatewayLogger.VerboseAsync($"Latency = {val} ms").ConfigureAwait(false); + LatencyUpdated += async (old, val) => await _gatewayLogger.DebugAsync($"Latency = {val} ms").ConfigureAwait(false); GuildAvailable += g => { From 3e988c7549738bfb5d1eda897f3585febc0603d6 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 1 Apr 2017 12:59:06 -0300 Subject: [PATCH 073/243] Fixed incoming audio, removed nameresolution dep. --- src/Discord.Net.Core/Net/Udp/IUdpSocket.cs | 2 +- src/Discord.Net.WebSocket/API/Voice/ReadyEvent.cs | 2 ++ src/Discord.Net.WebSocket/Audio/AudioClient.cs | 8 ++++---- src/Discord.Net.WebSocket/ConnectionManager.cs | 2 -- src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj | 1 - src/Discord.Net.WebSocket/DiscordVoiceApiClient.cs | 4 ++-- src/Discord.Net.WebSocket/Net/DefaultUdpSocket.cs | 5 ++--- 7 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/Discord.Net.Core/Net/Udp/IUdpSocket.cs b/src/Discord.Net.Core/Net/Udp/IUdpSocket.cs index 8da948d1a..feb94b683 100644 --- a/src/Discord.Net.Core/Net/Udp/IUdpSocket.cs +++ b/src/Discord.Net.Core/Net/Udp/IUdpSocket.cs @@ -9,7 +9,7 @@ namespace Discord.Net.Udp event Func ReceivedDatagram; void SetCancelToken(CancellationToken cancelToken); - void SetDestination(string host, int port); + void SetDestination(string ip, int port); Task StartAsync(); Task StopAsync(); diff --git a/src/Discord.Net.WebSocket/API/Voice/ReadyEvent.cs b/src/Discord.Net.WebSocket/API/Voice/ReadyEvent.cs index e4446f814..2a134ced1 100644 --- a/src/Discord.Net.WebSocket/API/Voice/ReadyEvent.cs +++ b/src/Discord.Net.WebSocket/API/Voice/ReadyEvent.cs @@ -7,6 +7,8 @@ namespace Discord.API.Voice { [JsonProperty("ssrc")] public uint SSRC { get; set; } + [JsonProperty("ip")] + public string Ip { get; set; } [JsonProperty("port")] public ushort Port { get; set; } [JsonProperty("modes")] diff --git a/src/Discord.Net.WebSocket/Audio/AudioClient.cs b/src/Discord.Net.WebSocket/Audio/AudioClient.cs index 3e11fa463..2030ed477 100644 --- a/src/Discord.Net.WebSocket/Audio/AudioClient.cs +++ b/src/Discord.Net.WebSocket/Audio/AudioClient.cs @@ -248,7 +248,7 @@ namespace Discord.Audio _heartbeatTask = RunHeartbeatAsync(data.HeartbeatInterval, _connection.CancelToken); - ApiClient.SetUdpEndpoint(_url, data.Port); + ApiClient.SetUdpEndpoint(data.Ip, data.Port); await ApiClient.SendDiscoveryAsync(_ssrc).ConfigureAwait(false); } break; @@ -302,7 +302,7 @@ namespace Discord.Audio } private async Task ProcessPacketAsync(byte[] packet) { - if (!_connection.IsCompleted) + if (_connection.State == ConnectionState.Connecting) { if (packet.Length != 70) { @@ -314,7 +314,7 @@ namespace Discord.Audio try { ip = Encoding.UTF8.GetString(packet, 4, 70 - 6).TrimEnd('\0'); - port = packet[69] | (packet[68] << 8); + port = (packet[69] << 8) | packet[68]; } catch (Exception ex) { @@ -325,7 +325,7 @@ namespace Discord.Audio await _audioLogger.DebugAsync("Received Discovery").ConfigureAwait(false); await ApiClient.SendSelectProtocol(ip, port).ConfigureAwait(false); } - else + else if (_connection.State == ConnectionState.Connected) { uint ssrc; ulong userId; diff --git a/src/Discord.Net.WebSocket/ConnectionManager.cs b/src/Discord.Net.WebSocket/ConnectionManager.cs index 8e40beab4..decae4163 100644 --- a/src/Discord.Net.WebSocket/ConnectionManager.cs +++ b/src/Discord.Net.WebSocket/ConnectionManager.cs @@ -26,8 +26,6 @@ namespace Discord public ConnectionState State { get; private set; } public CancellationToken CancelToken { get; private set; } - public bool IsCompleted => _readyPromise.Task.IsCompleted; - internal ConnectionManager(SemaphoreSlim stateLock, Logger logger, int connectionTimeout, Func onConnecting, Func onDisconnecting, Action> clientDisconnectHandler) { diff --git a/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj b/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj index b41cc84b4..4e252795b 100644 --- a/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj +++ b/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj @@ -16,7 +16,6 @@ - diff --git a/src/Discord.Net.WebSocket/DiscordVoiceApiClient.cs b/src/Discord.Net.WebSocket/DiscordVoiceApiClient.cs index fa619b58c..fe5283ef3 100644 --- a/src/Discord.Net.WebSocket/DiscordVoiceApiClient.cs +++ b/src/Discord.Net.WebSocket/DiscordVoiceApiClient.cs @@ -228,9 +228,9 @@ namespace Discord.Audio await _sentDiscoveryEvent.InvokeAsync().ConfigureAwait(false); } - public void SetUdpEndpoint(string host, int port) + public void SetUdpEndpoint(string ip, int port) { - _udp.SetDestination(host, port); + _udp.SetDestination(ip, port); } //Helpers diff --git a/src/Discord.Net.WebSocket/Net/DefaultUdpSocket.cs b/src/Discord.Net.WebSocket/Net/DefaultUdpSocket.cs index eb184e345..3366250cc 100644 --- a/src/Discord.Net.WebSocket/Net/DefaultUdpSocket.cs +++ b/src/Discord.Net.WebSocket/Net/DefaultUdpSocket.cs @@ -89,10 +89,9 @@ namespace Discord.Net.Udp } } - public void SetDestination(string host, int port) + public void SetDestination(string ip, int port) { - var entry = Dns.GetHostEntryAsync(host).GetAwaiter().GetResult(); - _destination = new IPEndPoint(entry.AddressList[0], port); + _destination = new IPEndPoint(IPAddress.Parse(ip), port); } public void SetCancelToken(CancellationToken cancelToken) { From 35e793fd9a399b8f3a8c8896d55d1087750ed915 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 1 Apr 2017 12:59:57 -0300 Subject: [PATCH 074/243] Leave voice channel on audioclient disconnect --- src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index d240aac1e..a71e1e916 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -547,6 +547,7 @@ namespace Discord.WebSocket _audioConnectPromise = null; if (_audioClient != null) await _audioClient.StopAsync().ConfigureAwait(false); + await Discord.ApiClient.SendVoiceStateUpdateAsync(Id, null, false, false).ConfigureAwait(false); _audioClient = null; } internal async Task FinishConnectAudio(string url, string token) From 5229ddb5795bae8a03d5a717a0dc706385c37bc2 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 1 Apr 2017 13:12:41 -0300 Subject: [PATCH 075/243] Added SpeakingUpdated event --- .../Audio/AudioClient.Events.cs | 46 +++++++++++++++++++ .../Audio/AudioClient.cs | 41 +++-------------- 2 files changed, 52 insertions(+), 35 deletions(-) create mode 100644 src/Discord.Net.WebSocket/Audio/AudioClient.Events.cs diff --git a/src/Discord.Net.WebSocket/Audio/AudioClient.Events.cs b/src/Discord.Net.WebSocket/Audio/AudioClient.Events.cs new file mode 100644 index 000000000..5ab5ea1bb --- /dev/null +++ b/src/Discord.Net.WebSocket/Audio/AudioClient.Events.cs @@ -0,0 +1,46 @@ +using Discord.Audio; +using System; +using System.Threading.Tasks; + +namespace Discord.Audio +{ + internal partial class AudioClient + { + public event Func Connected + { + add { _connectedEvent.Add(value); } + remove { _connectedEvent.Remove(value); } + } + private readonly AsyncEvent> _connectedEvent = new AsyncEvent>(); + public event Func Disconnected + { + add { _disconnectedEvent.Add(value); } + remove { _disconnectedEvent.Remove(value); } + } + private readonly AsyncEvent> _disconnectedEvent = new AsyncEvent>(); + public event Func LatencyUpdated + { + add { _latencyUpdatedEvent.Add(value); } + remove { _latencyUpdatedEvent.Remove(value); } + } + private readonly AsyncEvent> _latencyUpdatedEvent = new AsyncEvent>(); + public event Func StreamCreated + { + add { _streamCreatedEvent.Add(value); } + remove { _streamCreatedEvent.Remove(value); } + } + private readonly AsyncEvent> _streamCreatedEvent = new AsyncEvent>(); + public event Func StreamDestroyed + { + add { _streamDestroyedEvent.Add(value); } + remove { _streamDestroyedEvent.Remove(value); } + } + private readonly AsyncEvent> _streamDestroyedEvent = new AsyncEvent>(); + public event Func SpeakingUpdated + { + add { _speakingUpdatedEvent.Add(value); } + remove { _speakingUpdatedEvent.Remove(value); } + } + private readonly AsyncEvent> _speakingUpdatedEvent = new AsyncEvent>(); + } +} diff --git a/src/Discord.Net.WebSocket/Audio/AudioClient.cs b/src/Discord.Net.WebSocket/Audio/AudioClient.cs index 2030ed477..0736b9626 100644 --- a/src/Discord.Net.WebSocket/Audio/AudioClient.cs +++ b/src/Discord.Net.WebSocket/Audio/AudioClient.cs @@ -15,7 +15,7 @@ using System.Threading.Tasks; namespace Discord.Audio { //TODO: Add audio reconnecting - internal class AudioClient : IAudioClient, IDisposable + internal partial class AudioClient : IAudioClient, IDisposable { internal struct StreamPair { @@ -29,37 +29,6 @@ namespace Discord.Audio } } - public event Func Connected - { - add { _connectedEvent.Add(value); } - remove { _connectedEvent.Remove(value); } - } - private readonly AsyncEvent> _connectedEvent = new AsyncEvent>(); - public event Func Disconnected - { - add { _disconnectedEvent.Add(value); } - remove { _disconnectedEvent.Remove(value); } - } - private readonly AsyncEvent> _disconnectedEvent = new AsyncEvent>(); - public event Func LatencyUpdated - { - add { _latencyUpdatedEvent.Add(value); } - remove { _latencyUpdatedEvent.Remove(value); } - } - private readonly AsyncEvent> _latencyUpdatedEvent = new AsyncEvent>(); - public event Func StreamCreated - { - add { _streamCreated.Add(value); } - remove { _streamCreated.Remove(value); } - } - private readonly AsyncEvent> _streamCreated = new AsyncEvent>(); - public event Func StreamDestroyed - { - add { _streamDestroyed.Add(value); } - remove { _streamDestroyed.Remove(value); } - } - private readonly AsyncEvent> _streamDestroyed = new AsyncEvent>(); - private readonly Logger _audioLogger; private readonly JsonSerializer _serializer; private readonly ConnectionManager _connection; @@ -202,7 +171,7 @@ namespace Discord.Audio var readerStream = new InputStream(); var writerStream = new OpusDecodeStream(new RTPReadStream(readerStream, _secretKey)); _streams.TryAdd(userId, new StreamPair(readerStream, writerStream)); - await _streamCreated.InvokeAsync(userId, readerStream); + await _streamCreatedEvent.InvokeAsync(userId, readerStream); } } internal AudioInStream GetInputStream(ulong id) @@ -215,14 +184,14 @@ namespace Discord.Audio internal async Task RemoveInputStreamAsync(ulong userId) { if (_streams.TryRemove(userId, out var ignored)) - await _streamDestroyed.InvokeAsync(userId).ConfigureAwait(false); + await _streamDestroyedEvent.InvokeAsync(userId).ConfigureAwait(false); } internal async Task ClearInputStreamsAsync() { foreach (var pair in _streams) { pair.Value.Reader.Dispose(); - await _streamDestroyed.InvokeAsync(pair.Key).ConfigureAwait(false); + await _streamDestroyedEvent.InvokeAsync(pair.Key).ConfigureAwait(false); } _ssrcMap.Clear(); _streams.Clear(); @@ -287,6 +256,8 @@ namespace Discord.Audio var data = (payload as JToken).ToObject(_serializer); _ssrcMap[data.Ssrc] = data.UserId; //TODO: Memory Leak: SSRCs are never cleaned up + + await _speakingUpdatedEvent.InvokeAsync(data.UserId, data.Speaking); } break; default: From 909127d330f9142448da502b124c417dcafed2aa Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 1 Apr 2017 13:13:20 -0300 Subject: [PATCH 076/243] InputStream reads should wait until data is available. --- src/Discord.Net.Core/Audio/AudioInStream.cs | 4 +-- src/Discord.Net.Core/Audio/IAudioClient.cs | 1 + .../Audio/Streams/InputStream.cs | 33 ++++++++++--------- 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/src/Discord.Net.Core/Audio/AudioInStream.cs b/src/Discord.Net.Core/Audio/AudioInStream.cs index a6b5c5e6b..6503474e5 100644 --- a/src/Discord.Net.Core/Audio/AudioInStream.cs +++ b/src/Discord.Net.Core/Audio/AudioInStream.cs @@ -11,9 +11,9 @@ namespace Discord.Audio public override bool CanSeek => false; public override bool CanWrite => true; - public abstract Task ReadFrameAsync(CancellationToken cancelToken); + public abstract Task ReadFrameAsync(CancellationToken cancelToken); - public RTPFrame? ReadFrame() + public RTPFrame ReadFrame() { return ReadFrameAsync(CancellationToken.None).GetAwaiter().GetResult(); } diff --git a/src/Discord.Net.Core/Audio/IAudioClient.cs b/src/Discord.Net.Core/Audio/IAudioClient.cs index 149905654..3ee008320 100644 --- a/src/Discord.Net.Core/Audio/IAudioClient.cs +++ b/src/Discord.Net.Core/Audio/IAudioClient.cs @@ -10,6 +10,7 @@ namespace Discord.Audio event Func LatencyUpdated; event Func StreamCreated; event Func StreamDestroyed; + event Func SpeakingUpdated; /// Gets the current connection state of this client. ConnectionState ConnectionState { get; } diff --git a/src/Discord.Net.WebSocket/Audio/Streams/InputStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/InputStream.cs index 4014c7e1e..14bb18851 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/InputStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/InputStream.cs @@ -8,7 +8,10 @@ namespace Discord.Audio.Streams /// Reads the payload from an RTP frame public class InputStream : AudioInStream { + private const int MaxFrames = 100; + private ConcurrentQueue _frames; + private SemaphoreSlim _signal; private ushort _nextSeq; private uint _nextTimestamp; private bool _hasHeader; @@ -21,28 +24,27 @@ namespace Discord.Audio.Streams public InputStream() { _frames = new ConcurrentQueue(); + _signal = new SemaphoreSlim(0, MaxFrames); } - public override Task ReadFrameAsync(CancellationToken cancelToken) + public override async Task ReadFrameAsync(CancellationToken cancelToken) { cancelToken.ThrowIfCancellationRequested(); - if (_frames.TryDequeue(out var frame)) - return Task.FromResult(frame); - return Task.FromResult(null); + RTPFrame frame; + await _signal.WaitAsync(cancelToken).ConfigureAwait(false); + _frames.TryDequeue(out frame); + return frame; } - public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancelToken) + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancelToken) { cancelToken.ThrowIfCancellationRequested(); - - if (_frames.TryDequeue(out var frame)) - { - if (count < frame.Payload.Length) - throw new InvalidOperationException("Buffer is too small."); - Buffer.BlockCopy(frame.Payload, 0, buffer, offset, frame.Payload.Length); - return Task.FromResult(frame.Payload.Length); - } - return Task.FromResult(0); + + var frame = await ReadFrameAsync(cancelToken).ConfigureAwait(false); + if (count < frame.Payload.Length) + throw new InvalidOperationException("Buffer is too small."); + Buffer.BlockCopy(frame.Payload, 0, buffer, offset, frame.Payload.Length); + return frame.Payload.Length; } public void WriteHeader(ushort seq, uint timestamp) @@ -57,7 +59,7 @@ namespace Discord.Audio.Streams { cancelToken.ThrowIfCancellationRequested(); - if (_frames.Count > 100) //1-2 seconds + if (_frames.Count > MaxFrames) //1-2 seconds { _hasHeader = false; return Task.Delay(0); //Buffer overloaded @@ -72,6 +74,7 @@ namespace Discord.Audio.Streams timestamp: _nextTimestamp, payload: payload )); + _signal.Release(); _hasHeader = false; return Task.Delay(0); } From d243587a97179104095408d463547e2b0f86fe1c Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 1 Apr 2017 13:16:18 -0300 Subject: [PATCH 077/243] Send no more than 10 frames of silence. --- .../Audio/Streams/BufferedWriteStream.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs index 3040da855..5c402785e 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs @@ -9,6 +9,8 @@ namespace Discord.Audio.Streams /// Wraps another stream with a timed buffer. public class BufferedWriteStream : AudioOutStream { + private const int MaxSilenceFrames = 10; + private struct Frame { public Frame(byte[] buffer, int bytes) @@ -33,6 +35,7 @@ namespace Discord.Audio.Streams private readonly Logger _logger; private readonly int _ticksPerFrame, _queueLength; private bool _isPreloaded; + private int _silenceFrames; public BufferedWriteStream(AudioOutStream next, int samplesPerFrame, int bufferMillis, CancellationToken cancelToken, int maxFrameSize = 1500) : this(next, samplesPerFrame, bufferMillis, cancelToken, null, maxFrameSize) { } @@ -51,6 +54,7 @@ namespace Discord.Audio.Streams for (int i = 0; i < _queueLength; i++) _bufferPool.Enqueue(new byte[maxFrameSize]); _queueLock = new SemaphoreSlim(_queueLength, _queueLength); + _silenceFrames = MaxSilenceFrames; _task = Run(); } @@ -78,6 +82,7 @@ namespace Discord.Audio.Streams _bufferPool.Enqueue(frame.Buffer); _queueLock.Release(); nextTick += _ticksPerFrame; + _silenceFrames = 0; #if DEBUG var _ = _logger.DebugAsync($"Sent {frame.Bytes} bytes ({_queuedFrames.Count} frames buffered)"); #endif @@ -86,7 +91,8 @@ namespace Discord.Audio.Streams { while ((nextTick - tick) <= 0) { - await _next.WriteAsync(_silenceFrame, 0, _silenceFrame.Length).ConfigureAwait(false); + if (_silenceFrames++ < MaxSilenceFrames) + await _next.WriteAsync(_silenceFrame, 0, _silenceFrame.Length).ConfigureAwait(false); nextTick += _ticksPerFrame; } #if DEBUG From d991834c50443a6aaccb46e79c2f10489bafb4dd Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 1 Apr 2017 13:21:08 -0300 Subject: [PATCH 078/243] InputStreams should be disposed when destroyed --- src/Discord.Net.WebSocket/Audio/AudioClient.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net.WebSocket/Audio/AudioClient.cs b/src/Discord.Net.WebSocket/Audio/AudioClient.cs index 0736b9626..d9419dac1 100644 --- a/src/Discord.Net.WebSocket/Audio/AudioClient.cs +++ b/src/Discord.Net.WebSocket/Audio/AudioClient.cs @@ -183,15 +183,18 @@ namespace Discord.Audio } internal async Task RemoveInputStreamAsync(ulong userId) { - if (_streams.TryRemove(userId, out var ignored)) + if (_streams.TryRemove(userId, out var pair)) + { await _streamDestroyedEvent.InvokeAsync(userId).ConfigureAwait(false); + pair.Reader.Dispose(); + } } internal async Task ClearInputStreamsAsync() { foreach (var pair in _streams) { - pair.Value.Reader.Dispose(); await _streamDestroyedEvent.InvokeAsync(pair.Key).ConfigureAwait(false); + pair.Value.Reader.Dispose(); } _ssrcMap.Clear(); _streams.Clear(); From fd043b3d53af93e20de3ac0ceeb753491dd73bdc Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 1 Apr 2017 13:32:19 -0300 Subject: [PATCH 079/243] Clear input streams on audiostream disconnect --- src/Discord.Net.WebSocket/Audio/AudioClient.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net.WebSocket/Audio/AudioClient.cs b/src/Discord.Net.WebSocket/Audio/AudioClient.cs index d9419dac1..fe8d763b3 100644 --- a/src/Discord.Net.WebSocket/Audio/AudioClient.cs +++ b/src/Discord.Net.WebSocket/Audio/AudioClient.cs @@ -91,8 +91,10 @@ namespace Discord.Audio _token = token; await _connection.StartAsync().ConfigureAwait(false); } - public async Task StopAsync() - => await _connection.StopAsync().ConfigureAwait(false); + public async Task StopAsync() + { + await _connection.StopAsync().ConfigureAwait(false); + } private async Task OnConnectingAsync() { @@ -120,6 +122,8 @@ namespace Discord.Audio while (_heartbeatTimes.TryDequeue(out time)) { } _lastMessageTime = 0; + await ClearInputStreamsAsync().ConfigureAwait(false); + await _audioLogger.DebugAsync("Sending Voice State").ConfigureAwait(false); await Discord.ApiClient.SendVoiceStateUpdateAsync(Guild.Id, null, false, false).ConfigureAwait(false); } From 6798ba0d4bc3101111ba5789cb6b65139eb3dfd7 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 1 Apr 2017 13:44:35 -0300 Subject: [PATCH 080/243] Prevent duplicate incoming stream events on connect --- src/Discord.Net.WebSocket/Audio/AudioClient.cs | 6 ++++-- .../Entities/Guilds/SocketGuild.cs | 10 ++++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/Discord.Net.WebSocket/Audio/AudioClient.cs b/src/Discord.Net.WebSocket/Audio/AudioClient.cs index fe8d763b3..d5e9895a0 100644 --- a/src/Discord.Net.WebSocket/Audio/AudioClient.cs +++ b/src/Discord.Net.WebSocket/Audio/AudioClient.cs @@ -47,15 +47,17 @@ namespace Discord.Audio public SocketGuild Guild { get; } public DiscordVoiceAPIClient ApiClient { get; private set; } public int Latency { get; private set; } + public ulong ChannelId { get; internal set; } private DiscordSocketClient Discord => Guild.Discord; public ConnectionState ConnectionState => _connection.State; /// Creates a new REST/WebSocket discord client. - internal AudioClient(SocketGuild guild, int id) + internal AudioClient(SocketGuild guild, int clientId, ulong channelId) { Guild = guild; - _audioLogger = Discord.LogManager.CreateLogger($"Audio #{id}"); + ChannelId = channelId; + _audioLogger = Discord.LogManager.CreateLogger($"Audio #{clientId}"); ApiClient = new DiscordVoiceAPIClient(guild.Id, Discord.WebSocketProvider, Discord.UdpSocketProvider); ApiClient.SentGatewayMessage += async opCode => await _audioLogger.DebugAsync($"Sent {opCode}").ConfigureAwait(false); diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index a71e1e916..e261d118f 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -433,7 +433,13 @@ namespace Discord.WebSocket if (_audioClient != null && before.VoiceChannel?.Id != after.VoiceChannel?.Id) { if (model.UserId == CurrentUser.Id) - await RepopulateAudioStreamsAsync().ConfigureAwait(false); + { + if (after.VoiceChannel != null && _audioClient.ChannelId != after.VoiceChannel?.Id) + { + _audioClient.ChannelId = after.VoiceChannel.Id; + await RepopulateAudioStreamsAsync().ConfigureAwait(false); + } + } else { await _audioClient.RemoveInputStreamAsync(model.UserId).ConfigureAwait(false); //User changed channels, end their stream @@ -480,7 +486,7 @@ namespace Discord.WebSocket if (_audioClient == null) { - var audioClient = new AudioClient(this, Discord.GetAudioId()); + var audioClient = new AudioClient(this, Discord.GetAudioId(), channelId); audioClient.Disconnected += async ex => { if (!promise.Task.IsCompleted) From 004bb4cae02c78716060c285f90ae4e9d6988332 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 1 Apr 2017 13:54:39 -0300 Subject: [PATCH 081/243] Don't nullref in ShardedClient's OnLogout if already logged out. --- 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 1dc348c20..ab2cb9266 100644 --- a/src/Discord.Net.WebSocket/DiscordShardedClient.cs +++ b/src/Discord.Net.WebSocket/DiscordShardedClient.cs @@ -98,8 +98,11 @@ namespace Discord.WebSocket internal override async Task OnLogoutAsync() { //Assume threadsafe: already in a connection lock - for (int i = 0; i < _shards.Length; i++) - await _shards[i].LogoutAsync(); + if (_shards != null) + { + for (int i = 0; i < _shards.Length; i++) + await _shards[i].LogoutAsync(); + } CurrentUser = null; if (_automaticShards) From fd72583a75a1c9ccc1f6040c8611d20c58513e4a Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 1 Apr 2017 15:05:51 -0300 Subject: [PATCH 082/243] Move guild presence updates to GuildMemberUpdated. Filter duplicate UserUpdated events. --- .../API/Common/GuildMember.cs | 6 ++-- .../Entities/Users/RestGuildUser.cs | 9 ++++-- .../DiscordSocketClient.cs | 30 ++++++++++++++----- .../Entities/Guilds/SocketGuild.cs | 6 ++-- .../Entities/Users/SocketGuildUser.cs | 16 ++++++---- .../Entities/Users/SocketSelfUser.cs | 15 ++++++++-- .../Entities/Users/SocketUnknownUser.cs | 13 -------- .../Entities/Users/SocketUser.cs | 30 +++++++++++++++---- .../Entities/Users/SocketWebhookUser.cs | 13 -------- 9 files changed, 81 insertions(+), 57 deletions(-) diff --git a/src/Discord.Net.Rest/API/Common/GuildMember.cs b/src/Discord.Net.Rest/API/Common/GuildMember.cs index daba36d23..24ad17c14 100644 --- a/src/Discord.Net.Rest/API/Common/GuildMember.cs +++ b/src/Discord.Net.Rest/API/Common/GuildMember.cs @@ -11,12 +11,12 @@ namespace Discord.API [JsonProperty("nick")] public Optional Nick { get; set; } [JsonProperty("roles")] - public ulong[] Roles { get; set; } + public Optional Roles { get; set; } [JsonProperty("joined_at")] public Optional JoinedAt { get; set; } [JsonProperty("deaf")] - public bool Deaf { get; set; } + public Optional Deaf { get; set; } [JsonProperty("mute")] - public bool Mute { get; set; } + public Optional Mute { get; set; } } } diff --git a/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs b/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs index 538f6b80f..f6db057f2 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs @@ -50,9 +50,12 @@ namespace Discord.Rest _joinedAtTicks = model.JoinedAt.Value.UtcTicks; if (model.Nick.IsSpecified) Nickname = model.Nick.Value; - IsDeafened = model.Deaf; - IsMuted = model.Mute; - UpdateRoles(model.Roles); + if (model.Deaf.IsSpecified) + IsDeafened = model.Deaf.Value; + if (model.Mute.IsSpecified) + IsMuted = model.Mute.Value; + if (model.Roles.IsSpecified) + UpdateRoles(model.Roles.Value); } private void UpdateRoles(ulong[] roleIds) { diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index 849a0ad65..64aa2d272 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -1336,14 +1336,30 @@ namespace Discord.WebSocket var user = guild.GetUser(data.User.Id); if (user == null) - guild.AddOrUpdateUser(data); + user = guild.AddOrUpdateUser(data); + else + { + var globalBefore = user.GlobalUser.Clone(); + if (user.GlobalUser.Update(State, data.User)) + { + //Global data was updated, trigger UserUpdated + await TimedInvokeAsync(_userUpdatedEvent, nameof(UserUpdated), globalBefore, user).ConfigureAwait(false); + } + } + + var before = user.Clone(); + user.Update(State, data, true); + await TimedInvokeAsync(_guildMemberUpdatedEvent, nameof(GuildMemberUpdated), before, user).ConfigureAwait(false); + } + else + { + //TODO: Add as part of friends list update + /*var globalUser = GetOrCreateUser(State, data.User); + var before = globalUser.Clone(); + globalUser.Update(State, data.User); + globalUser.Update(State, data); + await TimedInvokeAsync(_userUpdatedEvent, nameof(UserUpdated), before, globalUser).ConfigureAwait(false);*/ } - - var globalUser = GetOrCreateUser(State, data.User); //TODO: Memory leak, users removed from friends list will never RemoveRef. - var before = globalUser.Clone(); - globalUser.Update(State, data); - - await TimedInvokeAsync(_userUpdatedEvent, nameof(UserUpdated), before, globalUser).ConfigureAwait(false); } break; case "TYPING_START": diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index e261d118f..04a94b421 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -163,7 +163,7 @@ namespace Discord.WebSocket { SocketGuildUser member; if (members.TryGetValue(model.Presences[i].User.Id, out member)) - member.Update(state, model.Presences[i]); + member.Update(state, model.Presences[i], true); else Debug.Assert(false); } @@ -249,7 +249,7 @@ namespace Discord.WebSocket { SocketGuildUser member; if (members.TryGetValue(model.Presences[i].User.Id, out member)) - member.Update(state, model.Presences[i]); + member.Update(state, model.Presences[i], true); else Debug.Assert(false); } @@ -392,7 +392,7 @@ namespace Discord.WebSocket { SocketGuildUser member; if (_members.TryGetValue(model.User.Id, out member)) - member.Update(Discord.State, model); + member.Update(Discord.State, model, false); else { member = SocketGuildUser.Create(this, Discord.State, model); diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs index d20fd0d33..58d2a0b8d 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs @@ -26,7 +26,7 @@ namespace Discord.WebSocket public override ushort DiscriminatorValue { get { return GlobalUser.DiscriminatorValue; } internal set { GlobalUser.DiscriminatorValue = value; } } public override string AvatarId { get { return GlobalUser.AvatarId; } internal set { GlobalUser.AvatarId = value; } } public GuildPermissions GuildPermissions => new GuildPermissions(Permissions.ResolveGuild(Guild, this)); - internal override SocketPresence Presence { get { return GlobalUser.Presence; } set { GlobalUser.Presence = value; } } + internal override SocketPresence Presence { get; set; } public override bool IsWebhook => false; public bool IsSelfDeafened => VoiceState?.IsSelfDeafened ?? false; @@ -78,7 +78,7 @@ namespace Discord.WebSocket internal static SocketGuildUser Create(SocketGuild guild, ClientState state, PresenceModel model) { var entity = new SocketGuildUser(guild, guild.Discord.GetOrCreateUser(state, model.User)); - entity.Update(state, model); + entity.Update(state, model, false); return entity; } internal void Update(ClientState state, Model model) @@ -88,15 +88,19 @@ namespace Discord.WebSocket _joinedAtTicks = model.JoinedAt.Value.UtcTicks; if (model.Nick.IsSpecified) Nickname = model.Nick.Value; - UpdateRoles(model.Roles); + if (model.Roles.IsSpecified) + UpdateRoles(model.Roles.Value); } internal override void Update(ClientState state, PresenceModel model) + => Update(state, model, true); + internal void Update(ClientState state, PresenceModel model, bool updatePresence) { - base.Update(state, model); - if (model.Roles.IsSpecified) - UpdateRoles(model.Roles.Value); + if (updatePresence) + base.Update(state, model); if (model.Nick.IsSpecified) Nickname = model.Nick.Value; + if (model.Roles.IsSpecified) + UpdateRoles(model.Roles.Value); } private void UpdateRoles(ulong[] roleIds) { diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs index c0e483a56..b7c02c2db 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs @@ -33,16 +33,25 @@ namespace Discord.WebSocket entity.Update(state, model); return entity; } - internal override void Update(ClientState state, Model model) + internal override bool Update(ClientState state, Model model) { - base.Update(state, model); - + bool hasGlobalChanges = base.Update(state, model); if (model.Email.IsSpecified) + { Email = model.Email.Value; + hasGlobalChanges = true; + } if (model.Verified.IsSpecified) + { IsVerified = model.Verified.Value; + hasGlobalChanges = true; + } if (model.MfaEnabled.IsSpecified) + { IsMfaEnabled = model.MfaEnabled.Value; + hasGlobalChanges = true; + } + return hasGlobalChanges; } public Task ModifyAsync(Action func, RequestOptions options = null) diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs index 57ff81433..c7f6cb846 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs @@ -1,7 +1,6 @@ using System; using System.Diagnostics; using Model = Discord.API.User; -using PresenceModel = Discord.API.Presence; namespace Discord.WebSocket { @@ -29,18 +28,6 @@ namespace Discord.WebSocket return entity; } - internal override void Update(ClientState state, PresenceModel model) - { - if (model.User.Avatar.IsSpecified) - AvatarId = model.User.Avatar.Value; - if (model.User.Discriminator.IsSpecified) - DiscriminatorValue = ushort.Parse(model.User.Discriminator.Value); - if (model.User.Bot.IsSpecified) - IsBot = model.User.Bot.Value; - if (model.User.Username.IsSpecified) - Username = model.User.Username.Value; - } - internal new SocketUnknownUser Clone() => MemberwiseClone() as SocketUnknownUser; } } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs index 6e97d4b31..12f1b2b30 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs @@ -26,21 +26,39 @@ namespace Discord.WebSocket : base(discord, id) { } - internal virtual void Update(ClientState state, Model model) + internal virtual bool Update(ClientState state, Model model) { - if (model.Avatar.IsSpecified) + bool hasChanges = false; + if (model.Avatar.IsSpecified && model.Avatar.Value != AvatarId) + { AvatarId = model.Avatar.Value; + hasChanges = true; + } if (model.Discriminator.IsSpecified) - DiscriminatorValue = ushort.Parse(model.Discriminator.Value); - if (model.Bot.IsSpecified) + { + var newVal = ushort.Parse(model.Discriminator.Value); + if (newVal != DiscriminatorValue) + { + DiscriminatorValue = ushort.Parse(model.Discriminator.Value); + hasChanges = true; + } + } + if (model.Bot.IsSpecified && model.Bot.Value != IsBot) + { IsBot = model.Bot.Value; - if (model.Username.IsSpecified) + hasChanges = true; + } + if (model.Username.IsSpecified && model.Username.Value != Username) + { Username = model.Username.Value; + hasChanges = true; + } + return hasChanges; } internal virtual void Update(ClientState state, PresenceModel model) { Presence = SocketPresence.Create(model); - Update(state, model.User); + //Update(state, model.User); } public Task CreateDMChannelAsync(RequestOptions options = null) diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs index 1193eca8f..c34f866cb 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs @@ -4,7 +4,6 @@ using System.Collections.Immutable; using System.Diagnostics; using System.Threading.Tasks; using Model = Discord.API.User; -using PresenceModel = Discord.API.Presence; namespace Discord.WebSocket { @@ -36,18 +35,6 @@ namespace Discord.WebSocket return entity; } - internal override void Update(ClientState state, PresenceModel model) - { - if (model.User.Avatar.IsSpecified) - AvatarId = model.User.Avatar.Value; - if (model.User.Discriminator.IsSpecified) - DiscriminatorValue = ushort.Parse(model.User.Discriminator.Value); - if (model.User.Bot.IsSpecified) - IsBot = model.User.Bot.Value; - if (model.User.Username.IsSpecified) - Username = model.User.Username.Value; - } - internal new SocketWebhookUser Clone() => MemberwiseClone() as SocketWebhookUser; From 3424473566e5f90ce05d353b450cc7ead4e46f3c Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 1 Apr 2017 15:27:18 -0300 Subject: [PATCH 083/243] Fixed a couple CI issues --- Discord.Net.targets | 10 ++++++++-- test.ps1 | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/Discord.Net.targets b/Discord.Net.targets index 867af36ec..15591afc2 100644 --- a/Discord.Net.targets +++ b/Discord.Net.targets @@ -2,8 +2,6 @@ 1.0.0 rc - $(VersionSuffix)-dev - $(VersionSuffix)-$(BuildNumber) RogueException discord;discordapp https://github.com/RogueException/Discord.Net @@ -12,6 +10,14 @@ git://github.com/RogueException/Discord.Net true + + dev + $(VersionSuffix)-dev + + + $(BuildNumber) + $(VersionSuffix)-$(BuildNumber) + $(NoWarn);CS1573;CS1591 true diff --git a/test.ps1 b/test.ps1 index b8a817743..d73a01f23 100644 --- a/test.ps1 +++ b/test.ps1 @@ -1,2 +1,2 @@ -dotnet test test/Discord.Net.Tests/Discord.Net.Tests.csproj -c "Release" --no-build /p:BuildNumber="$Env:BUILD" +dotnet test test/Discord.Net.Tests/Discord.Net.Tests.csproj -c "Release" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) } \ No newline at end of file From bc2e0a19afbb3afcec7e03b671b47700247b2336 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 1 Apr 2017 16:40:53 -0300 Subject: [PATCH 084/243] Fixed non-guild presence updates --- src/Discord.Net.WebSocket/DiscordSocketClient.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index 64aa2d272..7fd66fd40 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -1353,12 +1353,17 @@ namespace Discord.WebSocket } else { - //TODO: Add as part of friends list update - /*var globalUser = GetOrCreateUser(State, data.User); + var globalUser = State.GetUser(data.User.Id); + if (globalUser == null) + { + await _gatewayLogger.WarningAsync("PRESENCE_UPDATE referenced an unknown user.").ConfigureAwait(false); + break; + } + var before = globalUser.Clone(); globalUser.Update(State, data.User); globalUser.Update(State, data); - await TimedInvokeAsync(_userUpdatedEvent, nameof(UserUpdated), before, globalUser).ConfigureAwait(false);*/ + await TimedInvokeAsync(_userUpdatedEvent, nameof(UserUpdated), before, globalUser).ConfigureAwait(false); } } break; From a7f50e7f599fdb2ea77ba6e58434347e50e0a92a Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 1 Apr 2017 18:19:50 -0300 Subject: [PATCH 085/243] Added EmbedBuilder field helpers --- .../Entities/Messages/EmbedBuilder.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/Discord.Net.Rest/Entities/Messages/EmbedBuilder.cs b/src/Discord.Net.Rest/Entities/Messages/EmbedBuilder.cs index 8890df683..74f1441e1 100644 --- a/src/Discord.Net.Rest/Entities/Messages/EmbedBuilder.cs +++ b/src/Discord.Net.Rest/Entities/Messages/EmbedBuilder.cs @@ -92,6 +92,24 @@ namespace Discord return this; } + public EmbedBuilder AddField(string name, object value) + { + var field = new EmbedFieldBuilder() + .WithIsInline(false) + .WithName(name) + .WithValue(value); + _fields.Add(field); + return this; + } + public EmbedBuilder AddInlineField(string name, object value) + { + var field = new EmbedFieldBuilder() + .WithIsInline(true) + .WithName(name) + .WithValue(value); + _fields.Add(field); + return this; + } public EmbedBuilder AddField(EmbedFieldBuilder field) { _fields.Add(field); From aae2667fed6beed9d60fd63066dadc771b7cd268 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 1 Apr 2017 21:47:04 -0300 Subject: [PATCH 086/243] Keep tags sorted when adding everyone/here mentions --- .../Entities/Messages/MessageHelper.cs | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs b/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs index d872901fa..367a33be6 100644 --- a/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs +++ b/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs @@ -126,8 +126,9 @@ namespace Discord.Rest index = text.IndexOf("@everyone", index); if (index == -1) break; - if (!TagOverlaps(tags, index)) - tags.Add(new Tag(TagType.EveryoneMention, index, "@everyone".Length, 0, null)); + var tagIndex = FindIndex(tags, index); + if (tagIndex.HasValue) + tags.Insert(tagIndex.Value, new Tag(TagType.EveryoneMention, index, "@everyone".Length, 0, null)); index++; } @@ -137,22 +138,26 @@ namespace Discord.Rest index = text.IndexOf("@here", index); if (index == -1) break; - if (!TagOverlaps(tags, index)) - tags.Add(new Tag(TagType.HereMention, index, "@here".Length, 0, null)); + var tagIndex = FindIndex(tags, index); + if (tagIndex.HasValue) + tags.Insert(tagIndex.Value, new Tag(TagType.HereMention, index, "@here".Length, 0, null)); index++; } return tags.ToImmutable(); } - private static bool TagOverlaps(IReadOnlyList tags, int index) + private static int? FindIndex(IReadOnlyList tags, int index) { - for (int i = 0; i < tags.Count; i++) + int i = 0; + for (; i < tags.Count; i++) { var tag = tags[i]; - if (index >= tag.Index && index < tag.Index + tag.Length) - return true; + if (index < tag.Index) + break; //Position before this tag } - return false; + if (i > 0 && index < tags[i - 1].Index + tags[i - 1].Length) + return null; //Overlaps tag before this + return i; } public static ImmutableArray FilterTagsByKey(TagType type, ImmutableArray tags) { From e0e28c6dd10864385a43dfdaa47e320d1db1468f Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 1 Apr 2017 21:49:34 -0300 Subject: [PATCH 087/243] Changed EmbedChannel's type to GuildChannel --- src/Discord.Net.Core/Entities/Guilds/IGuild.cs | 2 +- src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs | 9 +++------ src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs | 8 ++++---- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs index b3367fab5..6c7b73370 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs @@ -83,7 +83,7 @@ namespace Discord Task GetVoiceChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); Task GetAFKChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); Task GetDefaultChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); - Task GetEmbedChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + Task GetEmbedChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); /// Creates a new text channel. Task CreateTextChannelAsync(string name, RequestOptions options = null); /// Creates a new voice channel. diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs index e4e970487..15acef457 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -188,14 +188,11 @@ namespace Discord.Rest var channel = await GuildHelper.GetChannelAsync(this, Discord, DefaultChannelId, options).ConfigureAwait(false); return channel as RestTextChannel; } - public async Task GetEmbedChannelAsync(RequestOptions options = null) + public async Task GetEmbedChannelAsync(RequestOptions options = null) { var embedId = EmbedChannelId; if (embedId.HasValue) - { - var channel = await GuildHelper.GetChannelAsync(this, Discord, embedId.Value, options).ConfigureAwait(false); - return channel as RestVoiceChannel; - } + return await GuildHelper.GetChannelAsync(this, Discord, embedId.Value, options).ConfigureAwait(false); return null; } public Task CreateTextChannelAsync(string name, RequestOptions options = null) @@ -311,7 +308,7 @@ namespace Discord.Rest else return null; } - async Task IGuild.GetEmbedChannelAsync(CacheMode mode, RequestOptions options) + async Task IGuild.GetEmbedChannelAsync(CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) return await GetEmbedChannelAsync(options).ConfigureAwait(false); diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index 04a94b421..9f106ff1c 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -69,12 +69,12 @@ namespace Discord.WebSocket return id.HasValue ? GetVoiceChannel(id.Value) : null; } } - public SocketVoiceChannel EmbedChannel + public SocketGuildChannel EmbedChannel { get { var id = EmbedChannelId; - return id.HasValue ? GetVoiceChannel(id.Value) : null; + return id.HasValue ? GetChannel(id.Value) : null; } } public IReadOnlyCollection TextChannels @@ -627,8 +627,8 @@ namespace Discord.WebSocket => Task.FromResult(AFKChannel); Task IGuild.GetDefaultChannelAsync(CacheMode mode, RequestOptions options) => Task.FromResult(DefaultChannel); - Task IGuild.GetEmbedChannelAsync(CacheMode mode, RequestOptions options) - => Task.FromResult(EmbedChannel); + Task IGuild.GetEmbedChannelAsync(CacheMode mode, RequestOptions options) + => Task.FromResult(EmbedChannel); async Task IGuild.CreateTextChannelAsync(string name, RequestOptions options) => await CreateTextChannelAsync(name, options).ConfigureAwait(false); async Task IGuild.CreateVoiceChannelAsync(string name, RequestOptions options) From f0202e4d4eb6441982e96386f910925195d7061c Mon Sep 17 00:00:00 2001 From: RogueException Date: Sun, 2 Apr 2017 14:38:05 -0300 Subject: [PATCH 088/243] Improved warnings for unknown entities --- .../Audio/AudioClient.Events.cs | 3 +- .../DiscordSocketClient.cs | 298 ++++++++++-------- .../Entities/Channels/ISocketAudioChannel.cs | 5 +- 3 files changed, 176 insertions(+), 130 deletions(-) diff --git a/src/Discord.Net.WebSocket/Audio/AudioClient.Events.cs b/src/Discord.Net.WebSocket/Audio/AudioClient.Events.cs index 5ab5ea1bb..20ebe7e6d 100644 --- a/src/Discord.Net.WebSocket/Audio/AudioClient.Events.cs +++ b/src/Discord.Net.WebSocket/Audio/AudioClient.Events.cs @@ -1,5 +1,4 @@ -using Discord.Audio; -using System; +using System; using System.Threading.Tasks; namespace Discord.Audio diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index 7fd66fd40..c7fcef572 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -550,7 +550,7 @@ namespace Discord.WebSocket } else { - await _gatewayLogger.WarningAsync($"GUILD_AVAILABLE referenced an unknown guild.").ConfigureAwait(false); + await UnknownGuildAsync(type, data.Id).ConfigureAwait(false); return; } } @@ -567,7 +567,7 @@ namespace Discord.WebSocket } else { - await _gatewayLogger.WarningAsync($"GUILD_CREATE referenced an unknown guild.").ConfigureAwait(false); + await UnknownGuildAsync(type, data.Id).ConfigureAwait(false); return; } } @@ -587,7 +587,7 @@ namespace Discord.WebSocket } else { - await _gatewayLogger.WarningAsync("GUILD_UPDATE referenced an unknown guild.").ConfigureAwait(false); + await UnknownGuildAsync(type, data.Id).ConfigureAwait(false); return; } } @@ -606,7 +606,7 @@ namespace Discord.WebSocket } else { - await _gatewayLogger.WarningAsync("GUILD_EMOJIS_UPDATE referenced an unknown guild.").ConfigureAwait(false); + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); return; } } @@ -628,7 +628,7 @@ namespace Discord.WebSocket } else { - await _gatewayLogger.WarningAsync("GUILD_SYNC referenced an unknown guild.").ConfigureAwait(false); + await UnknownGuildAsync(type, data.Id).ConfigureAwait(false); return; } } @@ -649,7 +649,7 @@ namespace Discord.WebSocket } else { - await _gatewayLogger.WarningAsync($"GUILD_UNAVAILABLE referenced an unknown guild.").ConfigureAwait(false); + await UnknownGuildAsync(type, data.Id).ConfigureAwait(false); return; } } @@ -665,7 +665,7 @@ namespace Discord.WebSocket } else { - await _gatewayLogger.WarningAsync($"GUILD_DELETE referenced an unknown guild.").ConfigureAwait(false); + await UnknownGuildAsync(type, data.Id).ConfigureAwait(false); return; } } @@ -688,13 +688,13 @@ namespace Discord.WebSocket if (!guild.IsSynced) { - await _gatewayLogger.DebugAsync("Ignored CHANNEL_CREATE, guild is not synced yet.").ConfigureAwait(false); + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); return; } } else { - await _gatewayLogger.WarningAsync("CHANNEL_CREATE referenced an unknown guild.").ConfigureAwait(false); + await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); return; } } @@ -716,9 +716,10 @@ namespace Discord.WebSocket var before = channel.Clone(); channel.Update(State, data); - if (!((channel as SocketGuildChannel)?.Guild.IsSynced ?? true)) + var guild = (channel as SocketGuildChannel)?.Guild; + if (!(guild?.IsSynced ?? true)) { - await _gatewayLogger.DebugAsync("Ignored CHANNEL_UPDATE, guild is not synced yet.").ConfigureAwait(false); + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); return; } @@ -726,7 +727,7 @@ namespace Discord.WebSocket } else { - await _gatewayLogger.WarningAsync("CHANNEL_UPDATE referenced an unknown channel.").ConfigureAwait(false); + await UnknownChannelAsync(type, data.Id).ConfigureAwait(false); return; } } @@ -746,13 +747,13 @@ namespace Discord.WebSocket if (!guild.IsSynced) { - await _gatewayLogger.DebugAsync("Ignored CHANNEL_DELETE, guild is not synced yet.").ConfigureAwait(false); + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); return; } } else { - await _gatewayLogger.WarningAsync("CHANNEL_DELETE referenced an unknown guild.").ConfigureAwait(false); + await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); return; } } @@ -763,7 +764,7 @@ namespace Discord.WebSocket await TimedInvokeAsync(_channelDestroyedEvent, nameof(ChannelDestroyed), channel).ConfigureAwait(false); else { - await _gatewayLogger.WarningAsync("CHANNEL_DELETE referenced an unknown channel.").ConfigureAwait(false); + await UnknownChannelAsync(type, data.Id, data.GuildId.GetValueOrDefault(0)).ConfigureAwait(false); return; } } @@ -783,7 +784,7 @@ namespace Discord.WebSocket if (!guild.IsSynced) { - await _gatewayLogger.DebugAsync("Ignored GUILD_MEMBER_ADD, guild is not synced yet.").ConfigureAwait(false); + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); return; } @@ -791,7 +792,7 @@ namespace Discord.WebSocket } else { - await _gatewayLogger.WarningAsync("GUILD_MEMBER_ADD referenced an unknown guild.").ConfigureAwait(false); + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); return; } } @@ -808,7 +809,7 @@ namespace Discord.WebSocket if (!guild.IsSynced) { - await _gatewayLogger.DebugAsync("Ignored GUILD_MEMBER_UPDATE, guild is not synced yet.").ConfigureAwait(false); + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); return; } @@ -821,18 +822,15 @@ namespace Discord.WebSocket else { if (!guild.HasAllMembers) - { - await _gatewayLogger.DebugAsync("Ignored GUILD_MEMBER_UPDATE, this user has not been downloaded yet.").ConfigureAwait(false); - return; - } - - await _gatewayLogger.WarningAsync("GUILD_MEMBER_UPDATE referenced an unknown user.").ConfigureAwait(false); + await IncompleteGuildUserAsync(type, data.User.Id, data.GuildId).ConfigureAwait(false); + else + await UnknownGuildUserAsync(type, data.User.Id, data.GuildId).ConfigureAwait(false); return; } } else { - await _gatewayLogger.WarningAsync("GUILD_MEMBER_UPDATE referenced an unknown guild.").ConfigureAwait(false); + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); return; } } @@ -850,7 +848,7 @@ namespace Discord.WebSocket if (!guild.IsSynced) { - await _gatewayLogger.DebugAsync("Ignored GUILD_MEMBER_REMOVE, guild is not synced yet.").ConfigureAwait(false); + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); return; } @@ -859,18 +857,15 @@ namespace Discord.WebSocket else { if (!guild.HasAllMembers) - { - await _gatewayLogger.DebugAsync("Ignored GUILD_MEMBER_REMOVE, this user has not been downloaded yet.").ConfigureAwait(false); - return; - } - - await _gatewayLogger.WarningAsync("GUILD_MEMBER_REMOVE referenced an unknown user.").ConfigureAwait(false); + await IncompleteGuildUserAsync(type, data.User.Id, data.GuildId).ConfigureAwait(false); + else + await UnknownGuildUserAsync(type, data.User.Id, data.GuildId).ConfigureAwait(false); return; } } else { - await _gatewayLogger.WarningAsync("GUILD_MEMBER_REMOVE referenced an unknown guild.").ConfigureAwait(false); + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); return; } } @@ -894,7 +889,7 @@ namespace Discord.WebSocket } else { - await _gatewayLogger.WarningAsync("GUILD_MEMBERS_CHUNK referenced an unknown guild.").ConfigureAwait(false); + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); return; } } @@ -912,7 +907,7 @@ namespace Discord.WebSocket } else { - await _gatewayLogger.WarningAsync("CHANNEL_RECIPIENT_ADD referenced an unknown channel.").ConfigureAwait(false); + await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); return; } } @@ -930,13 +925,13 @@ namespace Discord.WebSocket await TimedInvokeAsync(_recipientRemovedEvent, nameof(RecipientRemoved), user).ConfigureAwait(false); else { - await _gatewayLogger.WarningAsync("CHANNEL_RECIPIENT_REMOVE referenced an unknown user.").ConfigureAwait(false); + await UnknownChannelUserAsync(type, data.User.Id, data.ChannelId).ConfigureAwait(false); return; } } else { - await _gatewayLogger.WarningAsync("CHANNEL_RECIPIENT_ADD referenced an unknown channel.").ConfigureAwait(false); + await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); return; } } @@ -955,14 +950,14 @@ namespace Discord.WebSocket if (!guild.IsSynced) { - await _gatewayLogger.DebugAsync("Ignored GUILD_ROLE_CREATE, guild is not synced yet.").ConfigureAwait(false); + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); return; } await TimedInvokeAsync(_roleCreatedEvent, nameof(RoleCreated), role).ConfigureAwait(false); } else { - await _gatewayLogger.WarningAsync("GUILD_ROLE_CREATE referenced an unknown guild.").ConfigureAwait(false); + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); return; } } @@ -983,7 +978,7 @@ namespace Discord.WebSocket if (!guild.IsSynced) { - await _gatewayLogger.DebugAsync("Ignored GUILD_ROLE_UPDATE, guild is not synced yet.").ConfigureAwait(false); + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); return; } @@ -991,13 +986,13 @@ namespace Discord.WebSocket } else { - await _gatewayLogger.WarningAsync("GUILD_ROLE_UPDATE referenced an unknown role.").ConfigureAwait(false); + await UnknownRoleAsync(type, data.Role.Id, guild.Id).ConfigureAwait(false); return; } } else { - await _gatewayLogger.WarningAsync("GUILD_ROLE_UPDATE referenced an unknown guild.").ConfigureAwait(false); + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); return; } } @@ -1015,7 +1010,7 @@ namespace Discord.WebSocket { if (!guild.IsSynced) { - await _gatewayLogger.DebugAsync("Ignored GUILD_ROLE_DELETE, guild is not synced yet.").ConfigureAwait(false); + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); return; } @@ -1023,13 +1018,13 @@ namespace Discord.WebSocket } else { - await _gatewayLogger.WarningAsync("GUILD_ROLE_DELETE referenced an unknown role.").ConfigureAwait(false); + await UnknownRoleAsync(type, data.RoleId, guild.Id).ConfigureAwait(false); return; } } else { - await _gatewayLogger.WarningAsync("GUILD_ROLE_DELETE referenced an unknown guild.").ConfigureAwait(false); + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); return; } } @@ -1046,7 +1041,7 @@ namespace Discord.WebSocket { if (!guild.IsSynced) { - await _gatewayLogger.DebugAsync("Ignored GUILD_BAN_ADD, guild is not synced yet.").ConfigureAwait(false); + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); return; } @@ -1057,7 +1052,7 @@ namespace Discord.WebSocket } else { - await _gatewayLogger.WarningAsync("GUILD_BAN_ADD referenced an unknown guild.").ConfigureAwait(false); + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); return; } } @@ -1072,7 +1067,7 @@ namespace Discord.WebSocket { if (!guild.IsSynced) { - await _gatewayLogger.DebugAsync("Ignored GUILD_BAN_REMOVE, guild is not synced yet.").ConfigureAwait(false); + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); return; } @@ -1083,7 +1078,7 @@ namespace Discord.WebSocket } else { - await _gatewayLogger.WarningAsync("GUILD_BAN_REMOVE referenced an unknown guild.").ConfigureAwait(false); + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); return; } } @@ -1101,7 +1096,7 @@ namespace Discord.WebSocket var guild = (channel as SocketGuildChannel)?.Guild; if (guild != null && !guild.IsSynced) { - await _gatewayLogger.DebugAsync("Ignored MESSAGE_CREATE, guild is not synced yet.").ConfigureAwait(false); + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); return; } @@ -1124,13 +1119,16 @@ namespace Discord.WebSocket } else { - await _gatewayLogger.WarningAsync("MESSAGE_CREATE referenced an unknown user.").ConfigureAwait(false); + if (guild != null) + await UnknownGuildUserAsync(type, data.Author.Value.Id, guild.Id).ConfigureAwait(false); + else + await UnknownChannelUserAsync(type, data.Author.Value.Id, channel.Id).ConfigureAwait(false); return; } } else { - await _gatewayLogger.WarningAsync("MESSAGE_CREATE referenced an unknown channel.").ConfigureAwait(false); + await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); return; } } @@ -1146,7 +1144,7 @@ namespace Discord.WebSocket var guild = (channel as SocketGuildChannel)?.Guild; if (guild != null && !guild.IsSynced) { - await _gatewayLogger.DebugAsync("Ignored MESSAGE_UPDATE, guild is not synced yet.").ConfigureAwait(false); + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); return; } @@ -1178,7 +1176,7 @@ namespace Discord.WebSocket } else { - await _gatewayLogger.WarningAsync("MESSAGE_UPDATE referenced an unknown channel.").ConfigureAwait(false); + await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); return; } } @@ -1191,9 +1189,10 @@ namespace Discord.WebSocket var channel = State.GetChannel(data.ChannelId) as ISocketMessageChannel; if (channel != null) { - if (!((channel as SocketGuildChannel)?.Guild.IsSynced ?? true)) + var guild = (channel as SocketGuildChannel)?.Guild; + if (!(guild?.IsSynced ?? true)) { - await _gatewayLogger.DebugAsync("Ignored MESSAGE_DELETE, guild is not synced yet.").ConfigureAwait(false); + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); return; } @@ -1205,7 +1204,7 @@ namespace Discord.WebSocket } else { - await _gatewayLogger.WarningAsync("MESSAGE_DELETE referenced an unknown channel.").ConfigureAwait(false); + await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); return; } } @@ -1230,7 +1229,7 @@ namespace Discord.WebSocket } else { - await _gatewayLogger.WarningAsync("MESSAGE_REACTION_ADD referenced an unknown channel.").ConfigureAwait(false); + await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); return; } } @@ -1255,7 +1254,7 @@ namespace Discord.WebSocket } else { - await _gatewayLogger.WarningAsync("MESSAGE_REACTION_REMOVE referenced an unknown channel.").ConfigureAwait(false); + await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); return; } } @@ -1278,7 +1277,7 @@ namespace Discord.WebSocket } else { - await _gatewayLogger.WarningAsync("MESSAGE_REACTION_REMOVE_ALL referenced an unknown channel.").ConfigureAwait(false); + await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); return; } } @@ -1291,9 +1290,10 @@ namespace Discord.WebSocket var channel = State.GetChannel(data.ChannelId) as ISocketMessageChannel; if (channel != null) { - if (!((channel as SocketGuildChannel)?.Guild.IsSynced ?? true)) + var guild = (channel as SocketGuildChannel)?.Guild; + if (!(guild?.IsSynced ?? true)) { - await _gatewayLogger.DebugAsync("Ignored MESSAGE_DELETE_BULK, guild is not synced yet.").ConfigureAwait(false); + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); return; } @@ -1307,7 +1307,7 @@ namespace Discord.WebSocket } else { - await _gatewayLogger.WarningAsync("MESSAGE_DELETE_BULK referenced an unknown channel.").ConfigureAwait(false); + await UnknownChannelAsync(type, data.ChannelId).ConfigureAwait(false); return; } } @@ -1325,12 +1325,12 @@ namespace Discord.WebSocket var guild = State.GetGuild(data.GuildId.Value); if (guild == null) { - await _gatewayLogger.WarningAsync("PRESENCE_UPDATE referenced an unknown guild.").ConfigureAwait(false); - break; + await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); + return; } if (!guild.IsSynced) { - await _gatewayLogger.DebugAsync("Ignored PRESENCE_UPDATE, guild is not synced yet.").ConfigureAwait(false); + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); return; } @@ -1356,8 +1356,8 @@ namespace Discord.WebSocket var globalUser = State.GetUser(data.User.Id); if (globalUser == null) { - await _gatewayLogger.WarningAsync("PRESENCE_UPDATE referenced an unknown user.").ConfigureAwait(false); - break; + await UnknownGlobalUserAsync(type, data.User.Id).ConfigureAwait(false); + return; } var before = globalUser.Clone(); @@ -1375,9 +1375,10 @@ namespace Discord.WebSocket var channel = State.GetChannel(data.ChannelId) as ISocketMessageChannel; if (channel != null) { - if (!((channel as SocketGuildChannel)?.Guild.IsSynced ?? true)) + var guild = (channel as SocketGuildChannel)?.Guild; + if (!(guild?.IsSynced ?? true)) { - await _gatewayLogger.DebugAsync("Ignored TYPING_START, guild is not synced yet.").ConfigureAwait(false); + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); return; } @@ -1414,73 +1415,71 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (VOICE_STATE_UPDATE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - if (data.GuildId.HasValue) + SocketUser user; + SocketVoiceState before, after; + if (data.GuildId != null) { - SocketUser user; - SocketVoiceState before, after; - if (data.GuildId != null) + var guild = State.GetGuild(data.GuildId.Value); + if (guild == null) { - var guild = State.GetGuild(data.GuildId.Value); - if (guild == null) - { - await _gatewayLogger.WarningAsync("VOICE_STATE_UPDATE referenced an unknown guild.").ConfigureAwait(false); - return; - } - else if (!guild.IsSynced) - { - await _gatewayLogger.DebugAsync("Ignored VOICE_STATE_UPDATE, guild is not synced yet.").ConfigureAwait(false); - return; - } + await UnknownGuildAsync(type, data.GuildId.Value).ConfigureAwait(false); + return; + } + else if (!guild.IsSynced) + { + await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); + return; + } - if (data.ChannelId != null) - { - before = guild.GetVoiceState(data.UserId)?.Clone() ?? SocketVoiceState.Default; - after = await guild.AddOrUpdateVoiceStateAsync(State, data).ConfigureAwait(false); - /*if (data.UserId == CurrentUser.Id) - { - var _ = guild.FinishJoinAudioChannel().ConfigureAwait(false); - }*/ - } - else + if (data.ChannelId != null) + { + before = guild.GetVoiceState(data.UserId)?.Clone() ?? SocketVoiceState.Default; + after = await guild.AddOrUpdateVoiceStateAsync(State, data).ConfigureAwait(false); + /*if (data.UserId == CurrentUser.Id) { - before = guild.RemoveVoiceState(data.UserId) ?? SocketVoiceState.Default; - after = SocketVoiceState.Create(null, data); - } - - user = guild.GetUser(data.UserId); + var _ = guild.FinishJoinAudioChannel().ConfigureAwait(false); + }*/ } else { - var groupChannel = State.GetChannel(data.ChannelId.Value) as SocketGroupChannel; - if (groupChannel != null) - { - if (data.ChannelId != null) - { - before = groupChannel.GetVoiceState(data.UserId)?.Clone() ?? SocketVoiceState.Default; - after = groupChannel.AddOrUpdateVoiceState(State, data); - } - else - { - before = groupChannel.RemoveVoiceState(data.UserId) ?? SocketVoiceState.Default; - after = SocketVoiceState.Create(null, data); - } - user = groupChannel.GetUser(data.UserId); - } - else - { - await _gatewayLogger.WarningAsync("VOICE_STATE_UPDATE referenced an unknown channel.").ConfigureAwait(false); - return; - } + before = guild.RemoveVoiceState(data.UserId) ?? SocketVoiceState.Default; + after = SocketVoiceState.Create(null, data); } - if (user != null) - await TimedInvokeAsync(_userVoiceStateUpdatedEvent, nameof(UserVoiceStateUpdated), user, before, after).ConfigureAwait(false); + user = guild.GetUser(data.UserId); + if (user == null) + { + await UnknownGuildUserAsync(type, data.UserId, guild.Id).ConfigureAwait(false); + return; + } + } + else + { + var groupChannel = State.GetChannel(data.ChannelId.Value) as SocketGroupChannel; + if (groupChannel == null) + { + await UnknownChannelAsync(type, data.ChannelId.Value).ConfigureAwait(false); + return; + } + if (data.ChannelId != null) + { + before = groupChannel.GetVoiceState(data.UserId)?.Clone() ?? SocketVoiceState.Default; + after = groupChannel.AddOrUpdateVoiceState(State, data); + } else { - await _gatewayLogger.WarningAsync("VOICE_STATE_UPDATE referenced an unknown user.").ConfigureAwait(false); + before = groupChannel.RemoveVoiceState(data.UserId) ?? SocketVoiceState.Default; + after = SocketVoiceState.Create(null, data); + } + user = groupChannel.GetUser(data.UserId); + if (user == null) + { + await UnknownChannelUserAsync(type, data.UserId, groupChannel.Id).ConfigureAwait(false); return; } } + + await TimedInvokeAsync(_userVoiceStateUpdatedEvent, nameof(UserVoiceStateUpdated), user, before, after).ConfigureAwait(false); } break; case "VOICE_SERVER_UPDATE": @@ -1496,7 +1495,7 @@ namespace Discord.WebSocket } else { - await _gatewayLogger.WarningAsync("VOICE_SERVER_UPDATE referenced an unknown guild.").ConfigureAwait(false); + await UnknownGuildAsync(type, data.GuildId).ConfigureAwait(false); return; } } @@ -1746,6 +1745,57 @@ namespace Discord.WebSocket } } + private async Task UnknownGlobalUserAsync(string evnt, ulong userId) + { + var details = $"{evnt} User={userId}"; + await _gatewayLogger.WarningAsync($"Unknown User ({details}).").ConfigureAwait(false); + } + private async Task UnknownChannelUserAsync(string evnt, ulong userId, ulong channelId) + { + var details = $"{evnt} User={userId} Channel={channelId}"; + await _gatewayLogger.WarningAsync($"Unknown User ({details}).").ConfigureAwait(false); + } + private async Task UnknownGuildUserAsync(string evnt, ulong userId, ulong guildId) + { + var details = $"{evnt} User={userId} Guild={guildId}"; + await _gatewayLogger.WarningAsync($"Unknown User ({details}).").ConfigureAwait(false); + } + private async Task IncompleteGuildUserAsync(string evnt, ulong userId, ulong guildId) + { + var details = $"{evnt} User={userId} Guild={guildId}"; + await _gatewayLogger.DebugAsync($"User has not been downloaded ({details}).").ConfigureAwait(false); + } + private async Task UnknownChannelAsync(string evnt, ulong channelId) + { + var details = $"{evnt} Channel={channelId}"; + await _gatewayLogger.WarningAsync($"Unknown Channel ({details}).").ConfigureAwait(false); + } + private async Task UnknownChannelAsync(string evnt, ulong channelId, ulong guildId) + { + if (guildId == 0) + { + await UnknownChannelAsync(evnt, channelId).ConfigureAwait(false); + return; + } + var details = $"{evnt} Channel={channelId} Guild={guildId}"; + await _gatewayLogger.WarningAsync($"Unknown Channel ({details}).").ConfigureAwait(false); + } + private async Task UnknownRoleAsync(string evnt, ulong roleId, ulong guildId) + { + var details = $"{evnt} Role={roleId} Guild={guildId}"; + await _gatewayLogger.WarningAsync($"Unknown Role ({details}).").ConfigureAwait(false); + } + private async Task UnknownGuildAsync(string evnt, ulong guildId) + { + var details = $"{evnt} Guild={guildId}"; + await _gatewayLogger.WarningAsync($"Unknown Guild ({details}).").ConfigureAwait(false); + } + private async Task UnsyncedGuildAsync(string evnt, ulong guildId) + { + var details = $"{evnt} Guild={guildId}"; + await _gatewayLogger.DebugAsync($"Unsynced Guild ({details}).").ConfigureAwait(false); + } + internal int GetAudioId() => _nextAudioId++; //IDiscordClient diff --git a/src/Discord.Net.WebSocket/Entities/Channels/ISocketAudioChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/ISocketAudioChannel.cs index c15eaf17b..7056a4df5 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/ISocketAudioChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/ISocketAudioChannel.cs @@ -1,7 +1,4 @@ -using Discord.Audio; -using System.Threading.Tasks; - -namespace Discord.WebSocket +namespace Discord.WebSocket { public interface ISocketAudioChannel : IAudioChannel { From 58d2de257849e527afa8881ec0f19264f30f4aca Mon Sep 17 00:00:00 2001 From: RogueException Date: Sun, 2 Apr 2017 14:49:04 -0300 Subject: [PATCH 089/243] Added config for handler timeout duration --- .../DiscordSocketClient.cs | 20 +++++++++---------- .../DiscordSocketConfig.cs | 4 ++-- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index c7fcef572..f56da83b9 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -21,8 +21,6 @@ namespace Discord.WebSocket { public partial class DiscordSocketClient : BaseDiscordClient, IDiscordClient { - private const int HandlerTimeoutMillis = 3000; - private readonly ConcurrentQueue _largeGuilds; private readonly JsonSerializer _serializer; private readonly SemaphoreSlim _connectionGroupLock; @@ -59,7 +57,7 @@ namespace Discord.WebSocket internal UdpSocketProvider UdpSocketProvider { get; private set; } internal WebSocketProvider WebSocketProvider { get; private set; } internal bool AlwaysDownloadUsers { get; private set; } - internal bool EnableHandlerTimeouts { get; private set; } + internal int? HandlerTimeout { get; private set; } internal new DiscordSocketApiClient ApiClient => base.ApiClient as DiscordSocketApiClient; public new SocketSelfUser CurrentUser { get { return base.CurrentUser as SocketSelfUser; } private set { base.CurrentUser = value; } } @@ -86,7 +84,7 @@ namespace Discord.WebSocket UdpSocketProvider = config.UdpSocketProvider; WebSocketProvider = config.WebSocketProvider; AlwaysDownloadUsers = config.AlwaysDownloadUsers; - EnableHandlerTimeouts = config.EnableHandlerTimeouts; + HandlerTimeout = config.HandlerTimeout; State = new ClientState(0, 0); _heartbeatTimes = new ConcurrentQueue(); @@ -1671,7 +1669,7 @@ namespace Discord.WebSocket { if (eventHandler.HasSubscribers) { - if (EnableHandlerTimeouts) + if (HandlerTimeout.HasValue) await TimeoutWrap(name, () => eventHandler.InvokeAsync()).ConfigureAwait(false); else await eventHandler.InvokeAsync().ConfigureAwait(false); @@ -1681,7 +1679,7 @@ namespace Discord.WebSocket { if (eventHandler.HasSubscribers) { - if (EnableHandlerTimeouts) + if (HandlerTimeout.HasValue) await TimeoutWrap(name, () => eventHandler.InvokeAsync(arg)).ConfigureAwait(false); else await eventHandler.InvokeAsync(arg).ConfigureAwait(false); @@ -1691,7 +1689,7 @@ namespace Discord.WebSocket { if (eventHandler.HasSubscribers) { - if (EnableHandlerTimeouts) + if (HandlerTimeout.HasValue) await TimeoutWrap(name, () => eventHandler.InvokeAsync(arg1, arg2)).ConfigureAwait(false); else await eventHandler.InvokeAsync(arg1, arg2).ConfigureAwait(false); @@ -1701,7 +1699,7 @@ namespace Discord.WebSocket { if (eventHandler.HasSubscribers) { - if (EnableHandlerTimeouts) + if (HandlerTimeout.HasValue) await TimeoutWrap(name, () => eventHandler.InvokeAsync(arg1, arg2, arg3)).ConfigureAwait(false); else await eventHandler.InvokeAsync(arg1, arg2, arg3).ConfigureAwait(false); @@ -1711,7 +1709,7 @@ namespace Discord.WebSocket { if (eventHandler.HasSubscribers) { - if (EnableHandlerTimeouts) + if (HandlerTimeout.HasValue) await TimeoutWrap(name, () => eventHandler.InvokeAsync(arg1, arg2, arg3, arg4)).ConfigureAwait(false); else await eventHandler.InvokeAsync(arg1, arg2, arg3, arg4).ConfigureAwait(false); @@ -1721,7 +1719,7 @@ namespace Discord.WebSocket { if (eventHandler.HasSubscribers) { - if (EnableHandlerTimeouts) + if (HandlerTimeout.HasValue) await TimeoutWrap(name, () => eventHandler.InvokeAsync(arg1, arg2, arg3, arg4, arg5)).ConfigureAwait(false); else await eventHandler.InvokeAsync(arg1, arg2, arg3, arg4, arg5).ConfigureAwait(false); @@ -1731,7 +1729,7 @@ namespace Discord.WebSocket { try { - var timeoutTask = Task.Delay(HandlerTimeoutMillis); + var timeoutTask = Task.Delay(HandlerTimeout.Value); var handlersTask = action(); if (await Task.WhenAny(timeoutTask, handlersTask).ConfigureAwait(false) == timeoutTask) { diff --git a/src/Discord.Net.WebSocket/DiscordSocketConfig.cs b/src/Discord.Net.WebSocket/DiscordSocketConfig.cs index add42ce80..3f9c18863 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketConfig.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketConfig.cs @@ -33,8 +33,8 @@ namespace Discord.WebSocket /// Gets or sets whether or not all users should be downloaded as guilds come available. public bool AlwaysDownloadUsers { get; set; } = false; - /// Gets or sets whether or not warnings should be logged if an event handler is taking too long to execute. - public bool EnableHandlerTimeouts { get; set; } = true; + /// Gets or sets the timeout for event handlers, in milliseconds, after which a warning will be logged. Null disables this check. + public int? HandlerTimeout { get; set; } = 3000; public DiscordSocketConfig() { From b1caec5f59c7bb0a3dba042491d7a1f5afdc4f67 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sun, 2 Apr 2017 15:08:28 -0300 Subject: [PATCH 090/243] Add better support for invisible users --- .../DiscordSocketClient.cs | 18 +++++++++--------- .../Entities/Channels/SocketGroupChannel.cs | 2 +- .../Entities/Guilds/SocketGuild.cs | 14 ++++++++++++++ .../Entities/Users/SocketGuildUser.cs | 14 +++++++++++--- 4 files changed, 35 insertions(+), 13 deletions(-) diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index f56da83b9..e4f88ef45 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -900,7 +900,7 @@ namespace Discord.WebSocket var channel = State.GetChannel(data.ChannelId) as SocketGroupChannel; if (channel != null) { - var user = channel.AddUser(data.User); + var user = channel.GetOrAddUser(data.User); await TimedInvokeAsync(_recipientAddedEvent, nameof(RecipientAdded), user).ConfigureAwait(false); } else @@ -1109,20 +1109,20 @@ namespace Discord.WebSocket else author = (channel as SocketChannel).GetUser(data.Author.Value.Id); - if (author != null) - { - var msg = SocketMessage.Create(this, State, author, channel, data); - SocketChannelHelper.AddMessage(channel, this, msg); - await TimedInvokeAsync(_messageReceivedEvent, nameof(MessageReceived), msg).ConfigureAwait(false); - } - else + if (author == null) { if (guild != null) - await UnknownGuildUserAsync(type, data.Author.Value.Id, guild.Id).ConfigureAwait(false); + author = guild.AddOrUpdateUser(data.Author.Value); //User has no guild-specific data + else if (channel is SocketGroupChannel) + author = (channel as SocketGroupChannel).GetOrAddUser(data.Author.Value); else await UnknownChannelUserAsync(type, data.Author.Value.Id, channel.Id).ConfigureAwait(false); return; } + + var msg = SocketMessage.Create(this, State, author, channel, data); + SocketChannelHelper.AddMessage(channel, this, msg); + await TimedInvokeAsync(_messageReceivedEvent, nameof(MessageReceived), msg).ConfigureAwait(false); } else { diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs index e6c875e5a..ff7048848 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs @@ -125,7 +125,7 @@ namespace Discord.WebSocket return user; return null; } - internal SocketGroupUser AddUser(UserModel model) + internal SocketGroupUser GetOrAddUser(UserModel model) { SocketGroupUser user; if (_users.TryGetValue(model.Id, out user)) diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index 9f106ff1c..8193a971f 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -16,6 +16,7 @@ using MemberModel = Discord.API.GuildMember; using Model = Discord.API.Guild; using PresenceModel = Discord.API.Presence; using RoleModel = Discord.API.Role; +using UserModel = Discord.API.User; using VoiceStateModel = Discord.API.VoiceState; namespace Discord.WebSocket @@ -375,6 +376,19 @@ namespace Discord.WebSocket public Task PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null) => GuildHelper.PruneUsersAsync(this, Discord, days, simulate, options); + internal SocketGuildUser AddOrUpdateUser(UserModel model) + { + SocketGuildUser member; + if (_members.TryGetValue(model.Id, out member)) + member.GlobalUser?.Update(Discord.State, model); + else + { + member = SocketGuildUser.Create(this, Discord.State, model); + _members[member.Id] = member; + DownloadedMemberCount++; + } + return member; + } internal SocketGuildUser AddOrUpdateUser(MemberModel model) { SocketGuildUser member; diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs index 58d2a0b8d..63dc64dbf 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs @@ -6,7 +6,8 @@ using System.Collections.Immutable; using System.Diagnostics; using System.Linq; using System.Threading.Tasks; -using Model = Discord.API.GuildMember; +using UserModel = Discord.API.User; +using MemberModel = Discord.API.GuildMember; using PresenceModel = Discord.API.Presence; namespace Discord.WebSocket @@ -69,7 +70,14 @@ namespace Discord.WebSocket Guild = guild; GlobalUser = globalUser; } - internal static SocketGuildUser Create(SocketGuild guild, ClientState state, Model model) + internal static SocketGuildUser Create(SocketGuild guild, ClientState state, UserModel model) + { + var entity = new SocketGuildUser(guild, guild.Discord.GetOrCreateUser(state, model)); + entity.Update(state, model); + entity.UpdateRoles(new ulong[0]); + return entity; + } + internal static SocketGuildUser Create(SocketGuild guild, ClientState state, MemberModel model) { var entity = new SocketGuildUser(guild, guild.Discord.GetOrCreateUser(state, model.User)); entity.Update(state, model); @@ -81,7 +89,7 @@ namespace Discord.WebSocket entity.Update(state, model, false); return entity; } - internal void Update(ClientState state, Model model) + internal void Update(ClientState state, MemberModel model) { base.Update(state, model.User); if (model.JoinedAt.IsSpecified) From d6b6a95a2efa80cb09e4a151a441d6d0ea959403 Mon Sep 17 00:00:00 2001 From: RogueException Date: Mon, 3 Apr 2017 02:59:11 -0300 Subject: [PATCH 091/243] Renamed ClientAPIUrl -> APIUrl --- src/Discord.Net.Core/DiscordConfig.cs | 2 +- src/Discord.Net.Rest/DiscordRestApiClient.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net.Core/DiscordConfig.cs b/src/Discord.Net.Core/DiscordConfig.cs index 2a11a6edb..d24a0ea3a 100644 --- a/src/Discord.Net.Core/DiscordConfig.cs +++ b/src/Discord.Net.Core/DiscordConfig.cs @@ -10,7 +10,7 @@ namespace Discord typeof(DiscordConfig).GetTypeInfo().Assembly.GetName().Version.ToString(3) ?? "Unknown"; - public static readonly string ClientAPIUrl = $"https://discordapp.com/api/v{APIVersion}/"; + public static readonly string APIUrl = $"https://discordapp.com/api/v{APIVersion}/"; public const string CDNUrl = "https://cdn.discordapp.com/"; public const string InviteUrl = "https://discord.gg/"; diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index d57443605..c57d15645 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -56,7 +56,7 @@ namespace Discord.API RequestQueue = new RequestQueue(); _stateLock = new SemaphoreSlim(1, 1); - SetBaseUrl(DiscordConfig.ClientAPIUrl); + SetBaseUrl(DiscordConfig.APIUrl); } internal void SetBaseUrl(string baseUrl) { From 1b1ff1325fca830aabb102f223d8b9496df10689 Mon Sep 17 00:00:00 2001 From: RogueException Date: Mon, 3 Apr 2017 07:43:29 -0300 Subject: [PATCH 092/243] Fixed Discord.Net.Core's description --- src/Discord.Net.Core/Discord.Net.Core.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.Core/Discord.Net.Core.csproj b/src/Discord.Net.Core/Discord.Net.Core.csproj index 262ea9007..b41203a74 100644 --- a/src/Discord.Net.Core/Discord.Net.Core.csproj +++ b/src/Discord.Net.Core/Discord.Net.Core.csproj @@ -3,7 +3,7 @@ Discord.Net.Core Discord - A .Net API wrapper and bot framework for Discord. + The core components for the Discord.Net library. netstandard1.1;netstandard1.3 From 51464c0a4e292984a47e5f4b69d709c606ae85ea Mon Sep 17 00:00:00 2001 From: RogueException Date: Mon, 3 Apr 2017 07:44:04 -0300 Subject: [PATCH 093/243] Added webhook package to Discord.Net --- src/Discord.Net/Discord.Net.nuspec | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Discord.Net/Discord.Net.nuspec b/src/Discord.Net/Discord.Net.nuspec index d5537ec7c..e83cec52c 100644 --- a/src/Discord.Net/Discord.Net.nuspec +++ b/src/Discord.Net/Discord.Net.nuspec @@ -18,6 +18,7 @@ + @@ -25,6 +26,7 @@ + From df673e02e5241b3f9a1fa2a4b9b481726eb26fc9 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Mon, 3 Apr 2017 15:51:17 -0400 Subject: [PATCH 094/243] Coerce docfx into building API documentation Temporary fix while waiting on dotnet/docfx#1254 --- .gitignore | 5 ++++- docs/api/.manifest | 1 - docs/docfx.json | 4 ++-- docs/filterConfig.yml | 4 +++- 4 files changed, 9 insertions(+), 5 deletions(-) delete mode 100644 docs/api/.manifest diff --git a/.gitignore b/.gitignore index ccd272109..45e5e009d 100644 --- a/.gitignore +++ b/.gitignore @@ -202,4 +202,7 @@ project.lock.json /docs/_build *.pyc /.editorconfig -.vscode/ \ No newline at end of file +.vscode/ +docs/api/\.manifest + +\.idea/ diff --git a/docs/api/.manifest b/docs/api/.manifest deleted file mode 100644 index 0a47304c4..000000000 --- a/docs/api/.manifest +++ /dev/null @@ -1 +0,0 @@ -{"Discord.Rpc":"Discord.Rpc.yml","Discord.Rpc.DiscordRpcClient":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.ConnectionState":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.Scopes":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.TokenExpiresAt":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.CurrentUser":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.ApplicationInfo":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.#ctor(System.String,System.String)":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.#ctor(System.String,System.String,Discord.Rpc.DiscordRpcConfig)":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.ConnectAsync":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.DisconnectAsync":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.AuthorizeAsync(System.String[],System.String,RequestOptions)":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.SubscribeGlobal(Discord.Rpc.RpcGlobalEvent,RequestOptions)":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.UnsubscribeGlobal(Discord.Rpc.RpcGlobalEvent,RequestOptions)":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.SubscribeGuild(System.UInt64,Discord.Rpc.RpcChannelEvent,RequestOptions)":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.UnsubscribeGuild(System.UInt64,Discord.Rpc.RpcChannelEvent,RequestOptions)":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.SubscribeChannel(System.UInt64,Discord.Rpc.RpcChannelEvent,RequestOptions)":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.UnsubscribeChannel(System.UInt64,Discord.Rpc.RpcChannelEvent,RequestOptions)":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.GetRpcGuildAsync(System.UInt64,RequestOptions)":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.GetRpcGuildsAsync(RequestOptions)":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.GetRpcChannelAsync(System.UInt64,RequestOptions)":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.GetRpcChannelsAsync(System.UInt64,RequestOptions)":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.SelectTextChannelAsync(IChannel,RequestOptions)":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.SelectTextChannelAsync(Discord.Rpc.RpcChannelSummary,RequestOptions)":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.SelectTextChannelAsync(System.UInt64,RequestOptions)":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.SelectVoiceChannelAsync(IChannel,System.Boolean,RequestOptions)":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.SelectVoiceChannelAsync(Discord.Rpc.RpcChannelSummary,System.Boolean,RequestOptions)":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.SelectVoiceChannelAsync(System.UInt64,System.Boolean,RequestOptions)":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.GetVoiceSettingsAsync(RequestOptions)":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.SetVoiceSettingsAsync(Action{Discord.Rpc.VoiceProperties},RequestOptions)":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.SetUserVoiceSettingsAsync(System.UInt64,Action{Discord.Rpc.UserVoiceProperties},RequestOptions)":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.Connected":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.Disconnected":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.Ready":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.ChannelCreated":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.GuildCreated":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.GuildStatusUpdated":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.VoiceStateCreated":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.VoiceStateUpdated":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.VoiceStateDeleted":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.SpeakingStarted":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.SpeakingStopped":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.VoiceSettingsUpdated":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.MessageReceived":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.MessageUpdated":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcClient.MessageDeleted":"Discord.Rpc.DiscordRpcClient.yml","Discord.Rpc.DiscordRpcConfig":"Discord.Rpc.DiscordRpcConfig.yml","Discord.Rpc.DiscordRpcConfig.RpcAPIVersion":"Discord.Rpc.DiscordRpcConfig.yml","Discord.Rpc.DiscordRpcConfig.PortRangeStart":"Discord.Rpc.DiscordRpcConfig.yml","Discord.Rpc.DiscordRpcConfig.PortRangeEnd":"Discord.Rpc.DiscordRpcConfig.yml","Discord.Rpc.DiscordRpcConfig.ConnectionTimeout":"Discord.Rpc.DiscordRpcConfig.yml","Discord.Rpc.DiscordRpcConfig.WebSocketProvider":"Discord.Rpc.DiscordRpcConfig.yml","Discord.Rpc.DiscordRpcConfig.#ctor":"Discord.Rpc.DiscordRpcConfig.yml","Discord.Rpc.RpcChannelEvent":"Discord.Rpc.RpcChannelEvent.yml","Discord.Rpc.RpcChannelEvent.VoiceStateCreate":"Discord.Rpc.RpcChannelEvent.yml","Discord.Rpc.RpcChannelEvent.VoiceStateUpdate":"Discord.Rpc.RpcChannelEvent.yml","Discord.Rpc.RpcChannelEvent.VoiceStateDelete":"Discord.Rpc.RpcChannelEvent.yml","Discord.Rpc.RpcChannelEvent.SpeakingStart":"Discord.Rpc.RpcChannelEvent.yml","Discord.Rpc.RpcChannelEvent.SpeakingStop":"Discord.Rpc.RpcChannelEvent.yml","Discord.Rpc.RpcChannelEvent.MessageCreate":"Discord.Rpc.RpcChannelEvent.yml","Discord.Rpc.RpcChannelEvent.MessageUpdate":"Discord.Rpc.RpcChannelEvent.yml","Discord.Rpc.RpcChannelEvent.MessageDelete":"Discord.Rpc.RpcChannelEvent.yml","Discord.Rpc.RpcGlobalEvent":"Discord.Rpc.RpcGlobalEvent.yml","Discord.Rpc.RpcGlobalEvent.ChannelCreated":"Discord.Rpc.RpcGlobalEvent.yml","Discord.Rpc.RpcGlobalEvent.GuildCreated":"Discord.Rpc.RpcGlobalEvent.yml","Discord.Rpc.RpcGlobalEvent.VoiceSettingsUpdated":"Discord.Rpc.RpcGlobalEvent.yml","Discord.Rpc.RpcGuildEvent":"Discord.Rpc.RpcGuildEvent.yml","Discord.Rpc.RpcGuildEvent.GuildStatus":"Discord.Rpc.RpcGuildEvent.yml","Discord.Rpc.RpcEntity`1":"Discord.Rpc.RpcEntity-1.yml","Discord.Rpc.RpcEntity`1.Discord":"Discord.Rpc.RpcEntity-1.yml","Discord.Rpc.RpcEntity`1.Id":"Discord.Rpc.RpcEntity-1.yml","Discord.Rpc.UserVoiceProperties":"Discord.Rpc.UserVoiceProperties.yml","Discord.Rpc.UserVoiceProperties.Pan":"Discord.Rpc.UserVoiceProperties.yml","Discord.Rpc.UserVoiceProperties.Volume":"Discord.Rpc.UserVoiceProperties.yml","Discord.Rpc.UserVoiceProperties.Mute":"Discord.Rpc.UserVoiceProperties.yml","Discord.Rpc.VoiceDevice":"Discord.Rpc.VoiceDevice.yml","Discord.Rpc.VoiceDevice.Id":"Discord.Rpc.VoiceDevice.yml","Discord.Rpc.VoiceDevice.Name":"Discord.Rpc.VoiceDevice.yml","Discord.Rpc.VoiceDevice.ToString":"Discord.Rpc.VoiceDevice.yml","Discord.Rpc.VoiceDeviceProperties":"Discord.Rpc.VoiceDeviceProperties.yml","Discord.Rpc.VoiceDeviceProperties.DeviceId":"Discord.Rpc.VoiceDeviceProperties.yml","Discord.Rpc.VoiceDeviceProperties.Volume":"Discord.Rpc.VoiceDeviceProperties.yml","Discord.Rpc.VoiceDeviceProperties.AvailableDevices":"Discord.Rpc.VoiceDeviceProperties.yml","Discord.Rpc.VoiceModeProperties":"Discord.Rpc.VoiceModeProperties.yml","Discord.Rpc.VoiceModeProperties.Type":"Discord.Rpc.VoiceModeProperties.yml","Discord.Rpc.VoiceModeProperties.AutoThreshold":"Discord.Rpc.VoiceModeProperties.yml","Discord.Rpc.VoiceModeProperties.Threshold":"Discord.Rpc.VoiceModeProperties.yml","Discord.Rpc.VoiceModeProperties.Shortcut":"Discord.Rpc.VoiceModeProperties.yml","Discord.Rpc.VoiceModeProperties.Delay":"Discord.Rpc.VoiceModeProperties.yml","Discord.Rpc.VoiceProperties":"Discord.Rpc.VoiceProperties.yml","Discord.Rpc.VoiceProperties.Input":"Discord.Rpc.VoiceProperties.yml","Discord.Rpc.VoiceProperties.Output":"Discord.Rpc.VoiceProperties.yml","Discord.Rpc.VoiceProperties.Mode":"Discord.Rpc.VoiceProperties.yml","Discord.Rpc.VoiceProperties.AutomaticGainControl":"Discord.Rpc.VoiceProperties.yml","Discord.Rpc.VoiceProperties.EchoCancellation":"Discord.Rpc.VoiceProperties.yml","Discord.Rpc.VoiceProperties.NoiseSuppression":"Discord.Rpc.VoiceProperties.yml","Discord.Rpc.VoiceProperties.QualityOfService":"Discord.Rpc.VoiceProperties.yml","Discord.Rpc.VoiceProperties.SilenceWarning":"Discord.Rpc.VoiceProperties.yml","Discord.Rpc.VoiceSettings":"Discord.Rpc.VoiceSettings.yml","Discord.Rpc.VoiceSettings.InputDeviceId":"Discord.Rpc.VoiceSettings.yml","Discord.Rpc.VoiceSettings.InputVolume":"Discord.Rpc.VoiceSettings.yml","Discord.Rpc.VoiceSettings.AvailableInputDevices":"Discord.Rpc.VoiceSettings.yml","Discord.Rpc.VoiceSettings.OutputDeviceId":"Discord.Rpc.VoiceSettings.yml","Discord.Rpc.VoiceSettings.OutputVolume":"Discord.Rpc.VoiceSettings.yml","Discord.Rpc.VoiceSettings.AvailableOutputDevices":"Discord.Rpc.VoiceSettings.yml","Discord.Rpc.VoiceSettings.AutomaticGainControl":"Discord.Rpc.VoiceSettings.yml","Discord.Rpc.VoiceSettings.EchoCancellation":"Discord.Rpc.VoiceSettings.yml","Discord.Rpc.VoiceSettings.NoiseSuppression":"Discord.Rpc.VoiceSettings.yml","Discord.Rpc.VoiceSettings.QualityOfService":"Discord.Rpc.VoiceSettings.yml","Discord.Rpc.VoiceSettings.SilenceWarning":"Discord.Rpc.VoiceSettings.yml","Discord.Rpc.VoiceSettings.ActivationMode":"Discord.Rpc.VoiceSettings.yml","Discord.Rpc.VoiceSettings.AutoThreshold":"Discord.Rpc.VoiceSettings.yml","Discord.Rpc.VoiceSettings.Threshold":"Discord.Rpc.VoiceSettings.yml","Discord.Rpc.VoiceSettings.Shortcuts":"Discord.Rpc.VoiceSettings.yml","Discord.Rpc.VoiceSettings.Delay":"Discord.Rpc.VoiceSettings.yml","Discord.Rpc.VoiceShortcut":"Discord.Rpc.VoiceShortcut.yml","Discord.Rpc.VoiceShortcut.Type":"Discord.Rpc.VoiceShortcut.yml","Discord.Rpc.VoiceShortcut.Code":"Discord.Rpc.VoiceShortcut.yml","Discord.Rpc.VoiceShortcut.Name":"Discord.Rpc.VoiceShortcut.yml","Discord.Rpc.VoiceShortcut.ToString":"Discord.Rpc.VoiceShortcut.yml","Discord.Rpc.VoiceShortcutType":"Discord.Rpc.VoiceShortcutType.yml","Discord.Rpc.VoiceShortcutType.KeyboardKey":"Discord.Rpc.VoiceShortcutType.yml","Discord.Rpc.VoiceShortcutType.MouseButton":"Discord.Rpc.VoiceShortcutType.yml","Discord.Rpc.VoiceShortcutType.KeyboardModifierKey":"Discord.Rpc.VoiceShortcutType.yml","Discord.Rpc.VoiceShortcutType.GamepadButton":"Discord.Rpc.VoiceShortcutType.yml","Discord.Rpc.IRpcAudioChannel":"Discord.Rpc.IRpcAudioChannel.yml","Discord.Rpc.IRpcAudioChannel.VoiceStates":"Discord.Rpc.IRpcAudioChannel.yml","Discord.Rpc.IRpcMessageChannel":"Discord.Rpc.IRpcMessageChannel.yml","Discord.Rpc.IRpcMessageChannel.CachedMessages":"Discord.Rpc.IRpcMessageChannel.yml","Discord.Rpc.IRpcPrivateChannel":"Discord.Rpc.IRpcPrivateChannel.yml","Discord.Rpc.RpcChannel":"Discord.Rpc.RpcChannel.yml","Discord.Rpc.RpcChannel.Name":"Discord.Rpc.RpcChannel.yml","Discord.Rpc.RpcChannel.CreatedAt":"Discord.Rpc.RpcChannel.yml","Discord.Rpc.RpcChannelSummary":"Discord.Rpc.RpcChannelSummary.yml","Discord.Rpc.RpcChannelSummary.Id":"Discord.Rpc.RpcChannelSummary.yml","Discord.Rpc.RpcChannelSummary.Name":"Discord.Rpc.RpcChannelSummary.yml","Discord.Rpc.RpcChannelSummary.Type":"Discord.Rpc.RpcChannelSummary.yml","Discord.Rpc.RpcChannelSummary.ToString":"Discord.Rpc.RpcChannelSummary.yml","Discord.Rpc.RpcDMChannel":"Discord.Rpc.RpcDMChannel.yml","Discord.Rpc.RpcDMChannel.CachedMessages":"Discord.Rpc.RpcDMChannel.yml","Discord.Rpc.RpcDMChannel.CloseAsync(RequestOptions)":"Discord.Rpc.RpcDMChannel.yml","Discord.Rpc.RpcDMChannel.GetMessageAsync(System.UInt64,RequestOptions)":"Discord.Rpc.RpcDMChannel.yml","Discord.Rpc.RpcDMChannel.GetMessagesAsync(System.Int32,RequestOptions)":"Discord.Rpc.RpcDMChannel.yml","Discord.Rpc.RpcDMChannel.GetMessagesAsync(System.UInt64,Direction,System.Int32,RequestOptions)":"Discord.Rpc.RpcDMChannel.yml","Discord.Rpc.RpcDMChannel.GetMessagesAsync(IMessage,Direction,System.Int32,RequestOptions)":"Discord.Rpc.RpcDMChannel.yml","Discord.Rpc.RpcDMChannel.GetPinnedMessagesAsync(RequestOptions)":"Discord.Rpc.RpcDMChannel.yml","Discord.Rpc.RpcDMChannel.SendMessageAsync(System.String,System.Boolean,Embed,RequestOptions)":"Discord.Rpc.RpcDMChannel.yml","Discord.Rpc.RpcDMChannel.SendFileAsync(Stream,System.String,System.String,System.Boolean,RequestOptions)":"Discord.Rpc.RpcDMChannel.yml","Discord.Rpc.RpcDMChannel.DeleteMessagesAsync(IEnumerable{IMessage},RequestOptions)":"Discord.Rpc.RpcDMChannel.yml","Discord.Rpc.RpcDMChannel.TriggerTypingAsync(RequestOptions)":"Discord.Rpc.RpcDMChannel.yml","Discord.Rpc.RpcDMChannel.EnterTypingState(RequestOptions)":"Discord.Rpc.RpcDMChannel.yml","Discord.Rpc.RpcDMChannel.ToString":"Discord.Rpc.RpcDMChannel.yml","Discord.Rpc.RpcGroupChannel":"Discord.Rpc.RpcGroupChannel.yml","Discord.Rpc.RpcGroupChannel.CachedMessages":"Discord.Rpc.RpcGroupChannel.yml","Discord.Rpc.RpcGroupChannel.VoiceStates":"Discord.Rpc.RpcGroupChannel.yml","Discord.Rpc.RpcGroupChannel.LeaveAsync(RequestOptions)":"Discord.Rpc.RpcGroupChannel.yml","Discord.Rpc.RpcGroupChannel.GetMessageAsync(System.UInt64,RequestOptions)":"Discord.Rpc.RpcGroupChannel.yml","Discord.Rpc.RpcGroupChannel.GetMessagesAsync(System.Int32,RequestOptions)":"Discord.Rpc.RpcGroupChannel.yml","Discord.Rpc.RpcGroupChannel.GetMessagesAsync(System.UInt64,Direction,System.Int32,RequestOptions)":"Discord.Rpc.RpcGroupChannel.yml","Discord.Rpc.RpcGroupChannel.GetMessagesAsync(IMessage,Direction,System.Int32,RequestOptions)":"Discord.Rpc.RpcGroupChannel.yml","Discord.Rpc.RpcGroupChannel.GetPinnedMessagesAsync(RequestOptions)":"Discord.Rpc.RpcGroupChannel.yml","Discord.Rpc.RpcGroupChannel.SendMessageAsync(System.String,System.Boolean,Embed,RequestOptions)":"Discord.Rpc.RpcGroupChannel.yml","Discord.Rpc.RpcGroupChannel.SendFileAsync(Stream,System.String,System.String,System.Boolean,RequestOptions)":"Discord.Rpc.RpcGroupChannel.yml","Discord.Rpc.RpcGroupChannel.DeleteMessagesAsync(IEnumerable{IMessage},RequestOptions)":"Discord.Rpc.RpcGroupChannel.yml","Discord.Rpc.RpcGroupChannel.TriggerTypingAsync(RequestOptions)":"Discord.Rpc.RpcGroupChannel.yml","Discord.Rpc.RpcGroupChannel.EnterTypingState(RequestOptions)":"Discord.Rpc.RpcGroupChannel.yml","Discord.Rpc.RpcGroupChannel.ToString":"Discord.Rpc.RpcGroupChannel.yml","Discord.Rpc.RpcGuildChannel":"Discord.Rpc.RpcGuildChannel.yml","Discord.Rpc.RpcGuildChannel.GuildId":"Discord.Rpc.RpcGuildChannel.yml","Discord.Rpc.RpcGuildChannel.Position":"Discord.Rpc.RpcGuildChannel.yml","Discord.Rpc.RpcGuildChannel.ModifyAsync(Action{GuildChannelProperties},RequestOptions)":"Discord.Rpc.RpcGuildChannel.yml","Discord.Rpc.RpcGuildChannel.DeleteAsync(RequestOptions)":"Discord.Rpc.RpcGuildChannel.yml","Discord.Rpc.RpcGuildChannel.AddPermissionOverwriteAsync(IUser,OverwritePermissions,RequestOptions)":"Discord.Rpc.RpcGuildChannel.yml","Discord.Rpc.RpcGuildChannel.AddPermissionOverwriteAsync(IRole,OverwritePermissions,RequestOptions)":"Discord.Rpc.RpcGuildChannel.yml","Discord.Rpc.RpcGuildChannel.RemovePermissionOverwriteAsync(IUser,RequestOptions)":"Discord.Rpc.RpcGuildChannel.yml","Discord.Rpc.RpcGuildChannel.RemovePermissionOverwriteAsync(IRole,RequestOptions)":"Discord.Rpc.RpcGuildChannel.yml","Discord.Rpc.RpcGuildChannel.GetInvitesAsync(RequestOptions)":"Discord.Rpc.RpcGuildChannel.yml","Discord.Rpc.RpcGuildChannel.CreateInviteAsync(System.Nullable{System.Int32},System.Nullable{System.Int32},System.Boolean,RequestOptions)":"Discord.Rpc.RpcGuildChannel.yml","Discord.Rpc.RpcGuildChannel.ToString":"Discord.Rpc.RpcGuildChannel.yml","Discord.Rpc.RpcTextChannel":"Discord.Rpc.RpcTextChannel.yml","Discord.Rpc.RpcTextChannel.CachedMessages":"Discord.Rpc.RpcTextChannel.yml","Discord.Rpc.RpcTextChannel.Mention":"Discord.Rpc.RpcTextChannel.yml","Discord.Rpc.RpcTextChannel.ModifyAsync(Action{TextChannelProperties},RequestOptions)":"Discord.Rpc.RpcTextChannel.yml","Discord.Rpc.RpcTextChannel.GetMessageAsync(System.UInt64,RequestOptions)":"Discord.Rpc.RpcTextChannel.yml","Discord.Rpc.RpcTextChannel.GetMessagesAsync(System.Int32,RequestOptions)":"Discord.Rpc.RpcTextChannel.yml","Discord.Rpc.RpcTextChannel.GetMessagesAsync(System.UInt64,Direction,System.Int32,RequestOptions)":"Discord.Rpc.RpcTextChannel.yml","Discord.Rpc.RpcTextChannel.GetMessagesAsync(IMessage,Direction,System.Int32,RequestOptions)":"Discord.Rpc.RpcTextChannel.yml","Discord.Rpc.RpcTextChannel.GetPinnedMessagesAsync(RequestOptions)":"Discord.Rpc.RpcTextChannel.yml","Discord.Rpc.RpcTextChannel.SendMessageAsync(System.String,System.Boolean,Embed,RequestOptions)":"Discord.Rpc.RpcTextChannel.yml","Discord.Rpc.RpcTextChannel.SendFileAsync(Stream,System.String,System.String,System.Boolean,RequestOptions)":"Discord.Rpc.RpcTextChannel.yml","Discord.Rpc.RpcTextChannel.DeleteMessagesAsync(IEnumerable{IMessage},RequestOptions)":"Discord.Rpc.RpcTextChannel.yml","Discord.Rpc.RpcTextChannel.TriggerTypingAsync(RequestOptions)":"Discord.Rpc.RpcTextChannel.yml","Discord.Rpc.RpcTextChannel.EnterTypingState(RequestOptions)":"Discord.Rpc.RpcTextChannel.yml","Discord.Rpc.RpcVoiceChannel":"Discord.Rpc.RpcVoiceChannel.yml","Discord.Rpc.RpcVoiceChannel.UserLimit":"Discord.Rpc.RpcVoiceChannel.yml","Discord.Rpc.RpcVoiceChannel.Bitrate":"Discord.Rpc.RpcVoiceChannel.yml","Discord.Rpc.RpcVoiceChannel.VoiceStates":"Discord.Rpc.RpcVoiceChannel.yml","Discord.Rpc.RpcVoiceChannel.ModifyAsync(Action{VoiceChannelProperties},RequestOptions)":"Discord.Rpc.RpcVoiceChannel.yml","Discord.Rpc.RpcGuild":"Discord.Rpc.RpcGuild.yml","Discord.Rpc.RpcGuild.Name":"Discord.Rpc.RpcGuild.yml","Discord.Rpc.RpcGuild.IconUrl":"Discord.Rpc.RpcGuild.yml","Discord.Rpc.RpcGuild.Users":"Discord.Rpc.RpcGuild.yml","Discord.Rpc.RpcGuild.ToString":"Discord.Rpc.RpcGuild.yml","Discord.Rpc.RpcGuildStatus":"Discord.Rpc.RpcGuildStatus.yml","Discord.Rpc.RpcGuildStatus.Guild":"Discord.Rpc.RpcGuildStatus.yml","Discord.Rpc.RpcGuildStatus.Online":"Discord.Rpc.RpcGuildStatus.yml","Discord.Rpc.RpcGuildStatus.ToString":"Discord.Rpc.RpcGuildStatus.yml","Discord.Rpc.RpcGuildSummary":"Discord.Rpc.RpcGuildSummary.yml","Discord.Rpc.RpcGuildSummary.Id":"Discord.Rpc.RpcGuildSummary.yml","Discord.Rpc.RpcGuildSummary.Name":"Discord.Rpc.RpcGuildSummary.yml","Discord.Rpc.RpcGuildSummary.ToString":"Discord.Rpc.RpcGuildSummary.yml","Discord.Rpc.RpcMessage":"Discord.Rpc.RpcMessage.yml","Discord.Rpc.RpcMessage.Channel":"Discord.Rpc.RpcMessage.yml","Discord.Rpc.RpcMessage.Author":"Discord.Rpc.RpcMessage.yml","Discord.Rpc.RpcMessage.Content":"Discord.Rpc.RpcMessage.yml","Discord.Rpc.RpcMessage.AuthorColor":"Discord.Rpc.RpcMessage.yml","Discord.Rpc.RpcMessage.CreatedAt":"Discord.Rpc.RpcMessage.yml","Discord.Rpc.RpcMessage.IsTTS":"Discord.Rpc.RpcMessage.yml","Discord.Rpc.RpcMessage.IsPinned":"Discord.Rpc.RpcMessage.yml","Discord.Rpc.RpcMessage.IsBlocked":"Discord.Rpc.RpcMessage.yml","Discord.Rpc.RpcMessage.EditedTimestamp":"Discord.Rpc.RpcMessage.yml","Discord.Rpc.RpcMessage.Attachments":"Discord.Rpc.RpcMessage.yml","Discord.Rpc.RpcMessage.Embeds":"Discord.Rpc.RpcMessage.yml","Discord.Rpc.RpcMessage.MentionedChannelIds":"Discord.Rpc.RpcMessage.yml","Discord.Rpc.RpcMessage.MentionedRoleIds":"Discord.Rpc.RpcMessage.yml","Discord.Rpc.RpcMessage.MentionedUserIds":"Discord.Rpc.RpcMessage.yml","Discord.Rpc.RpcMessage.Tags":"Discord.Rpc.RpcMessage.yml","Discord.Rpc.RpcMessage.WebhookId":"Discord.Rpc.RpcMessage.yml","Discord.Rpc.RpcMessage.IsWebhook":"Discord.Rpc.RpcMessage.yml","Discord.Rpc.RpcMessage.Timestamp":"Discord.Rpc.RpcMessage.yml","Discord.Rpc.RpcMessage.DeleteAsync(RequestOptions)":"Discord.Rpc.RpcMessage.yml","Discord.Rpc.RpcMessage.ToString":"Discord.Rpc.RpcMessage.yml","Discord.Rpc.RpcSystemMessage":"Discord.Rpc.RpcSystemMessage.yml","Discord.Rpc.RpcSystemMessage.Type":"Discord.Rpc.RpcSystemMessage.yml","Discord.Rpc.RpcUserMessage":"Discord.Rpc.RpcUserMessage.yml","Discord.Rpc.RpcUserMessage.IsTTS":"Discord.Rpc.RpcUserMessage.yml","Discord.Rpc.RpcUserMessage.IsPinned":"Discord.Rpc.RpcUserMessage.yml","Discord.Rpc.RpcUserMessage.IsBlocked":"Discord.Rpc.RpcUserMessage.yml","Discord.Rpc.RpcUserMessage.WebhookId":"Discord.Rpc.RpcUserMessage.yml","Discord.Rpc.RpcUserMessage.EditedTimestamp":"Discord.Rpc.RpcUserMessage.yml","Discord.Rpc.RpcUserMessage.Attachments":"Discord.Rpc.RpcUserMessage.yml","Discord.Rpc.RpcUserMessage.Embeds":"Discord.Rpc.RpcUserMessage.yml","Discord.Rpc.RpcUserMessage.MentionedChannelIds":"Discord.Rpc.RpcUserMessage.yml","Discord.Rpc.RpcUserMessage.MentionedRoleIds":"Discord.Rpc.RpcUserMessage.yml","Discord.Rpc.RpcUserMessage.MentionedUserIds":"Discord.Rpc.RpcUserMessage.yml","Discord.Rpc.RpcUserMessage.Tags":"Discord.Rpc.RpcUserMessage.yml","Discord.Rpc.RpcUserMessage.Reactions":"Discord.Rpc.RpcUserMessage.yml","Discord.Rpc.RpcUserMessage.ModifyAsync(Action{MessageProperties},RequestOptions)":"Discord.Rpc.RpcUserMessage.yml","Discord.Rpc.RpcUserMessage.AddReactionAsync(Emoji,RequestOptions)":"Discord.Rpc.RpcUserMessage.yml","Discord.Rpc.RpcUserMessage.AddReactionAsync(System.String,RequestOptions)":"Discord.Rpc.RpcUserMessage.yml","Discord.Rpc.RpcUserMessage.RemoveReactionAsync(Emoji,IUser,RequestOptions)":"Discord.Rpc.RpcUserMessage.yml","Discord.Rpc.RpcUserMessage.RemoveReactionAsync(System.String,IUser,RequestOptions)":"Discord.Rpc.RpcUserMessage.yml","Discord.Rpc.RpcUserMessage.RemoveAllReactionsAsync(RequestOptions)":"Discord.Rpc.RpcUserMessage.yml","Discord.Rpc.RpcUserMessage.GetReactionUsersAsync(System.String,System.Int32,System.Nullable{System.UInt64},RequestOptions)":"Discord.Rpc.RpcUserMessage.yml","Discord.Rpc.RpcUserMessage.PinAsync(RequestOptions)":"Discord.Rpc.RpcUserMessage.yml","Discord.Rpc.RpcUserMessage.UnpinAsync(RequestOptions)":"Discord.Rpc.RpcUserMessage.yml","Discord.Rpc.RpcUserMessage.Resolve(System.Int32,TagHandling,TagHandling,TagHandling,TagHandling,TagHandling)":"Discord.Rpc.RpcUserMessage.yml","Discord.Rpc.RpcUserMessage.Resolve(TagHandling,TagHandling,TagHandling,TagHandling,TagHandling)":"Discord.Rpc.RpcUserMessage.yml","Discord.Rpc.Pan":"Discord.Rpc.Pan.yml","Discord.Rpc.Pan.Left":"Discord.Rpc.Pan.yml","Discord.Rpc.Pan.Right":"Discord.Rpc.Pan.yml","Discord.Rpc.Pan.#ctor(System.Single,System.Single)":"Discord.Rpc.Pan.yml","Discord.Rpc.Pan.ToString":"Discord.Rpc.Pan.yml","Discord.Rpc.RpcGuildUser":"Discord.Rpc.RpcGuildUser.yml","Discord.Rpc.RpcGuildUser.Status":"Discord.Rpc.RpcGuildUser.yml","Discord.Rpc.RpcUser":"Discord.Rpc.RpcUser.yml","Discord.Rpc.RpcUser.IsBot":"Discord.Rpc.RpcUser.yml","Discord.Rpc.RpcUser.Username":"Discord.Rpc.RpcUser.yml","Discord.Rpc.RpcUser.DiscriminatorValue":"Discord.Rpc.RpcUser.yml","Discord.Rpc.RpcUser.AvatarId":"Discord.Rpc.RpcUser.yml","Discord.Rpc.RpcUser.AvatarUrl":"Discord.Rpc.RpcUser.yml","Discord.Rpc.RpcUser.CreatedAt":"Discord.Rpc.RpcUser.yml","Discord.Rpc.RpcUser.Discriminator":"Discord.Rpc.RpcUser.yml","Discord.Rpc.RpcUser.Mention":"Discord.Rpc.RpcUser.yml","Discord.Rpc.RpcUser.Game":"Discord.Rpc.RpcUser.yml","Discord.Rpc.RpcUser.Status":"Discord.Rpc.RpcUser.yml","Discord.Rpc.RpcUser.CreateDMChannelAsync(RequestOptions)":"Discord.Rpc.RpcUser.yml","Discord.Rpc.RpcUser.ToString":"Discord.Rpc.RpcUser.yml","Discord.Rpc.RpcVoiceState":"Discord.Rpc.RpcVoiceState.yml","Discord.Rpc.RpcVoiceState.User":"Discord.Rpc.RpcVoiceState.yml","Discord.Rpc.RpcVoiceState.Nickname":"Discord.Rpc.RpcVoiceState.yml","Discord.Rpc.RpcVoiceState.Volume":"Discord.Rpc.RpcVoiceState.yml","Discord.Rpc.RpcVoiceState.IsMuted2":"Discord.Rpc.RpcVoiceState.yml","Discord.Rpc.RpcVoiceState.Pan":"Discord.Rpc.RpcVoiceState.yml","Discord.Rpc.RpcVoiceState.IsMuted":"Discord.Rpc.RpcVoiceState.yml","Discord.Rpc.RpcVoiceState.IsDeafened":"Discord.Rpc.RpcVoiceState.yml","Discord.Rpc.RpcVoiceState.IsSuppressed":"Discord.Rpc.RpcVoiceState.yml","Discord.Rpc.RpcVoiceState.IsSelfMuted":"Discord.Rpc.RpcVoiceState.yml","Discord.Rpc.RpcVoiceState.IsSelfDeafened":"Discord.Rpc.RpcVoiceState.yml","Discord.Rpc.RpcVoiceState.ToString":"Discord.Rpc.RpcVoiceState.yml","Discord.Commands":"Discord.Commands.yml","Discord.Commands.RpcCommandContext":"Discord.Commands.RpcCommandContext.yml","Discord.Commands.RpcCommandContext.Client":"Discord.Commands.RpcCommandContext.yml","Discord.Commands.RpcCommandContext.Channel":"Discord.Commands.RpcCommandContext.yml","Discord.Commands.RpcCommandContext.User":"Discord.Commands.RpcCommandContext.yml","Discord.Commands.RpcCommandContext.Message":"Discord.Commands.RpcCommandContext.yml","Discord.Commands.RpcCommandContext.IsPrivate":"Discord.Commands.RpcCommandContext.yml","Discord.Commands.RpcCommandContext.#ctor(Discord.Rpc.DiscordRpcClient,Discord.Rpc.RpcUserMessage)":"Discord.Commands.RpcCommandContext.yml","Discord.Commands.ShardedCommandContext":"Discord.Commands.ShardedCommandContext.yml","Discord.Commands.ShardedCommandContext.Client":"Discord.Commands.ShardedCommandContext.yml","Discord.Commands.ShardedCommandContext.Guild":"Discord.Commands.ShardedCommandContext.yml","Discord.Commands.ShardedCommandContext.Channel":"Discord.Commands.ShardedCommandContext.yml","Discord.Commands.ShardedCommandContext.User":"Discord.Commands.ShardedCommandContext.yml","Discord.Commands.ShardedCommandContext.Message":"Discord.Commands.ShardedCommandContext.yml","Discord.Commands.ShardedCommandContext.IsPrivate":"Discord.Commands.ShardedCommandContext.yml","Discord.Commands.ShardedCommandContext.#ctor(Discord.WebSocket.DiscordShardedClient,Discord.WebSocket.SocketUserMessage)":"Discord.Commands.ShardedCommandContext.yml","Discord.Commands.SocketCommandContext":"Discord.Commands.SocketCommandContext.yml","Discord.Commands.SocketCommandContext.Client":"Discord.Commands.SocketCommandContext.yml","Discord.Commands.SocketCommandContext.Guild":"Discord.Commands.SocketCommandContext.yml","Discord.Commands.SocketCommandContext.Channel":"Discord.Commands.SocketCommandContext.yml","Discord.Commands.SocketCommandContext.User":"Discord.Commands.SocketCommandContext.yml","Discord.Commands.SocketCommandContext.Message":"Discord.Commands.SocketCommandContext.yml","Discord.Commands.SocketCommandContext.IsPrivate":"Discord.Commands.SocketCommandContext.yml","Discord.Commands.SocketCommandContext.#ctor(Discord.WebSocket.DiscordSocketClient,Discord.WebSocket.SocketUserMessage)":"Discord.Commands.SocketCommandContext.yml","Discord.Commands.CommandContext":"Discord.Commands.CommandContext.yml","Discord.Commands.CommandContext.Client":"Discord.Commands.CommandContext.yml","Discord.Commands.CommandContext.Guild":"Discord.Commands.CommandContext.yml","Discord.Commands.CommandContext.Channel":"Discord.Commands.CommandContext.yml","Discord.Commands.CommandContext.User":"Discord.Commands.CommandContext.yml","Discord.Commands.CommandContext.Message":"Discord.Commands.CommandContext.yml","Discord.Commands.CommandContext.IsPrivate":"Discord.Commands.CommandContext.yml","Discord.Commands.CommandContext.#ctor(IDiscordClient,IUserMessage)":"Discord.Commands.CommandContext.yml","Discord.Commands.CommandError":"Discord.Commands.CommandError.yml","Discord.Commands.CommandError.UnknownCommand":"Discord.Commands.CommandError.yml","Discord.Commands.CommandError.ParseFailed":"Discord.Commands.CommandError.yml","Discord.Commands.CommandError.BadArgCount":"Discord.Commands.CommandError.yml","Discord.Commands.CommandError.ObjectNotFound":"Discord.Commands.CommandError.yml","Discord.Commands.CommandError.MultipleMatches":"Discord.Commands.CommandError.yml","Discord.Commands.CommandError.UnmetPrecondition":"Discord.Commands.CommandError.yml","Discord.Commands.CommandError.Exception":"Discord.Commands.CommandError.yml","Discord.Commands.CommandMatch":"Discord.Commands.CommandMatch.yml","Discord.Commands.CommandMatch.Command":"Discord.Commands.CommandMatch.yml","Discord.Commands.CommandMatch.Alias":"Discord.Commands.CommandMatch.yml","Discord.Commands.CommandMatch.#ctor(Discord.Commands.CommandInfo,System.String)":"Discord.Commands.CommandMatch.yml","Discord.Commands.CommandMatch.CheckPreconditionsAsync(ICommandContext,Discord.Commands.IDependencyMap)":"Discord.Commands.CommandMatch.yml","Discord.Commands.CommandMatch.ParseAsync(ICommandContext,Discord.Commands.SearchResult,System.Nullable{Discord.Commands.PreconditionResult})":"Discord.Commands.CommandMatch.yml","Discord.Commands.CommandMatch.ExecuteAsync(ICommandContext,IEnumerable{System.Object},IEnumerable{System.Object},Discord.Commands.IDependencyMap)":"Discord.Commands.CommandMatch.yml","Discord.Commands.CommandMatch.ExecuteAsync(ICommandContext,Discord.Commands.ParseResult,Discord.Commands.IDependencyMap)":"Discord.Commands.CommandMatch.yml","Discord.Commands.CommandService":"Discord.Commands.CommandService.yml","Discord.Commands.CommandService.Modules":"Discord.Commands.CommandService.yml","Discord.Commands.CommandService.Commands":"Discord.Commands.CommandService.yml","Discord.Commands.CommandService.TypeReaders":"Discord.Commands.CommandService.yml","Discord.Commands.CommandService.#ctor":"Discord.Commands.CommandService.yml","Discord.Commands.CommandService.#ctor(Discord.Commands.CommandServiceConfig)":"Discord.Commands.CommandService.yml","Discord.Commands.CommandService.CreateModuleAsync(System.String,Action{Discord.Commands.Builders.ModuleBuilder})":"Discord.Commands.CommandService.yml","Discord.Commands.CommandService.AddModuleAsync``1":"Discord.Commands.CommandService.yml","Discord.Commands.CommandService.AddModulesAsync(Assembly)":"Discord.Commands.CommandService.yml","Discord.Commands.CommandService.RemoveModuleAsync(Discord.Commands.ModuleInfo)":"Discord.Commands.CommandService.yml","Discord.Commands.CommandService.RemoveModuleAsync``1":"Discord.Commands.CommandService.yml","Discord.Commands.CommandService.AddTypeReader``1(Discord.Commands.TypeReader)":"Discord.Commands.CommandService.yml","Discord.Commands.CommandService.AddTypeReader(Type,Discord.Commands.TypeReader)":"Discord.Commands.CommandService.yml","Discord.Commands.CommandService.Search(ICommandContext,System.Int32)":"Discord.Commands.CommandService.yml","Discord.Commands.CommandService.Search(ICommandContext,System.String)":"Discord.Commands.CommandService.yml","Discord.Commands.CommandService.ExecuteAsync(ICommandContext,System.Int32,Discord.Commands.IDependencyMap,Discord.Commands.MultiMatchHandling)":"Discord.Commands.CommandService.yml","Discord.Commands.CommandService.ExecuteAsync(ICommandContext,System.String,Discord.Commands.IDependencyMap,Discord.Commands.MultiMatchHandling)":"Discord.Commands.CommandService.yml","Discord.Commands.CommandServiceConfig":"Discord.Commands.CommandServiceConfig.yml","Discord.Commands.CommandServiceConfig.DefaultRunMode":"Discord.Commands.CommandServiceConfig.yml","Discord.Commands.CommandServiceConfig.SeparatorChar":"Discord.Commands.CommandServiceConfig.yml","Discord.Commands.CommandServiceConfig.CaseSensitiveCommands":"Discord.Commands.CommandServiceConfig.yml","Discord.Commands.ModuleBase":"Discord.Commands.ModuleBase.yml","Discord.Commands.ModuleBase`1":"Discord.Commands.ModuleBase-1.yml","Discord.Commands.ModuleBase`1.Context":"Discord.Commands.ModuleBase-1.yml","Discord.Commands.ModuleBase`1.ReplyAsync(System.String,System.Boolean,Embed,RequestOptions)":"Discord.Commands.ModuleBase-1.yml","Discord.Commands.MultiMatchHandling":"Discord.Commands.MultiMatchHandling.yml","Discord.Commands.MultiMatchHandling.Exception":"Discord.Commands.MultiMatchHandling.yml","Discord.Commands.MultiMatchHandling.Best":"Discord.Commands.MultiMatchHandling.yml","Discord.Commands.RunMode":"Discord.Commands.RunMode.yml","Discord.Commands.RunMode.Default":"Discord.Commands.RunMode.yml","Discord.Commands.RunMode.Sync":"Discord.Commands.RunMode.yml","Discord.Commands.RunMode.Mixed":"Discord.Commands.RunMode.yml","Discord.Commands.RunMode.Async":"Discord.Commands.RunMode.yml","Discord.Commands.AliasAttribute":"Discord.Commands.AliasAttribute.yml","Discord.Commands.AliasAttribute.Aliases":"Discord.Commands.AliasAttribute.yml","Discord.Commands.AliasAttribute.#ctor(System.String[])":"Discord.Commands.AliasAttribute.yml","Discord.Commands.CommandAttribute":"Discord.Commands.CommandAttribute.yml","Discord.Commands.CommandAttribute.Text":"Discord.Commands.CommandAttribute.yml","Discord.Commands.CommandAttribute.RunMode":"Discord.Commands.CommandAttribute.yml","Discord.Commands.CommandAttribute.#ctor":"Discord.Commands.CommandAttribute.yml","Discord.Commands.CommandAttribute.#ctor(System.String)":"Discord.Commands.CommandAttribute.yml","Discord.Commands.DontAutoLoadAttribute":"Discord.Commands.DontAutoLoadAttribute.yml","Discord.Commands.GroupAttribute":"Discord.Commands.GroupAttribute.yml","Discord.Commands.GroupAttribute.Prefix":"Discord.Commands.GroupAttribute.yml","Discord.Commands.GroupAttribute.#ctor":"Discord.Commands.GroupAttribute.yml","Discord.Commands.GroupAttribute.#ctor(System.String)":"Discord.Commands.GroupAttribute.yml","Discord.Commands.NameAttribute":"Discord.Commands.NameAttribute.yml","Discord.Commands.NameAttribute.Text":"Discord.Commands.NameAttribute.yml","Discord.Commands.NameAttribute.#ctor(System.String)":"Discord.Commands.NameAttribute.yml","Discord.Commands.OverrideTypeReaderAttribute":"Discord.Commands.OverrideTypeReaderAttribute.yml","Discord.Commands.OverrideTypeReaderAttribute.TypeReader":"Discord.Commands.OverrideTypeReaderAttribute.yml","Discord.Commands.OverrideTypeReaderAttribute.#ctor(Type)":"Discord.Commands.OverrideTypeReaderAttribute.yml","Discord.Commands.ParameterPreconditionAttribute":"Discord.Commands.ParameterPreconditionAttribute.yml","Discord.Commands.ParameterPreconditionAttribute.CheckPermissions(ICommandContext,Discord.Commands.ParameterInfo,System.Object,Discord.Commands.IDependencyMap)":"Discord.Commands.ParameterPreconditionAttribute.yml","Discord.Commands.PreconditionAttribute":"Discord.Commands.PreconditionAttribute.yml","Discord.Commands.PreconditionAttribute.CheckPermissions(ICommandContext,Discord.Commands.CommandInfo,Discord.Commands.IDependencyMap)":"Discord.Commands.PreconditionAttribute.yml","Discord.Commands.PriorityAttribute":"Discord.Commands.PriorityAttribute.yml","Discord.Commands.PriorityAttribute.Priority":"Discord.Commands.PriorityAttribute.yml","Discord.Commands.PriorityAttribute.#ctor(System.Int32)":"Discord.Commands.PriorityAttribute.yml","Discord.Commands.RemainderAttribute":"Discord.Commands.RemainderAttribute.yml","Discord.Commands.RemarksAttribute":"Discord.Commands.RemarksAttribute.yml","Discord.Commands.RemarksAttribute.Text":"Discord.Commands.RemarksAttribute.yml","Discord.Commands.RemarksAttribute.#ctor(System.String)":"Discord.Commands.RemarksAttribute.yml","Discord.Commands.SummaryAttribute":"Discord.Commands.SummaryAttribute.yml","Discord.Commands.SummaryAttribute.Text":"Discord.Commands.SummaryAttribute.yml","Discord.Commands.SummaryAttribute.#ctor(System.String)":"Discord.Commands.SummaryAttribute.yml","Discord.Commands.RequireBotPermissionAttribute":"Discord.Commands.RequireBotPermissionAttribute.yml","Discord.Commands.RequireBotPermissionAttribute.GuildPermission":"Discord.Commands.RequireBotPermissionAttribute.yml","Discord.Commands.RequireBotPermissionAttribute.ChannelPermission":"Discord.Commands.RequireBotPermissionAttribute.yml","Discord.Commands.RequireBotPermissionAttribute.#ctor(GuildPermission)":"Discord.Commands.RequireBotPermissionAttribute.yml","Discord.Commands.RequireBotPermissionAttribute.#ctor(ChannelPermission)":"Discord.Commands.RequireBotPermissionAttribute.yml","Discord.Commands.RequireBotPermissionAttribute.CheckPermissions(ICommandContext,Discord.Commands.CommandInfo,Discord.Commands.IDependencyMap)":"Discord.Commands.RequireBotPermissionAttribute.yml","Discord.Commands.ContextType":"Discord.Commands.ContextType.yml","Discord.Commands.ContextType.Guild":"Discord.Commands.ContextType.yml","Discord.Commands.ContextType.DM":"Discord.Commands.ContextType.yml","Discord.Commands.ContextType.Group":"Discord.Commands.ContextType.yml","Discord.Commands.RequireContextAttribute":"Discord.Commands.RequireContextAttribute.yml","Discord.Commands.RequireContextAttribute.Contexts":"Discord.Commands.RequireContextAttribute.yml","Discord.Commands.RequireContextAttribute.#ctor(Discord.Commands.ContextType)":"Discord.Commands.RequireContextAttribute.yml","Discord.Commands.RequireContextAttribute.CheckPermissions(ICommandContext,Discord.Commands.CommandInfo,Discord.Commands.IDependencyMap)":"Discord.Commands.RequireContextAttribute.yml","Discord.Commands.RequireOwnerAttribute":"Discord.Commands.RequireOwnerAttribute.yml","Discord.Commands.RequireOwnerAttribute.CheckPermissions(ICommandContext,Discord.Commands.CommandInfo,Discord.Commands.IDependencyMap)":"Discord.Commands.RequireOwnerAttribute.yml","Discord.Commands.RequireUserPermissionAttribute":"Discord.Commands.RequireUserPermissionAttribute.yml","Discord.Commands.RequireUserPermissionAttribute.GuildPermission":"Discord.Commands.RequireUserPermissionAttribute.yml","Discord.Commands.RequireUserPermissionAttribute.ChannelPermission":"Discord.Commands.RequireUserPermissionAttribute.yml","Discord.Commands.RequireUserPermissionAttribute.#ctor(GuildPermission)":"Discord.Commands.RequireUserPermissionAttribute.yml","Discord.Commands.RequireUserPermissionAttribute.#ctor(ChannelPermission)":"Discord.Commands.RequireUserPermissionAttribute.yml","Discord.Commands.RequireUserPermissionAttribute.CheckPermissions(ICommandContext,Discord.Commands.CommandInfo,Discord.Commands.IDependencyMap)":"Discord.Commands.RequireUserPermissionAttribute.yml","Discord.Commands.DependencyMap":"Discord.Commands.DependencyMap.yml","Discord.Commands.DependencyMap.Empty":"Discord.Commands.DependencyMap.yml","Discord.Commands.DependencyMap.#ctor":"Discord.Commands.DependencyMap.yml","Discord.Commands.DependencyMap.Add``1(``0)":"Discord.Commands.DependencyMap.yml","Discord.Commands.DependencyMap.Get``1":"Discord.Commands.DependencyMap.yml","Discord.Commands.DependencyMap.Get(Type)":"Discord.Commands.DependencyMap.yml","Discord.Commands.DependencyMap.TryGet``1(``0@)":"Discord.Commands.DependencyMap.yml","Discord.Commands.DependencyMap.TryGet(Type,System.Object@)":"Discord.Commands.DependencyMap.yml","Discord.Commands.IDependencyMap":"Discord.Commands.IDependencyMap.yml","Discord.Commands.IDependencyMap.Add``1(``0)":"Discord.Commands.IDependencyMap.yml","Discord.Commands.IDependencyMap.Get``1":"Discord.Commands.IDependencyMap.yml","Discord.Commands.IDependencyMap.TryGet``1(``0@)":"Discord.Commands.IDependencyMap.yml","Discord.Commands.IDependencyMap.Get(Type)":"Discord.Commands.IDependencyMap.yml","Discord.Commands.IDependencyMap.TryGet(Type,System.Object@)":"Discord.Commands.IDependencyMap.yml","Discord.Commands.IEnumerableExtensions":"Discord.Commands.IEnumerableExtensions.yml","Discord.Commands.IEnumerableExtensions.Permutate``3(IEnumerable{``0},IEnumerable{``1},Func{``0,``1,``2})":"Discord.Commands.IEnumerableExtensions.yml","Discord.Commands.MessageExtensions":"Discord.Commands.MessageExtensions.yml","Discord.Commands.MessageExtensions.HasCharPrefix(IUserMessage,System.Char,System.Int32@)":"Discord.Commands.MessageExtensions.yml","Discord.Commands.MessageExtensions.HasStringPrefix(IUserMessage,System.String,System.Int32@)":"Discord.Commands.MessageExtensions.yml","Discord.Commands.MessageExtensions.HasMentionPrefix(IUserMessage,IUser,System.Int32@)":"Discord.Commands.MessageExtensions.yml","Discord.Commands.CommandInfo":"Discord.Commands.CommandInfo.yml","Discord.Commands.CommandInfo.Module":"Discord.Commands.CommandInfo.yml","Discord.Commands.CommandInfo.Name":"Discord.Commands.CommandInfo.yml","Discord.Commands.CommandInfo.Summary":"Discord.Commands.CommandInfo.yml","Discord.Commands.CommandInfo.Remarks":"Discord.Commands.CommandInfo.yml","Discord.Commands.CommandInfo.Priority":"Discord.Commands.CommandInfo.yml","Discord.Commands.CommandInfo.HasVarArgs":"Discord.Commands.CommandInfo.yml","Discord.Commands.CommandInfo.RunMode":"Discord.Commands.CommandInfo.yml","Discord.Commands.CommandInfo.Aliases":"Discord.Commands.CommandInfo.yml","Discord.Commands.CommandInfo.Parameters":"Discord.Commands.CommandInfo.yml","Discord.Commands.CommandInfo.Preconditions":"Discord.Commands.CommandInfo.yml","Discord.Commands.CommandInfo.CheckPreconditionsAsync(ICommandContext,Discord.Commands.IDependencyMap)":"Discord.Commands.CommandInfo.yml","Discord.Commands.CommandInfo.ParseAsync(ICommandContext,System.Int32,Discord.Commands.SearchResult,System.Nullable{Discord.Commands.PreconditionResult})":"Discord.Commands.CommandInfo.yml","Discord.Commands.CommandInfo.ExecuteAsync(ICommandContext,Discord.Commands.ParseResult,Discord.Commands.IDependencyMap)":"Discord.Commands.CommandInfo.yml","Discord.Commands.CommandInfo.ExecuteAsync(ICommandContext,IEnumerable{System.Object},IEnumerable{System.Object},Discord.Commands.IDependencyMap)":"Discord.Commands.CommandInfo.yml","Discord.Commands.ModuleInfo":"Discord.Commands.ModuleInfo.yml","Discord.Commands.ModuleInfo.Service":"Discord.Commands.ModuleInfo.yml","Discord.Commands.ModuleInfo.Name":"Discord.Commands.ModuleInfo.yml","Discord.Commands.ModuleInfo.Summary":"Discord.Commands.ModuleInfo.yml","Discord.Commands.ModuleInfo.Remarks":"Discord.Commands.ModuleInfo.yml","Discord.Commands.ModuleInfo.Aliases":"Discord.Commands.ModuleInfo.yml","Discord.Commands.ModuleInfo.Commands":"Discord.Commands.ModuleInfo.yml","Discord.Commands.ModuleInfo.Preconditions":"Discord.Commands.ModuleInfo.yml","Discord.Commands.ModuleInfo.Submodules":"Discord.Commands.ModuleInfo.yml","Discord.Commands.ModuleInfo.Parent":"Discord.Commands.ModuleInfo.yml","Discord.Commands.ModuleInfo.IsSubmodule":"Discord.Commands.ModuleInfo.yml","Discord.Commands.ParameterInfo":"Discord.Commands.ParameterInfo.yml","Discord.Commands.ParameterInfo.Command":"Discord.Commands.ParameterInfo.yml","Discord.Commands.ParameterInfo.Name":"Discord.Commands.ParameterInfo.yml","Discord.Commands.ParameterInfo.Summary":"Discord.Commands.ParameterInfo.yml","Discord.Commands.ParameterInfo.IsOptional":"Discord.Commands.ParameterInfo.yml","Discord.Commands.ParameterInfo.IsRemainder":"Discord.Commands.ParameterInfo.yml","Discord.Commands.ParameterInfo.IsMultiple":"Discord.Commands.ParameterInfo.yml","Discord.Commands.ParameterInfo.Type":"Discord.Commands.ParameterInfo.yml","Discord.Commands.ParameterInfo.DefaultValue":"Discord.Commands.ParameterInfo.yml","Discord.Commands.ParameterInfo.Preconditions":"Discord.Commands.ParameterInfo.yml","Discord.Commands.ParameterInfo.CheckPreconditionsAsync(ICommandContext,System.Object[],Discord.Commands.IDependencyMap)":"Discord.Commands.ParameterInfo.yml","Discord.Commands.ParameterInfo.Parse(ICommandContext,System.String)":"Discord.Commands.ParameterInfo.yml","Discord.Commands.ParameterInfo.ToString":"Discord.Commands.ParameterInfo.yml","Discord.Commands.TypeReader":"Discord.Commands.TypeReader.yml","Discord.Commands.TypeReader.Read(ICommandContext,System.String)":"Discord.Commands.TypeReader.yml","Discord.Commands.ExecuteResult":"Discord.Commands.ExecuteResult.yml","Discord.Commands.ExecuteResult.Exception":"Discord.Commands.ExecuteResult.yml","Discord.Commands.ExecuteResult.Error":"Discord.Commands.ExecuteResult.yml","Discord.Commands.ExecuteResult.ErrorReason":"Discord.Commands.ExecuteResult.yml","Discord.Commands.ExecuteResult.IsSuccess":"Discord.Commands.ExecuteResult.yml","Discord.Commands.ExecuteResult.FromSuccess":"Discord.Commands.ExecuteResult.yml","Discord.Commands.ExecuteResult.FromError(Discord.Commands.CommandError,System.String)":"Discord.Commands.ExecuteResult.yml","Discord.Commands.ExecuteResult.FromError(Exception)":"Discord.Commands.ExecuteResult.yml","Discord.Commands.ExecuteResult.FromError(Discord.Commands.IResult)":"Discord.Commands.ExecuteResult.yml","Discord.Commands.ExecuteResult.ToString":"Discord.Commands.ExecuteResult.yml","Discord.Commands.IResult":"Discord.Commands.IResult.yml","Discord.Commands.IResult.Error":"Discord.Commands.IResult.yml","Discord.Commands.IResult.ErrorReason":"Discord.Commands.IResult.yml","Discord.Commands.IResult.IsSuccess":"Discord.Commands.IResult.yml","Discord.Commands.ParseResult":"Discord.Commands.ParseResult.yml","Discord.Commands.ParseResult.ArgValues":"Discord.Commands.ParseResult.yml","Discord.Commands.ParseResult.ParamValues":"Discord.Commands.ParseResult.yml","Discord.Commands.ParseResult.Error":"Discord.Commands.ParseResult.yml","Discord.Commands.ParseResult.ErrorReason":"Discord.Commands.ParseResult.yml","Discord.Commands.ParseResult.IsSuccess":"Discord.Commands.ParseResult.yml","Discord.Commands.ParseResult.FromSuccess(IReadOnlyList{Discord.Commands.TypeReaderResult},IReadOnlyList{Discord.Commands.TypeReaderResult})":"Discord.Commands.ParseResult.yml","Discord.Commands.ParseResult.FromSuccess(IReadOnlyList{Discord.Commands.TypeReaderValue},IReadOnlyList{Discord.Commands.TypeReaderValue})":"Discord.Commands.ParseResult.yml","Discord.Commands.ParseResult.FromError(Discord.Commands.CommandError,System.String)":"Discord.Commands.ParseResult.yml","Discord.Commands.ParseResult.FromError(Discord.Commands.IResult)":"Discord.Commands.ParseResult.yml","Discord.Commands.ParseResult.ToString":"Discord.Commands.ParseResult.yml","Discord.Commands.PreconditionResult":"Discord.Commands.PreconditionResult.yml","Discord.Commands.PreconditionResult.Error":"Discord.Commands.PreconditionResult.yml","Discord.Commands.PreconditionResult.ErrorReason":"Discord.Commands.PreconditionResult.yml","Discord.Commands.PreconditionResult.IsSuccess":"Discord.Commands.PreconditionResult.yml","Discord.Commands.PreconditionResult.FromSuccess":"Discord.Commands.PreconditionResult.yml","Discord.Commands.PreconditionResult.FromError(System.String)":"Discord.Commands.PreconditionResult.yml","Discord.Commands.PreconditionResult.FromError(Discord.Commands.IResult)":"Discord.Commands.PreconditionResult.yml","Discord.Commands.PreconditionResult.ToString":"Discord.Commands.PreconditionResult.yml","Discord.Commands.SearchResult":"Discord.Commands.SearchResult.yml","Discord.Commands.SearchResult.Text":"Discord.Commands.SearchResult.yml","Discord.Commands.SearchResult.Commands":"Discord.Commands.SearchResult.yml","Discord.Commands.SearchResult.Error":"Discord.Commands.SearchResult.yml","Discord.Commands.SearchResult.ErrorReason":"Discord.Commands.SearchResult.yml","Discord.Commands.SearchResult.IsSuccess":"Discord.Commands.SearchResult.yml","Discord.Commands.SearchResult.FromSuccess(System.String,IReadOnlyList{Discord.Commands.CommandMatch})":"Discord.Commands.SearchResult.yml","Discord.Commands.SearchResult.FromError(Discord.Commands.CommandError,System.String)":"Discord.Commands.SearchResult.yml","Discord.Commands.SearchResult.FromError(Discord.Commands.IResult)":"Discord.Commands.SearchResult.yml","Discord.Commands.SearchResult.ToString":"Discord.Commands.SearchResult.yml","Discord.Commands.TypeReaderValue":"Discord.Commands.TypeReaderValue.yml","Discord.Commands.TypeReaderValue.Value":"Discord.Commands.TypeReaderValue.yml","Discord.Commands.TypeReaderValue.Score":"Discord.Commands.TypeReaderValue.yml","Discord.Commands.TypeReaderValue.#ctor(System.Object,System.Single)":"Discord.Commands.TypeReaderValue.yml","Discord.Commands.TypeReaderValue.ToString":"Discord.Commands.TypeReaderValue.yml","Discord.Commands.TypeReaderResult":"Discord.Commands.TypeReaderResult.yml","Discord.Commands.TypeReaderResult.Values":"Discord.Commands.TypeReaderResult.yml","Discord.Commands.TypeReaderResult.Error":"Discord.Commands.TypeReaderResult.yml","Discord.Commands.TypeReaderResult.ErrorReason":"Discord.Commands.TypeReaderResult.yml","Discord.Commands.TypeReaderResult.IsSuccess":"Discord.Commands.TypeReaderResult.yml","Discord.Commands.TypeReaderResult.FromSuccess(System.Object)":"Discord.Commands.TypeReaderResult.yml","Discord.Commands.TypeReaderResult.FromSuccess(Discord.Commands.TypeReaderValue)":"Discord.Commands.TypeReaderResult.yml","Discord.Commands.TypeReaderResult.FromSuccess(IReadOnlyCollection{Discord.Commands.TypeReaderValue})":"Discord.Commands.TypeReaderResult.yml","Discord.Commands.TypeReaderResult.FromError(Discord.Commands.CommandError,System.String)":"Discord.Commands.TypeReaderResult.yml","Discord.Commands.TypeReaderResult.FromError(Discord.Commands.IResult)":"Discord.Commands.TypeReaderResult.yml","Discord.Commands.TypeReaderResult.ToString":"Discord.Commands.TypeReaderResult.yml","Discord.Commands.ICommandContext":"Discord.Commands.ICommandContext.yml","Discord.Commands.ICommandContext.Client":"Discord.Commands.ICommandContext.yml","Discord.Commands.ICommandContext.Guild":"Discord.Commands.ICommandContext.yml","Discord.Commands.ICommandContext.Channel":"Discord.Commands.ICommandContext.yml","Discord.Commands.ICommandContext.User":"Discord.Commands.ICommandContext.yml","Discord.Commands.ICommandContext.Message":"Discord.Commands.ICommandContext.yml","Discord.WebSocket":"Discord.WebSocket.yml","Discord.WebSocket.DiscordShardedClient":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.Latency":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.CurrentUser":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.Guilds":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.PrivateChannels":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.Shards":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.VoiceRegions":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.#ctor":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.#ctor(Discord.WebSocket.DiscordSocketConfig)":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.#ctor(System.Int32[])":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.#ctor(System.Int32[],Discord.WebSocket.DiscordSocketConfig)":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.OnLoginAsync(TokenType,System.String)":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.OnLogoutAsync":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.ConnectAsync(System.Boolean)":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.DisconnectAsync":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.GetShard(System.Int32)":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.GetApplicationInfoAsync":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.GetGuild(System.UInt64)":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.CreateGuildAsync(System.String,IVoiceRegion,Stream)":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.GetChannel(System.UInt64)":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.GetConnectionsAsync":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.GetInviteAsync(System.String)":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.GetUser(System.UInt64)":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.GetUser(System.String,System.String)":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.GetVoiceRegion(System.String)":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.DownloadAllUsersAsync":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.DownloadUsersAsync(IEnumerable{Discord.WebSocket.SocketGuild})":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.SetStatusAsync(UserStatus)":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.SetGameAsync(System.String,System.String,StreamType)":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.ChannelCreated":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.ChannelDestroyed":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.ChannelUpdated":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.MessageReceived":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.MessageDeleted":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.MessageUpdated":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.ReactionAdded":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.ReactionRemoved":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.ReactionsCleared":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.RoleCreated":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.RoleDeleted":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.RoleUpdated":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.JoinedGuild":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.LeftGuild":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.GuildAvailable":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.GuildUnavailable":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.GuildMembersDownloaded":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.GuildUpdated":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.UserJoined":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.UserLeft":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.UserBanned":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.UserUnbanned":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.UserUpdated":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.GuildMemberUpdated":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.UserPresenceUpdated":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.UserVoiceStateUpdated":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.CurrentUserUpdated":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.UserIsTyping":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.RecipientAdded":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordShardedClient.RecipientRemoved":"Discord.WebSocket.DiscordShardedClient.yml","Discord.WebSocket.DiscordSocketClient":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.ShardId":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.ConnectionState":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.Latency":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.CurrentUser":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.Guilds":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.PrivateChannels":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.VoiceRegions":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.#ctor":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.#ctor(Discord.WebSocket.DiscordSocketConfig)":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.OnLoginAsync(TokenType,System.String)":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.OnLogoutAsync":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.ConnectAsync(System.Boolean)":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.DisconnectAsync":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.GetApplicationInfoAsync":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.GetGuild(System.UInt64)":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.CreateGuildAsync(System.String,IVoiceRegion,Stream)":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.GetChannel(System.UInt64)":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.GetConnectionsAsync":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.GetInviteAsync(System.String)":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.GetUser(System.UInt64)":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.GetUser(System.String,System.String)":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.GetVoiceRegion(System.String)":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.DownloadAllUsersAsync":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.DownloadUsersAsync(IEnumerable{Discord.WebSocket.SocketGuild})":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.SetStatusAsync(UserStatus)":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.SetGameAsync(System.String,System.String,StreamType)":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.Connected":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.Disconnected":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.Ready":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.LatencyUpdated":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.ChannelCreated":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.ChannelDestroyed":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.ChannelUpdated":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.MessageReceived":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.MessageDeleted":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.MessageUpdated":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.ReactionAdded":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.ReactionRemoved":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.ReactionsCleared":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.RoleCreated":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.RoleDeleted":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.RoleUpdated":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.JoinedGuild":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.LeftGuild":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.GuildAvailable":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.GuildUnavailable":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.GuildMembersDownloaded":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.GuildUpdated":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.UserJoined":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.UserLeft":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.UserBanned":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.UserUnbanned":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.UserUpdated":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.GuildMemberUpdated":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.UserPresenceUpdated":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.UserVoiceStateUpdated":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.CurrentUserUpdated":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.UserIsTyping":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.RecipientAdded":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketClient.RecipientRemoved":"Discord.WebSocket.DiscordSocketClient.yml","Discord.WebSocket.DiscordSocketConfig":"Discord.WebSocket.DiscordSocketConfig.yml","Discord.WebSocket.DiscordSocketConfig.GatewayEncoding":"Discord.WebSocket.DiscordSocketConfig.yml","Discord.WebSocket.DiscordSocketConfig.ConnectionTimeout":"Discord.WebSocket.DiscordSocketConfig.yml","Discord.WebSocket.DiscordSocketConfig.ShardId":"Discord.WebSocket.DiscordSocketConfig.yml","Discord.WebSocket.DiscordSocketConfig.TotalShards":"Discord.WebSocket.DiscordSocketConfig.yml","Discord.WebSocket.DiscordSocketConfig.MessageCacheSize":"Discord.WebSocket.DiscordSocketConfig.yml","Discord.WebSocket.DiscordSocketConfig.LargeThreshold":"Discord.WebSocket.DiscordSocketConfig.yml","Discord.WebSocket.DiscordSocketConfig.AudioMode":"Discord.WebSocket.DiscordSocketConfig.yml","Discord.WebSocket.DiscordSocketConfig.WebSocketProvider":"Discord.WebSocket.DiscordSocketConfig.yml","Discord.WebSocket.DiscordSocketConfig.UdpSocketProvider":"Discord.WebSocket.DiscordSocketConfig.yml","Discord.WebSocket.DiscordSocketConfig.DownloadUsersOnGuildAvailable":"Discord.WebSocket.DiscordSocketConfig.yml","Discord.WebSocket.DiscordSocketConfig.#ctor":"Discord.WebSocket.DiscordSocketConfig.yml","Discord.WebSocket.SocketEntity`1":"Discord.WebSocket.SocketEntity-1.yml","Discord.WebSocket.SocketEntity`1.Discord":"Discord.WebSocket.SocketEntity-1.yml","Discord.WebSocket.SocketEntity`1.Id":"Discord.WebSocket.SocketEntity-1.yml","Discord.WebSocket.ISocketAudioChannel":"Discord.WebSocket.ISocketAudioChannel.yml","Discord.WebSocket.ISocketAudioChannel.ConnectAsync":"Discord.WebSocket.ISocketAudioChannel.yml","Discord.WebSocket.ISocketMessageChannel":"Discord.WebSocket.ISocketMessageChannel.yml","Discord.WebSocket.ISocketMessageChannel.CachedMessages":"Discord.WebSocket.ISocketMessageChannel.yml","Discord.WebSocket.ISocketMessageChannel.SendMessageAsync(System.String,System.Boolean,Embed,RequestOptions)":"Discord.WebSocket.ISocketMessageChannel.yml","Discord.WebSocket.ISocketMessageChannel.SendFileAsync(Stream,System.String,System.String,System.Boolean,RequestOptions)":"Discord.WebSocket.ISocketMessageChannel.yml","Discord.WebSocket.ISocketMessageChannel.GetCachedMessage(System.UInt64)":"Discord.WebSocket.ISocketMessageChannel.yml","Discord.WebSocket.ISocketMessageChannel.GetCachedMessages(System.Int32)":"Discord.WebSocket.ISocketMessageChannel.yml","Discord.WebSocket.ISocketMessageChannel.GetCachedMessages(System.UInt64,Direction,System.Int32)":"Discord.WebSocket.ISocketMessageChannel.yml","Discord.WebSocket.ISocketMessageChannel.GetCachedMessages(IMessage,Direction,System.Int32)":"Discord.WebSocket.ISocketMessageChannel.yml","Discord.WebSocket.ISocketMessageChannel.GetPinnedMessagesAsync(RequestOptions)":"Discord.WebSocket.ISocketMessageChannel.yml","Discord.WebSocket.ISocketPrivateChannel":"Discord.WebSocket.ISocketPrivateChannel.yml","Discord.WebSocket.ISocketPrivateChannel.Recipients":"Discord.WebSocket.ISocketPrivateChannel.yml","Discord.WebSocket.SocketChannel":"Discord.WebSocket.SocketChannel.yml","Discord.WebSocket.SocketChannel.CreatedAt":"Discord.WebSocket.SocketChannel.yml","Discord.WebSocket.SocketChannel.Users":"Discord.WebSocket.SocketChannel.yml","Discord.WebSocket.SocketChannel.GetUser(System.UInt64)":"Discord.WebSocket.SocketChannel.yml","Discord.WebSocket.SocketDMChannel":"Discord.WebSocket.SocketDMChannel.yml","Discord.WebSocket.SocketDMChannel.Recipient":"Discord.WebSocket.SocketDMChannel.yml","Discord.WebSocket.SocketDMChannel.CachedMessages":"Discord.WebSocket.SocketDMChannel.yml","Discord.WebSocket.SocketDMChannel.Users":"Discord.WebSocket.SocketDMChannel.yml","Discord.WebSocket.SocketDMChannel.CloseAsync(RequestOptions)":"Discord.WebSocket.SocketDMChannel.yml","Discord.WebSocket.SocketDMChannel.GetCachedMessage(System.UInt64)":"Discord.WebSocket.SocketDMChannel.yml","Discord.WebSocket.SocketDMChannel.GetMessageAsync(System.UInt64,RequestOptions)":"Discord.WebSocket.SocketDMChannel.yml","Discord.WebSocket.SocketDMChannel.GetMessagesAsync(System.Int32,RequestOptions)":"Discord.WebSocket.SocketDMChannel.yml","Discord.WebSocket.SocketDMChannel.GetMessagesAsync(System.UInt64,Direction,System.Int32,RequestOptions)":"Discord.WebSocket.SocketDMChannel.yml","Discord.WebSocket.SocketDMChannel.GetMessagesAsync(IMessage,Direction,System.Int32,RequestOptions)":"Discord.WebSocket.SocketDMChannel.yml","Discord.WebSocket.SocketDMChannel.GetCachedMessages(System.Int32)":"Discord.WebSocket.SocketDMChannel.yml","Discord.WebSocket.SocketDMChannel.GetCachedMessages(System.UInt64,Direction,System.Int32)":"Discord.WebSocket.SocketDMChannel.yml","Discord.WebSocket.SocketDMChannel.GetCachedMessages(IMessage,Direction,System.Int32)":"Discord.WebSocket.SocketDMChannel.yml","Discord.WebSocket.SocketDMChannel.GetPinnedMessagesAsync(RequestOptions)":"Discord.WebSocket.SocketDMChannel.yml","Discord.WebSocket.SocketDMChannel.SendMessageAsync(System.String,System.Boolean,Embed,RequestOptions)":"Discord.WebSocket.SocketDMChannel.yml","Discord.WebSocket.SocketDMChannel.SendFileAsync(Stream,System.String,System.String,System.Boolean,RequestOptions)":"Discord.WebSocket.SocketDMChannel.yml","Discord.WebSocket.SocketDMChannel.DeleteMessagesAsync(IEnumerable{IMessage},RequestOptions)":"Discord.WebSocket.SocketDMChannel.yml","Discord.WebSocket.SocketDMChannel.TriggerTypingAsync(RequestOptions)":"Discord.WebSocket.SocketDMChannel.yml","Discord.WebSocket.SocketDMChannel.EnterTypingState(RequestOptions)":"Discord.WebSocket.SocketDMChannel.yml","Discord.WebSocket.SocketDMChannel.GetUser(System.UInt64)":"Discord.WebSocket.SocketDMChannel.yml","Discord.WebSocket.SocketDMChannel.ToString":"Discord.WebSocket.SocketDMChannel.yml","Discord.WebSocket.SocketDMChannel.Discord#WebSocket#ISocketPrivateChannel#Recipients":"Discord.WebSocket.SocketDMChannel.yml","Discord.WebSocket.SocketGroupChannel":"Discord.WebSocket.SocketGroupChannel.yml","Discord.WebSocket.SocketGroupChannel.Name":"Discord.WebSocket.SocketGroupChannel.yml","Discord.WebSocket.SocketGroupChannel.CachedMessages":"Discord.WebSocket.SocketGroupChannel.yml","Discord.WebSocket.SocketGroupChannel.Users":"Discord.WebSocket.SocketGroupChannel.yml","Discord.WebSocket.SocketGroupChannel.Recipients":"Discord.WebSocket.SocketGroupChannel.yml","Discord.WebSocket.SocketGroupChannel.LeaveAsync(RequestOptions)":"Discord.WebSocket.SocketGroupChannel.yml","Discord.WebSocket.SocketGroupChannel.ConnectAsync":"Discord.WebSocket.SocketGroupChannel.yml","Discord.WebSocket.SocketGroupChannel.GetCachedMessage(System.UInt64)":"Discord.WebSocket.SocketGroupChannel.yml","Discord.WebSocket.SocketGroupChannel.GetMessageAsync(System.UInt64,RequestOptions)":"Discord.WebSocket.SocketGroupChannel.yml","Discord.WebSocket.SocketGroupChannel.GetMessagesAsync(System.Int32,RequestOptions)":"Discord.WebSocket.SocketGroupChannel.yml","Discord.WebSocket.SocketGroupChannel.GetMessagesAsync(System.UInt64,Direction,System.Int32,RequestOptions)":"Discord.WebSocket.SocketGroupChannel.yml","Discord.WebSocket.SocketGroupChannel.GetMessagesAsync(IMessage,Direction,System.Int32,RequestOptions)":"Discord.WebSocket.SocketGroupChannel.yml","Discord.WebSocket.SocketGroupChannel.GetCachedMessages(System.Int32)":"Discord.WebSocket.SocketGroupChannel.yml","Discord.WebSocket.SocketGroupChannel.GetCachedMessages(System.UInt64,Direction,System.Int32)":"Discord.WebSocket.SocketGroupChannel.yml","Discord.WebSocket.SocketGroupChannel.GetCachedMessages(IMessage,Direction,System.Int32)":"Discord.WebSocket.SocketGroupChannel.yml","Discord.WebSocket.SocketGroupChannel.GetPinnedMessagesAsync(RequestOptions)":"Discord.WebSocket.SocketGroupChannel.yml","Discord.WebSocket.SocketGroupChannel.SendMessageAsync(System.String,System.Boolean,Embed,RequestOptions)":"Discord.WebSocket.SocketGroupChannel.yml","Discord.WebSocket.SocketGroupChannel.SendFileAsync(Stream,System.String,System.String,System.Boolean,RequestOptions)":"Discord.WebSocket.SocketGroupChannel.yml","Discord.WebSocket.SocketGroupChannel.DeleteMessagesAsync(IEnumerable{IMessage},RequestOptions)":"Discord.WebSocket.SocketGroupChannel.yml","Discord.WebSocket.SocketGroupChannel.TriggerTypingAsync(RequestOptions)":"Discord.WebSocket.SocketGroupChannel.yml","Discord.WebSocket.SocketGroupChannel.EnterTypingState(RequestOptions)":"Discord.WebSocket.SocketGroupChannel.yml","Discord.WebSocket.SocketGroupChannel.GetUser(System.UInt64)":"Discord.WebSocket.SocketGroupChannel.yml","Discord.WebSocket.SocketGroupChannel.ToString":"Discord.WebSocket.SocketGroupChannel.yml","Discord.WebSocket.SocketGroupChannel.Discord#WebSocket#ISocketPrivateChannel#Recipients":"Discord.WebSocket.SocketGroupChannel.yml","Discord.WebSocket.SocketGuildChannel":"Discord.WebSocket.SocketGuildChannel.yml","Discord.WebSocket.SocketGuildChannel.Guild":"Discord.WebSocket.SocketGuildChannel.yml","Discord.WebSocket.SocketGuildChannel.Name":"Discord.WebSocket.SocketGuildChannel.yml","Discord.WebSocket.SocketGuildChannel.Position":"Discord.WebSocket.SocketGuildChannel.yml","Discord.WebSocket.SocketGuildChannel.PermissionOverwrites":"Discord.WebSocket.SocketGuildChannel.yml","Discord.WebSocket.SocketGuildChannel.Users":"Discord.WebSocket.SocketGuildChannel.yml","Discord.WebSocket.SocketGuildChannel.ModifyAsync(Action{GuildChannelProperties},RequestOptions)":"Discord.WebSocket.SocketGuildChannel.yml","Discord.WebSocket.SocketGuildChannel.DeleteAsync(RequestOptions)":"Discord.WebSocket.SocketGuildChannel.yml","Discord.WebSocket.SocketGuildChannel.GetPermissionOverwrite(IUser)":"Discord.WebSocket.SocketGuildChannel.yml","Discord.WebSocket.SocketGuildChannel.GetPermissionOverwrite(IRole)":"Discord.WebSocket.SocketGuildChannel.yml","Discord.WebSocket.SocketGuildChannel.AddPermissionOverwriteAsync(IUser,OverwritePermissions,RequestOptions)":"Discord.WebSocket.SocketGuildChannel.yml","Discord.WebSocket.SocketGuildChannel.AddPermissionOverwriteAsync(IRole,OverwritePermissions,RequestOptions)":"Discord.WebSocket.SocketGuildChannel.yml","Discord.WebSocket.SocketGuildChannel.RemovePermissionOverwriteAsync(IUser,RequestOptions)":"Discord.WebSocket.SocketGuildChannel.yml","Discord.WebSocket.SocketGuildChannel.RemovePermissionOverwriteAsync(IRole,RequestOptions)":"Discord.WebSocket.SocketGuildChannel.yml","Discord.WebSocket.SocketGuildChannel.GetInvitesAsync(RequestOptions)":"Discord.WebSocket.SocketGuildChannel.yml","Discord.WebSocket.SocketGuildChannel.CreateInviteAsync(System.Nullable{System.Int32},System.Nullable{System.Int32},System.Boolean,RequestOptions)":"Discord.WebSocket.SocketGuildChannel.yml","Discord.WebSocket.SocketGuildChannel.GetUser(System.UInt64)":"Discord.WebSocket.SocketGuildChannel.yml","Discord.WebSocket.SocketGuildChannel.ToString":"Discord.WebSocket.SocketGuildChannel.yml","Discord.WebSocket.SocketTextChannel":"Discord.WebSocket.SocketTextChannel.yml","Discord.WebSocket.SocketTextChannel.Topic":"Discord.WebSocket.SocketTextChannel.yml","Discord.WebSocket.SocketTextChannel.Mention":"Discord.WebSocket.SocketTextChannel.yml","Discord.WebSocket.SocketTextChannel.CachedMessages":"Discord.WebSocket.SocketTextChannel.yml","Discord.WebSocket.SocketTextChannel.Users":"Discord.WebSocket.SocketTextChannel.yml","Discord.WebSocket.SocketTextChannel.ModifyAsync(Action{TextChannelProperties},RequestOptions)":"Discord.WebSocket.SocketTextChannel.yml","Discord.WebSocket.SocketTextChannel.GetCachedMessage(System.UInt64)":"Discord.WebSocket.SocketTextChannel.yml","Discord.WebSocket.SocketTextChannel.GetMessageAsync(System.UInt64,RequestOptions)":"Discord.WebSocket.SocketTextChannel.yml","Discord.WebSocket.SocketTextChannel.GetMessagesAsync(System.Int32,RequestOptions)":"Discord.WebSocket.SocketTextChannel.yml","Discord.WebSocket.SocketTextChannel.GetMessagesAsync(System.UInt64,Direction,System.Int32,RequestOptions)":"Discord.WebSocket.SocketTextChannel.yml","Discord.WebSocket.SocketTextChannel.GetMessagesAsync(IMessage,Direction,System.Int32,RequestOptions)":"Discord.WebSocket.SocketTextChannel.yml","Discord.WebSocket.SocketTextChannel.GetCachedMessages(System.Int32)":"Discord.WebSocket.SocketTextChannel.yml","Discord.WebSocket.SocketTextChannel.GetCachedMessages(System.UInt64,Direction,System.Int32)":"Discord.WebSocket.SocketTextChannel.yml","Discord.WebSocket.SocketTextChannel.GetCachedMessages(IMessage,Direction,System.Int32)":"Discord.WebSocket.SocketTextChannel.yml","Discord.WebSocket.SocketTextChannel.GetPinnedMessagesAsync(RequestOptions)":"Discord.WebSocket.SocketTextChannel.yml","Discord.WebSocket.SocketTextChannel.SendMessageAsync(System.String,System.Boolean,Embed,RequestOptions)":"Discord.WebSocket.SocketTextChannel.yml","Discord.WebSocket.SocketTextChannel.SendFileAsync(Stream,System.String,System.String,System.Boolean,RequestOptions)":"Discord.WebSocket.SocketTextChannel.yml","Discord.WebSocket.SocketTextChannel.DeleteMessagesAsync(IEnumerable{IMessage},RequestOptions)":"Discord.WebSocket.SocketTextChannel.yml","Discord.WebSocket.SocketTextChannel.TriggerTypingAsync(RequestOptions)":"Discord.WebSocket.SocketTextChannel.yml","Discord.WebSocket.SocketTextChannel.EnterTypingState(RequestOptions)":"Discord.WebSocket.SocketTextChannel.yml","Discord.WebSocket.SocketTextChannel.GetUser(System.UInt64)":"Discord.WebSocket.SocketTextChannel.yml","Discord.WebSocket.SocketVoiceChannel":"Discord.WebSocket.SocketVoiceChannel.yml","Discord.WebSocket.SocketVoiceChannel.Bitrate":"Discord.WebSocket.SocketVoiceChannel.yml","Discord.WebSocket.SocketVoiceChannel.UserLimit":"Discord.WebSocket.SocketVoiceChannel.yml","Discord.WebSocket.SocketVoiceChannel.Users":"Discord.WebSocket.SocketVoiceChannel.yml","Discord.WebSocket.SocketVoiceChannel.ModifyAsync(Action{VoiceChannelProperties},RequestOptions)":"Discord.WebSocket.SocketVoiceChannel.yml","Discord.WebSocket.SocketVoiceChannel.ConnectAsync":"Discord.WebSocket.SocketVoiceChannel.yml","Discord.WebSocket.SocketVoiceChannel.GetUser(System.UInt64)":"Discord.WebSocket.SocketVoiceChannel.yml","Discord.WebSocket.SocketGuild":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.Name":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.AFKTimeout":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.IsEmbeddable":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.VerificationLevel":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.MfaLevel":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.DefaultMessageNotifications":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.MemberCount":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.DownloadedMemberCount":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.AFKChannelId":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.EmbedChannelId":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.OwnerId":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.VoiceRegionId":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.IconId":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.SplashId":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.CreatedAt":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.DefaultChannelId":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.IconUrl":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.SplashUrl":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.HasAllMembers":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.IsSynced":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.SyncPromise":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.DownloaderPromise":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.AudioClient":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.CurrentUser":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.EveryoneRole":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.Channels":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.Emojis":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.Features":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.Users":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.Roles":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.DeleteAsync(RequestOptions)":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.ModifyAsync(Action{GuildProperties},RequestOptions)":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.ModifyEmbedAsync(Action{GuildEmbedProperties},RequestOptions)":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.ModifyChannelsAsync(IEnumerable{BulkGuildChannelProperties},RequestOptions)":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.ModifyRolesAsync(IEnumerable{BulkRoleProperties},RequestOptions)":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.LeaveAsync(RequestOptions)":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.GetBansAsync(RequestOptions)":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.AddBanAsync(IUser,System.Int32,RequestOptions)":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.AddBanAsync(System.UInt64,System.Int32,RequestOptions)":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.RemoveBanAsync(IUser,RequestOptions)":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.RemoveBanAsync(System.UInt64,RequestOptions)":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.GetChannel(System.UInt64)":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.CreateTextChannelAsync(System.String,RequestOptions)":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.CreateVoiceChannelAsync(System.String,RequestOptions)":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.GetIntegrationsAsync(RequestOptions)":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.CreateIntegrationAsync(System.UInt64,System.String,RequestOptions)":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.GetInvitesAsync(RequestOptions)":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.GetRole(System.UInt64)":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.CreateRoleAsync(System.String,System.Nullable{GuildPermissions},System.Nullable{Color},System.Boolean,RequestOptions)":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.GetUser(System.UInt64)":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.PruneUsersAsync(System.Int32,System.Boolean,RequestOptions)":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.DownloadUsersAsync":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketGuild.ToString":"Discord.WebSocket.SocketGuild.yml","Discord.WebSocket.SocketMessage":"Discord.WebSocket.SocketMessage.yml","Discord.WebSocket.SocketMessage.Author":"Discord.WebSocket.SocketMessage.yml","Discord.WebSocket.SocketMessage.Channel":"Discord.WebSocket.SocketMessage.yml","Discord.WebSocket.SocketMessage.Content":"Discord.WebSocket.SocketMessage.yml","Discord.WebSocket.SocketMessage.CreatedAt":"Discord.WebSocket.SocketMessage.yml","Discord.WebSocket.SocketMessage.IsTTS":"Discord.WebSocket.SocketMessage.yml","Discord.WebSocket.SocketMessage.IsPinned":"Discord.WebSocket.SocketMessage.yml","Discord.WebSocket.SocketMessage.EditedTimestamp":"Discord.WebSocket.SocketMessage.yml","Discord.WebSocket.SocketMessage.Attachments":"Discord.WebSocket.SocketMessage.yml","Discord.WebSocket.SocketMessage.Embeds":"Discord.WebSocket.SocketMessage.yml","Discord.WebSocket.SocketMessage.MentionedChannels":"Discord.WebSocket.SocketMessage.yml","Discord.WebSocket.SocketMessage.MentionedRoles":"Discord.WebSocket.SocketMessage.yml","Discord.WebSocket.SocketMessage.MentionedUsers":"Discord.WebSocket.SocketMessage.yml","Discord.WebSocket.SocketMessage.Tags":"Discord.WebSocket.SocketMessage.yml","Discord.WebSocket.SocketMessage.WebhookId":"Discord.WebSocket.SocketMessage.yml","Discord.WebSocket.SocketMessage.IsWebhook":"Discord.WebSocket.SocketMessage.yml","Discord.WebSocket.SocketMessage.Timestamp":"Discord.WebSocket.SocketMessage.yml","Discord.WebSocket.SocketMessage.DeleteAsync(RequestOptions)":"Discord.WebSocket.SocketMessage.yml","Discord.WebSocket.SocketMessage.ToString":"Discord.WebSocket.SocketMessage.yml","Discord.WebSocket.SocketReaction":"Discord.WebSocket.SocketReaction.yml","Discord.WebSocket.SocketReaction.UserId":"Discord.WebSocket.SocketReaction.yml","Discord.WebSocket.SocketReaction.User":"Discord.WebSocket.SocketReaction.yml","Discord.WebSocket.SocketReaction.MessageId":"Discord.WebSocket.SocketReaction.yml","Discord.WebSocket.SocketReaction.Message":"Discord.WebSocket.SocketReaction.yml","Discord.WebSocket.SocketReaction.Channel":"Discord.WebSocket.SocketReaction.yml","Discord.WebSocket.SocketReaction.Emoji":"Discord.WebSocket.SocketReaction.yml","Discord.WebSocket.SocketUserMessage":"Discord.WebSocket.SocketUserMessage.yml","Discord.WebSocket.SocketUserMessage.IsTTS":"Discord.WebSocket.SocketUserMessage.yml","Discord.WebSocket.SocketUserMessage.IsPinned":"Discord.WebSocket.SocketUserMessage.yml","Discord.WebSocket.SocketUserMessage.WebhookId":"Discord.WebSocket.SocketUserMessage.yml","Discord.WebSocket.SocketUserMessage.EditedTimestamp":"Discord.WebSocket.SocketUserMessage.yml","Discord.WebSocket.SocketUserMessage.Attachments":"Discord.WebSocket.SocketUserMessage.yml","Discord.WebSocket.SocketUserMessage.Embeds":"Discord.WebSocket.SocketUserMessage.yml","Discord.WebSocket.SocketUserMessage.Tags":"Discord.WebSocket.SocketUserMessage.yml","Discord.WebSocket.SocketUserMessage.MentionedChannels":"Discord.WebSocket.SocketUserMessage.yml","Discord.WebSocket.SocketUserMessage.MentionedRoles":"Discord.WebSocket.SocketUserMessage.yml","Discord.WebSocket.SocketUserMessage.MentionedUsers":"Discord.WebSocket.SocketUserMessage.yml","Discord.WebSocket.SocketUserMessage.Reactions":"Discord.WebSocket.SocketUserMessage.yml","Discord.WebSocket.SocketUserMessage.ModifyAsync(Action{MessageProperties},RequestOptions)":"Discord.WebSocket.SocketUserMessage.yml","Discord.WebSocket.SocketUserMessage.AddReactionAsync(Emoji,RequestOptions)":"Discord.WebSocket.SocketUserMessage.yml","Discord.WebSocket.SocketUserMessage.AddReactionAsync(System.String,RequestOptions)":"Discord.WebSocket.SocketUserMessage.yml","Discord.WebSocket.SocketUserMessage.RemoveReactionAsync(Emoji,IUser,RequestOptions)":"Discord.WebSocket.SocketUserMessage.yml","Discord.WebSocket.SocketUserMessage.RemoveReactionAsync(System.String,IUser,RequestOptions)":"Discord.WebSocket.SocketUserMessage.yml","Discord.WebSocket.SocketUserMessage.RemoveAllReactionsAsync(RequestOptions)":"Discord.WebSocket.SocketUserMessage.yml","Discord.WebSocket.SocketUserMessage.GetReactionUsersAsync(System.String,System.Int32,System.Nullable{System.UInt64},RequestOptions)":"Discord.WebSocket.SocketUserMessage.yml","Discord.WebSocket.SocketUserMessage.PinAsync(RequestOptions)":"Discord.WebSocket.SocketUserMessage.yml","Discord.WebSocket.SocketUserMessage.UnpinAsync(RequestOptions)":"Discord.WebSocket.SocketUserMessage.yml","Discord.WebSocket.SocketUserMessage.Resolve(System.Int32,TagHandling,TagHandling,TagHandling,TagHandling,TagHandling)":"Discord.WebSocket.SocketUserMessage.yml","Discord.WebSocket.SocketUserMessage.Resolve(TagHandling,TagHandling,TagHandling,TagHandling,TagHandling)":"Discord.WebSocket.SocketUserMessage.yml","Discord.WebSocket.SocketRole":"Discord.WebSocket.SocketRole.yml","Discord.WebSocket.SocketRole.Guild":"Discord.WebSocket.SocketRole.yml","Discord.WebSocket.SocketRole.Color":"Discord.WebSocket.SocketRole.yml","Discord.WebSocket.SocketRole.IsHoisted":"Discord.WebSocket.SocketRole.yml","Discord.WebSocket.SocketRole.IsManaged":"Discord.WebSocket.SocketRole.yml","Discord.WebSocket.SocketRole.IsMentionable":"Discord.WebSocket.SocketRole.yml","Discord.WebSocket.SocketRole.Name":"Discord.WebSocket.SocketRole.yml","Discord.WebSocket.SocketRole.Permissions":"Discord.WebSocket.SocketRole.yml","Discord.WebSocket.SocketRole.Position":"Discord.WebSocket.SocketRole.yml","Discord.WebSocket.SocketRole.CreatedAt":"Discord.WebSocket.SocketRole.yml","Discord.WebSocket.SocketRole.IsEveryone":"Discord.WebSocket.SocketRole.yml","Discord.WebSocket.SocketRole.Mention":"Discord.WebSocket.SocketRole.yml","Discord.WebSocket.SocketRole.ModifyAsync(Action{RoleProperties},RequestOptions)":"Discord.WebSocket.SocketRole.yml","Discord.WebSocket.SocketRole.DeleteAsync(RequestOptions)":"Discord.WebSocket.SocketRole.yml","Discord.WebSocket.SocketRole.ToString":"Discord.WebSocket.SocketRole.yml","Discord.WebSocket.SocketRole.CompareTo(IRole)":"Discord.WebSocket.SocketRole.yml","Discord.WebSocket.SocketGroupUser":"Discord.WebSocket.SocketGroupUser.yml","Discord.WebSocket.SocketGroupUser.Channel":"Discord.WebSocket.SocketGroupUser.yml","Discord.WebSocket.SocketGroupUser.IsBot":"Discord.WebSocket.SocketGroupUser.yml","Discord.WebSocket.SocketGroupUser.Username":"Discord.WebSocket.SocketGroupUser.yml","Discord.WebSocket.SocketGroupUser.DiscriminatorValue":"Discord.WebSocket.SocketGroupUser.yml","Discord.WebSocket.SocketGroupUser.AvatarId":"Discord.WebSocket.SocketGroupUser.yml","Discord.WebSocket.SocketGuildUser":"Discord.WebSocket.SocketGuildUser.yml","Discord.WebSocket.SocketGuildUser.Guild":"Discord.WebSocket.SocketGuildUser.yml","Discord.WebSocket.SocketGuildUser.Nickname":"Discord.WebSocket.SocketGuildUser.yml","Discord.WebSocket.SocketGuildUser.IsBot":"Discord.WebSocket.SocketGuildUser.yml","Discord.WebSocket.SocketGuildUser.Username":"Discord.WebSocket.SocketGuildUser.yml","Discord.WebSocket.SocketGuildUser.DiscriminatorValue":"Discord.WebSocket.SocketGuildUser.yml","Discord.WebSocket.SocketGuildUser.AvatarId":"Discord.WebSocket.SocketGuildUser.yml","Discord.WebSocket.SocketGuildUser.GuildPermissions":"Discord.WebSocket.SocketGuildUser.yml","Discord.WebSocket.SocketGuildUser.IsSelfDeafened":"Discord.WebSocket.SocketGuildUser.yml","Discord.WebSocket.SocketGuildUser.IsSelfMuted":"Discord.WebSocket.SocketGuildUser.yml","Discord.WebSocket.SocketGuildUser.IsSuppressed":"Discord.WebSocket.SocketGuildUser.yml","Discord.WebSocket.SocketGuildUser.IsDeafened":"Discord.WebSocket.SocketGuildUser.yml","Discord.WebSocket.SocketGuildUser.IsMuted":"Discord.WebSocket.SocketGuildUser.yml","Discord.WebSocket.SocketGuildUser.JoinedAt":"Discord.WebSocket.SocketGuildUser.yml","Discord.WebSocket.SocketGuildUser.RoleIds":"Discord.WebSocket.SocketGuildUser.yml","Discord.WebSocket.SocketGuildUser.VoiceChannel":"Discord.WebSocket.SocketGuildUser.yml","Discord.WebSocket.SocketGuildUser.VoiceSessionId":"Discord.WebSocket.SocketGuildUser.yml","Discord.WebSocket.SocketGuildUser.VoiceState":"Discord.WebSocket.SocketGuildUser.yml","Discord.WebSocket.SocketGuildUser.Hierarchy":"Discord.WebSocket.SocketGuildUser.yml","Discord.WebSocket.SocketGuildUser.ModifyAsync(Action{GuildUserProperties},RequestOptions)":"Discord.WebSocket.SocketGuildUser.yml","Discord.WebSocket.SocketGuildUser.KickAsync(RequestOptions)":"Discord.WebSocket.SocketGuildUser.yml","Discord.WebSocket.SocketGuildUser.GetPermissions(IGuildChannel)":"Discord.WebSocket.SocketGuildUser.yml","Discord.WebSocket.SocketPresence":"Discord.WebSocket.SocketPresence.yml","Discord.WebSocket.SocketPresence.Status":"Discord.WebSocket.SocketPresence.yml","Discord.WebSocket.SocketPresence.Game":"Discord.WebSocket.SocketPresence.yml","Discord.WebSocket.SocketPresence.ToString":"Discord.WebSocket.SocketPresence.yml","Discord.WebSocket.SocketSelfUser":"Discord.WebSocket.SocketSelfUser.yml","Discord.WebSocket.SocketSelfUser.Email":"Discord.WebSocket.SocketSelfUser.yml","Discord.WebSocket.SocketSelfUser.IsVerified":"Discord.WebSocket.SocketSelfUser.yml","Discord.WebSocket.SocketSelfUser.IsMfaEnabled":"Discord.WebSocket.SocketSelfUser.yml","Discord.WebSocket.SocketSelfUser.IsBot":"Discord.WebSocket.SocketSelfUser.yml","Discord.WebSocket.SocketSelfUser.Username":"Discord.WebSocket.SocketSelfUser.yml","Discord.WebSocket.SocketSelfUser.DiscriminatorValue":"Discord.WebSocket.SocketSelfUser.yml","Discord.WebSocket.SocketSelfUser.AvatarId":"Discord.WebSocket.SocketSelfUser.yml","Discord.WebSocket.SocketSelfUser.ModifyAsync(Action{SelfUserProperties},RequestOptions)":"Discord.WebSocket.SocketSelfUser.yml","Discord.WebSocket.SocketSimpleUser":"Discord.WebSocket.SocketSimpleUser.yml","Discord.WebSocket.SocketSimpleUser.IsBot":"Discord.WebSocket.SocketSimpleUser.yml","Discord.WebSocket.SocketSimpleUser.Username":"Discord.WebSocket.SocketSimpleUser.yml","Discord.WebSocket.SocketSimpleUser.DiscriminatorValue":"Discord.WebSocket.SocketSimpleUser.yml","Discord.WebSocket.SocketSimpleUser.AvatarId":"Discord.WebSocket.SocketSimpleUser.yml","Discord.WebSocket.SocketUser":"Discord.WebSocket.SocketUser.yml","Discord.WebSocket.SocketUser.IsBot":"Discord.WebSocket.SocketUser.yml","Discord.WebSocket.SocketUser.Username":"Discord.WebSocket.SocketUser.yml","Discord.WebSocket.SocketUser.DiscriminatorValue":"Discord.WebSocket.SocketUser.yml","Discord.WebSocket.SocketUser.AvatarId":"Discord.WebSocket.SocketUser.yml","Discord.WebSocket.SocketUser.AvatarUrl":"Discord.WebSocket.SocketUser.yml","Discord.WebSocket.SocketUser.CreatedAt":"Discord.WebSocket.SocketUser.yml","Discord.WebSocket.SocketUser.Discriminator":"Discord.WebSocket.SocketUser.yml","Discord.WebSocket.SocketUser.Mention":"Discord.WebSocket.SocketUser.yml","Discord.WebSocket.SocketUser.Game":"Discord.WebSocket.SocketUser.yml","Discord.WebSocket.SocketUser.Status":"Discord.WebSocket.SocketUser.yml","Discord.WebSocket.SocketUser.CreateDMChannelAsync(RequestOptions)":"Discord.WebSocket.SocketUser.yml","Discord.WebSocket.SocketUser.ToString":"Discord.WebSocket.SocketUser.yml","Discord.WebSocket.SocketVoiceState":"Discord.WebSocket.SocketVoiceState.yml","Discord.WebSocket.SocketVoiceState.Default":"Discord.WebSocket.SocketVoiceState.yml","Discord.WebSocket.SocketVoiceState.VoiceChannel":"Discord.WebSocket.SocketVoiceState.yml","Discord.WebSocket.SocketVoiceState.VoiceSessionId":"Discord.WebSocket.SocketVoiceState.yml","Discord.WebSocket.SocketVoiceState.IsMuted":"Discord.WebSocket.SocketVoiceState.yml","Discord.WebSocket.SocketVoiceState.IsDeafened":"Discord.WebSocket.SocketVoiceState.yml","Discord.WebSocket.SocketVoiceState.IsSuppressed":"Discord.WebSocket.SocketVoiceState.yml","Discord.WebSocket.SocketVoiceState.IsSelfMuted":"Discord.WebSocket.SocketVoiceState.yml","Discord.WebSocket.SocketVoiceState.IsSelfDeafened":"Discord.WebSocket.SocketVoiceState.yml","Discord.WebSocket.SocketVoiceState.ToString":"Discord.WebSocket.SocketVoiceState.yml","Discord.Audio":"Discord.Audio.yml","Discord.Audio.AudioMode":"Discord.Audio.AudioMode.yml","Discord.Audio.AudioMode.Disabled":"Discord.Audio.AudioMode.yml","Discord.Audio.AudioMode.Outgoing":"Discord.Audio.AudioMode.yml","Discord.Audio.AudioMode.Incoming":"Discord.Audio.AudioMode.yml","Discord.Audio.AudioMode.Both":"Discord.Audio.AudioMode.yml","Discord.Audio.OpusApplication":"Discord.Audio.OpusApplication.yml","Discord.Audio.OpusApplication.Voice":"Discord.Audio.OpusApplication.yml","Discord.Audio.OpusApplication.MusicOrMixed":"Discord.Audio.OpusApplication.yml","Discord.Audio.OpusApplication.LowLatency":"Discord.Audio.OpusApplication.yml","Discord.Audio.SecretBox":"Discord.Audio.SecretBox.yml","Discord.Audio.SecretBox.Encrypt(System.Byte[],System.Int32,System.Int32,System.Byte[],System.Int32,System.Byte[],System.Byte[])":"Discord.Audio.SecretBox.yml","Discord.Audio.SecretBox.Decrypt(System.Byte[],System.Int32,System.Int32,System.Byte[],System.Int32,System.Byte[],System.Byte[])":"Discord.Audio.SecretBox.yml","Discord.Audio.AudioInStream":"Discord.Audio.AudioInStream.yml","Discord.Audio.AudioOutStream":"Discord.Audio.AudioOutStream.yml","Discord.Audio.AudioOutStream.CanRead":"Discord.Audio.AudioOutStream.yml","Discord.Audio.AudioOutStream.CanSeek":"Discord.Audio.AudioOutStream.yml","Discord.Audio.AudioOutStream.CanWrite":"Discord.Audio.AudioOutStream.yml","Discord.Audio.AudioOutStream.Clear":"Discord.Audio.AudioOutStream.yml","Discord.Audio.AudioOutStream.ClearAsync(CancellationToken)":"Discord.Audio.AudioOutStream.yml","Discord.Audio.IAudioClient":"Discord.Audio.IAudioClient.yml","Discord.Audio.IAudioClient.Connected":"Discord.Audio.IAudioClient.yml","Discord.Audio.IAudioClient.Disconnected":"Discord.Audio.IAudioClient.yml","Discord.Audio.IAudioClient.LatencyUpdated":"Discord.Audio.IAudioClient.yml","Discord.Audio.IAudioClient.ConnectionState":"Discord.Audio.IAudioClient.yml","Discord.Audio.IAudioClient.Latency":"Discord.Audio.IAudioClient.yml","Discord.Audio.IAudioClient.DisconnectAsync":"Discord.Audio.IAudioClient.yml","Discord.Audio.IAudioClient.CreateOpusStream(System.Int32,System.Int32)":"Discord.Audio.IAudioClient.yml","Discord.Audio.IAudioClient.CreateDirectOpusStream(System.Int32)":"Discord.Audio.IAudioClient.yml","Discord.Audio.IAudioClient.CreatePCMStream(System.Int32,System.Int32,System.Nullable{System.Int32},System.Int32)":"Discord.Audio.IAudioClient.yml","Discord.Audio.IAudioClient.CreateDirectPCMStream(System.Int32,System.Int32,System.Nullable{System.Int32})":"Discord.Audio.IAudioClient.yml","Discord.Commands.Builders":"Discord.Commands.Builders.yml","Discord.Commands.Builders.CommandBuilder":"Discord.Commands.Builders.CommandBuilder.yml","Discord.Commands.Builders.CommandBuilder.Module":"Discord.Commands.Builders.CommandBuilder.yml","Discord.Commands.Builders.CommandBuilder.Name":"Discord.Commands.Builders.CommandBuilder.yml","Discord.Commands.Builders.CommandBuilder.Summary":"Discord.Commands.Builders.CommandBuilder.yml","Discord.Commands.Builders.CommandBuilder.Remarks":"Discord.Commands.Builders.CommandBuilder.yml","Discord.Commands.Builders.CommandBuilder.RunMode":"Discord.Commands.Builders.CommandBuilder.yml","Discord.Commands.Builders.CommandBuilder.Priority":"Discord.Commands.Builders.CommandBuilder.yml","Discord.Commands.Builders.CommandBuilder.Preconditions":"Discord.Commands.Builders.CommandBuilder.yml","Discord.Commands.Builders.CommandBuilder.Parameters":"Discord.Commands.Builders.CommandBuilder.yml","Discord.Commands.Builders.CommandBuilder.Aliases":"Discord.Commands.Builders.CommandBuilder.yml","Discord.Commands.Builders.CommandBuilder.WithName(System.String)":"Discord.Commands.Builders.CommandBuilder.yml","Discord.Commands.Builders.CommandBuilder.WithSummary(System.String)":"Discord.Commands.Builders.CommandBuilder.yml","Discord.Commands.Builders.CommandBuilder.WithRemarks(System.String)":"Discord.Commands.Builders.CommandBuilder.yml","Discord.Commands.Builders.CommandBuilder.WithRunMode(Discord.Commands.RunMode)":"Discord.Commands.Builders.CommandBuilder.yml","Discord.Commands.Builders.CommandBuilder.WithPriority(System.Int32)":"Discord.Commands.Builders.CommandBuilder.yml","Discord.Commands.Builders.CommandBuilder.AddAliases(System.String[])":"Discord.Commands.Builders.CommandBuilder.yml","Discord.Commands.Builders.CommandBuilder.AddPrecondition(Discord.Commands.PreconditionAttribute)":"Discord.Commands.Builders.CommandBuilder.yml","Discord.Commands.Builders.CommandBuilder.AddParameter``1(System.String,Action{Discord.Commands.Builders.ParameterBuilder})":"Discord.Commands.Builders.CommandBuilder.yml","Discord.Commands.Builders.CommandBuilder.AddParameter(System.String,Type,Action{Discord.Commands.Builders.ParameterBuilder})":"Discord.Commands.Builders.CommandBuilder.yml","Discord.Commands.Builders.ModuleBuilder":"Discord.Commands.Builders.ModuleBuilder.yml","Discord.Commands.Builders.ModuleBuilder.Service":"Discord.Commands.Builders.ModuleBuilder.yml","Discord.Commands.Builders.ModuleBuilder.Parent":"Discord.Commands.Builders.ModuleBuilder.yml","Discord.Commands.Builders.ModuleBuilder.Name":"Discord.Commands.Builders.ModuleBuilder.yml","Discord.Commands.Builders.ModuleBuilder.Summary":"Discord.Commands.Builders.ModuleBuilder.yml","Discord.Commands.Builders.ModuleBuilder.Remarks":"Discord.Commands.Builders.ModuleBuilder.yml","Discord.Commands.Builders.ModuleBuilder.Commands":"Discord.Commands.Builders.ModuleBuilder.yml","Discord.Commands.Builders.ModuleBuilder.Modules":"Discord.Commands.Builders.ModuleBuilder.yml","Discord.Commands.Builders.ModuleBuilder.Preconditions":"Discord.Commands.Builders.ModuleBuilder.yml","Discord.Commands.Builders.ModuleBuilder.Aliases":"Discord.Commands.Builders.ModuleBuilder.yml","Discord.Commands.Builders.ModuleBuilder.WithName(System.String)":"Discord.Commands.Builders.ModuleBuilder.yml","Discord.Commands.Builders.ModuleBuilder.WithSummary(System.String)":"Discord.Commands.Builders.ModuleBuilder.yml","Discord.Commands.Builders.ModuleBuilder.WithRemarks(System.String)":"Discord.Commands.Builders.ModuleBuilder.yml","Discord.Commands.Builders.ModuleBuilder.AddAliases(System.String[])":"Discord.Commands.Builders.ModuleBuilder.yml","Discord.Commands.Builders.ModuleBuilder.AddPrecondition(Discord.Commands.PreconditionAttribute)":"Discord.Commands.Builders.ModuleBuilder.yml","Discord.Commands.Builders.ModuleBuilder.AddCommand(System.String,Func{ICommandContext,System.Object[],Discord.Commands.IDependencyMap,Task},Action{Discord.Commands.Builders.CommandBuilder})":"Discord.Commands.Builders.ModuleBuilder.yml","Discord.Commands.Builders.ModuleBuilder.AddModule(System.String,Action{Discord.Commands.Builders.ModuleBuilder})":"Discord.Commands.Builders.ModuleBuilder.yml","Discord.Commands.Builders.ModuleBuilder.Build(Discord.Commands.CommandService)":"Discord.Commands.Builders.ModuleBuilder.yml","Discord.Commands.Builders.ParameterBuilder":"Discord.Commands.Builders.ParameterBuilder.yml","Discord.Commands.Builders.ParameterBuilder.Command":"Discord.Commands.Builders.ParameterBuilder.yml","Discord.Commands.Builders.ParameterBuilder.Name":"Discord.Commands.Builders.ParameterBuilder.yml","Discord.Commands.Builders.ParameterBuilder.ParameterType":"Discord.Commands.Builders.ParameterBuilder.yml","Discord.Commands.Builders.ParameterBuilder.TypeReader":"Discord.Commands.Builders.ParameterBuilder.yml","Discord.Commands.Builders.ParameterBuilder.IsOptional":"Discord.Commands.Builders.ParameterBuilder.yml","Discord.Commands.Builders.ParameterBuilder.IsRemainder":"Discord.Commands.Builders.ParameterBuilder.yml","Discord.Commands.Builders.ParameterBuilder.IsMultiple":"Discord.Commands.Builders.ParameterBuilder.yml","Discord.Commands.Builders.ParameterBuilder.DefaultValue":"Discord.Commands.Builders.ParameterBuilder.yml","Discord.Commands.Builders.ParameterBuilder.Summary":"Discord.Commands.Builders.ParameterBuilder.yml","Discord.Commands.Builders.ParameterBuilder.Preconditions":"Discord.Commands.Builders.ParameterBuilder.yml","Discord.Commands.Builders.ParameterBuilder.WithSummary(System.String)":"Discord.Commands.Builders.ParameterBuilder.yml","Discord.Commands.Builders.ParameterBuilder.WithDefault(System.Object)":"Discord.Commands.Builders.ParameterBuilder.yml","Discord.Commands.Builders.ParameterBuilder.WithIsOptional(System.Boolean)":"Discord.Commands.Builders.ParameterBuilder.yml","Discord.Commands.Builders.ParameterBuilder.WithIsRemainder(System.Boolean)":"Discord.Commands.Builders.ParameterBuilder.yml","Discord.Commands.Builders.ParameterBuilder.WithIsMultiple(System.Boolean)":"Discord.Commands.Builders.ParameterBuilder.yml","Discord.Commands.Builders.ParameterBuilder.AddPrecondition(Discord.Commands.ParameterPreconditionAttribute)":"Discord.Commands.Builders.ParameterBuilder.yml","Discord":"Discord.yml","Discord.CDN":"Discord.CDN.yml","Discord.CDN.GetApplicationIconUrl(System.UInt64,System.String)":"Discord.CDN.yml","Discord.CDN.GetUserAvatarUrl(System.UInt64,System.String)":"Discord.CDN.yml","Discord.CDN.GetGuildIconUrl(System.UInt64,System.String)":"Discord.CDN.yml","Discord.CDN.GetGuildSplashUrl(System.UInt64,System.String)":"Discord.CDN.yml","Discord.CDN.GetChannelIconUrl(System.UInt64,System.String)":"Discord.CDN.yml","Discord.CDN.GetEmojiUrl(System.UInt64)":"Discord.CDN.yml","Discord.ConnectionState":"Discord.ConnectionState.yml","Discord.ConnectionState.Disconnected":"Discord.ConnectionState.yml","Discord.ConnectionState.Connecting":"Discord.ConnectionState.yml","Discord.ConnectionState.Connected":"Discord.ConnectionState.yml","Discord.ConnectionState.Disconnecting":"Discord.ConnectionState.yml","Discord.DiscordConfig":"Discord.DiscordConfig.yml","Discord.DiscordConfig.APIVersion":"Discord.DiscordConfig.yml","Discord.DiscordConfig.Version":"Discord.DiscordConfig.yml","Discord.DiscordConfig.ClientAPIUrl":"Discord.DiscordConfig.yml","Discord.DiscordConfig.CDNUrl":"Discord.DiscordConfig.yml","Discord.DiscordConfig.InviteUrl":"Discord.DiscordConfig.yml","Discord.DiscordConfig.DefaultRequestTimeout":"Discord.DiscordConfig.yml","Discord.DiscordConfig.MaxMessageSize":"Discord.DiscordConfig.yml","Discord.DiscordConfig.MaxMessagesPerBatch":"Discord.DiscordConfig.yml","Discord.DiscordConfig.MaxUsersPerBatch":"Discord.DiscordConfig.yml","Discord.DiscordConfig.DefaultRetryMode":"Discord.DiscordConfig.yml","Discord.DiscordConfig.LogLevel":"Discord.DiscordConfig.yml","Discord.Format":"Discord.Format.yml","Discord.Format.Bold(System.String)":"Discord.Format.yml","Discord.Format.Italics(System.String)":"Discord.Format.yml","Discord.Format.Underline(System.String)":"Discord.Format.yml","Discord.Format.Strikethrough(System.String)":"Discord.Format.yml","Discord.Format.Code(System.String,System.String)":"Discord.Format.yml","Discord.Format.Sanitize(System.String)":"Discord.Format.yml","Discord.IDiscordClient":"Discord.IDiscordClient.yml","Discord.IDiscordClient.ConnectionState":"Discord.IDiscordClient.yml","Discord.IDiscordClient.CurrentUser":"Discord.IDiscordClient.yml","Discord.IDiscordClient.ConnectAsync":"Discord.IDiscordClient.yml","Discord.IDiscordClient.DisconnectAsync":"Discord.IDiscordClient.yml","Discord.IDiscordClient.GetApplicationInfoAsync":"Discord.IDiscordClient.yml","Discord.IDiscordClient.GetChannelAsync(System.UInt64,Discord.CacheMode)":"Discord.IDiscordClient.yml","Discord.IDiscordClient.GetPrivateChannelsAsync(Discord.CacheMode)":"Discord.IDiscordClient.yml","Discord.IDiscordClient.GetConnectionsAsync":"Discord.IDiscordClient.yml","Discord.IDiscordClient.GetGuildAsync(System.UInt64,Discord.CacheMode)":"Discord.IDiscordClient.yml","Discord.IDiscordClient.GetGuildsAsync(Discord.CacheMode)":"Discord.IDiscordClient.yml","Discord.IDiscordClient.CreateGuildAsync(System.String,Discord.IVoiceRegion,Stream)":"Discord.IDiscordClient.yml","Discord.IDiscordClient.GetInviteAsync(System.String)":"Discord.IDiscordClient.yml","Discord.IDiscordClient.GetUserAsync(System.UInt64,Discord.CacheMode)":"Discord.IDiscordClient.yml","Discord.IDiscordClient.GetUserAsync(System.String,System.String)":"Discord.IDiscordClient.yml","Discord.IDiscordClient.GetVoiceRegionsAsync":"Discord.IDiscordClient.yml","Discord.IDiscordClient.GetVoiceRegionAsync(System.String)":"Discord.IDiscordClient.yml","Discord.LoginState":"Discord.LoginState.yml","Discord.LoginState.LoggedOut":"Discord.LoginState.yml","Discord.LoginState.LoggingIn":"Discord.LoginState.yml","Discord.LoginState.LoggedIn":"Discord.LoginState.yml","Discord.LoginState.LoggingOut":"Discord.LoginState.yml","Discord.RequestOptions":"Discord.RequestOptions.yml","Discord.RequestOptions.Default":"Discord.RequestOptions.yml","Discord.RequestOptions.Timeout":"Discord.RequestOptions.yml","Discord.RequestOptions.CancelToken":"Discord.RequestOptions.yml","Discord.RequestOptions.RetryMode":"Discord.RequestOptions.yml","Discord.RequestOptions.HeaderOnly":"Discord.RequestOptions.yml","Discord.RequestOptions.#ctor":"Discord.RequestOptions.yml","Discord.RequestOptions.Clone":"Discord.RequestOptions.yml","Discord.RetryMode":"Discord.RetryMode.yml","Discord.RetryMode.AlwaysFail":"Discord.RetryMode.yml","Discord.RetryMode.RetryTimeouts":"Discord.RetryMode.yml","Discord.RetryMode.RetryRatelimit":"Discord.RetryMode.yml","Discord.RetryMode.Retry502":"Discord.RetryMode.yml","Discord.RetryMode.AlwaysRetry":"Discord.RetryMode.yml","Discord.TokenType":"Discord.TokenType.yml","Discord.TokenType.User":"Discord.TokenType.yml","Discord.TokenType.Bearer":"Discord.TokenType.yml","Discord.TokenType.Bot":"Discord.TokenType.yml","Discord.CacheMode":"Discord.CacheMode.yml","Discord.CacheMode.AllowDownload":"Discord.CacheMode.yml","Discord.CacheMode.CacheOnly":"Discord.CacheMode.yml","Discord.IApplication":"Discord.IApplication.yml","Discord.IApplication.Name":"Discord.IApplication.yml","Discord.IApplication.Description":"Discord.IApplication.yml","Discord.IApplication.RPCOrigins":"Discord.IApplication.yml","Discord.IApplication.Flags":"Discord.IApplication.yml","Discord.IApplication.IconUrl":"Discord.IApplication.yml","Discord.IApplication.Owner":"Discord.IApplication.yml","Discord.IDeletable":"Discord.IDeletable.yml","Discord.IDeletable.DeleteAsync(Discord.RequestOptions)":"Discord.IDeletable.yml","Discord.IEntity`1":"Discord.IEntity-1.yml","Discord.IEntity`1.Id":"Discord.IEntity-1.yml","Discord.Image":"Discord.Image.yml","Discord.Image.Stream":"Discord.Image.yml","Discord.Image.#ctor(Stream)":"Discord.Image.yml","Discord.IMentionable":"Discord.IMentionable.yml","Discord.IMentionable.Mention":"Discord.IMentionable.yml","Discord.ISnowflakeEntity":"Discord.ISnowflakeEntity.yml","Discord.ISnowflakeEntity.CreatedAt":"Discord.ISnowflakeEntity.yml","Discord.IUpdateable":"Discord.IUpdateable.yml","Discord.IUpdateable.UpdateAsync(Discord.RequestOptions)":"Discord.IUpdateable.yml","Discord.BulkGuildChannelProperties":"Discord.BulkGuildChannelProperties.yml","Discord.BulkGuildChannelProperties.Id":"Discord.BulkGuildChannelProperties.yml","Discord.BulkGuildChannelProperties.Position":"Discord.BulkGuildChannelProperties.yml","Discord.BulkGuildChannelProperties.#ctor(System.UInt64,System.Int32)":"Discord.BulkGuildChannelProperties.yml","Discord.Direction":"Discord.Direction.yml","Discord.Direction.Before":"Discord.Direction.yml","Discord.Direction.After":"Discord.Direction.yml","Discord.Direction.Around":"Discord.Direction.yml","Discord.GuildChannelProperties":"Discord.GuildChannelProperties.yml","Discord.GuildChannelProperties.Name":"Discord.GuildChannelProperties.yml","Discord.GuildChannelProperties.Position":"Discord.GuildChannelProperties.yml","Discord.IAudioChannel":"Discord.IAudioChannel.yml","Discord.IChannel":"Discord.IChannel.yml","Discord.IChannel.Name":"Discord.IChannel.yml","Discord.IChannel.GetUsersAsync(Discord.CacheMode,Discord.RequestOptions)":"Discord.IChannel.yml","Discord.IChannel.GetUserAsync(System.UInt64,Discord.CacheMode,Discord.RequestOptions)":"Discord.IChannel.yml","Discord.IDMChannel":"Discord.IDMChannel.yml","Discord.IDMChannel.Recipient":"Discord.IDMChannel.yml","Discord.IDMChannel.CloseAsync(Discord.RequestOptions)":"Discord.IDMChannel.yml","Discord.IGroupChannel":"Discord.IGroupChannel.yml","Discord.IGroupChannel.LeaveAsync(Discord.RequestOptions)":"Discord.IGroupChannel.yml","Discord.IGuildChannel":"Discord.IGuildChannel.yml","Discord.IGuildChannel.Position":"Discord.IGuildChannel.yml","Discord.IGuildChannel.Guild":"Discord.IGuildChannel.yml","Discord.IGuildChannel.GuildId":"Discord.IGuildChannel.yml","Discord.IGuildChannel.PermissionOverwrites":"Discord.IGuildChannel.yml","Discord.IGuildChannel.CreateInviteAsync(System.Nullable{System.Int32},System.Nullable{System.Int32},System.Boolean,Discord.RequestOptions)":"Discord.IGuildChannel.yml","Discord.IGuildChannel.GetInvitesAsync(Discord.RequestOptions)":"Discord.IGuildChannel.yml","Discord.IGuildChannel.ModifyAsync(Action{Discord.GuildChannelProperties},Discord.RequestOptions)":"Discord.IGuildChannel.yml","Discord.IGuildChannel.GetPermissionOverwrite(Discord.IRole)":"Discord.IGuildChannel.yml","Discord.IGuildChannel.GetPermissionOverwrite(Discord.IUser)":"Discord.IGuildChannel.yml","Discord.IGuildChannel.RemovePermissionOverwriteAsync(Discord.IRole,Discord.RequestOptions)":"Discord.IGuildChannel.yml","Discord.IGuildChannel.RemovePermissionOverwriteAsync(Discord.IUser,Discord.RequestOptions)":"Discord.IGuildChannel.yml","Discord.IGuildChannel.AddPermissionOverwriteAsync(Discord.IRole,Discord.OverwritePermissions,Discord.RequestOptions)":"Discord.IGuildChannel.yml","Discord.IGuildChannel.AddPermissionOverwriteAsync(Discord.IUser,Discord.OverwritePermissions,Discord.RequestOptions)":"Discord.IGuildChannel.yml","Discord.IGuildChannel.GetUsersAsync(Discord.CacheMode,Discord.RequestOptions)":"Discord.IGuildChannel.yml","Discord.IGuildChannel.GetUserAsync(System.UInt64,Discord.CacheMode,Discord.RequestOptions)":"Discord.IGuildChannel.yml","Discord.IMessageChannel":"Discord.IMessageChannel.yml","Discord.IMessageChannel.SendMessageAsync(System.String,System.Boolean,Discord.Embed,Discord.RequestOptions)":"Discord.IMessageChannel.yml","Discord.IMessageChannel.SendFileAsync(Stream,System.String,System.String,System.Boolean,Discord.RequestOptions)":"Discord.IMessageChannel.yml","Discord.IMessageChannel.GetMessageAsync(System.UInt64,Discord.CacheMode,Discord.RequestOptions)":"Discord.IMessageChannel.yml","Discord.IMessageChannel.GetMessagesAsync(System.Int32,Discord.CacheMode,Discord.RequestOptions)":"Discord.IMessageChannel.yml","Discord.IMessageChannel.GetMessagesAsync(System.UInt64,Discord.Direction,System.Int32,Discord.CacheMode,Discord.RequestOptions)":"Discord.IMessageChannel.yml","Discord.IMessageChannel.GetMessagesAsync(Discord.IMessage,Discord.Direction,System.Int32,Discord.CacheMode,Discord.RequestOptions)":"Discord.IMessageChannel.yml","Discord.IMessageChannel.GetPinnedMessagesAsync(Discord.RequestOptions)":"Discord.IMessageChannel.yml","Discord.IMessageChannel.DeleteMessagesAsync(IEnumerable{Discord.IMessage},Discord.RequestOptions)":"Discord.IMessageChannel.yml","Discord.IMessageChannel.TriggerTypingAsync(Discord.RequestOptions)":"Discord.IMessageChannel.yml","Discord.IMessageChannel.EnterTypingState(Discord.RequestOptions)":"Discord.IMessageChannel.yml","Discord.IPrivateChannel":"Discord.IPrivateChannel.yml","Discord.IPrivateChannel.Recipients":"Discord.IPrivateChannel.yml","Discord.ITextChannel":"Discord.ITextChannel.yml","Discord.ITextChannel.Topic":"Discord.ITextChannel.yml","Discord.ITextChannel.ModifyAsync(Action{Discord.TextChannelProperties},Discord.RequestOptions)":"Discord.ITextChannel.yml","Discord.IVoiceChannel":"Discord.IVoiceChannel.yml","Discord.IVoiceChannel.Bitrate":"Discord.IVoiceChannel.yml","Discord.IVoiceChannel.UserLimit":"Discord.IVoiceChannel.yml","Discord.IVoiceChannel.ModifyAsync(Action{Discord.VoiceChannelProperties},Discord.RequestOptions)":"Discord.IVoiceChannel.yml","Discord.IVoiceChannel.ConnectAsync":"Discord.IVoiceChannel.yml","Discord.TextChannelProperties":"Discord.TextChannelProperties.yml","Discord.TextChannelProperties.Topic":"Discord.TextChannelProperties.yml","Discord.VoiceChannelProperties":"Discord.VoiceChannelProperties.yml","Discord.VoiceChannelProperties.Bitrate":"Discord.VoiceChannelProperties.yml","Discord.VoiceChannelProperties.UserLimit":"Discord.VoiceChannelProperties.yml","Discord.DefaultMessageNotifications":"Discord.DefaultMessageNotifications.yml","Discord.DefaultMessageNotifications.AllMessages":"Discord.DefaultMessageNotifications.yml","Discord.DefaultMessageNotifications.MentionsOnly":"Discord.DefaultMessageNotifications.yml","Discord.GuildEmbedProperties":"Discord.GuildEmbedProperties.yml","Discord.GuildEmbedProperties.Enabled":"Discord.GuildEmbedProperties.yml","Discord.GuildEmbedProperties.Channel":"Discord.GuildEmbedProperties.yml","Discord.GuildEmbedProperties.ChannelId":"Discord.GuildEmbedProperties.yml","Discord.GuildEmoji":"Discord.GuildEmoji.yml","Discord.GuildEmoji.Id":"Discord.GuildEmoji.yml","Discord.GuildEmoji.Name":"Discord.GuildEmoji.yml","Discord.GuildEmoji.IsManaged":"Discord.GuildEmoji.yml","Discord.GuildEmoji.RequireColons":"Discord.GuildEmoji.yml","Discord.GuildEmoji.RoleIds":"Discord.GuildEmoji.yml","Discord.GuildEmoji.ToString":"Discord.GuildEmoji.yml","Discord.GuildIntegrationProperties":"Discord.GuildIntegrationProperties.yml","Discord.GuildIntegrationProperties.ExpireBehavior":"Discord.GuildIntegrationProperties.yml","Discord.GuildIntegrationProperties.ExpireGracePeriod":"Discord.GuildIntegrationProperties.yml","Discord.GuildIntegrationProperties.EnableEmoticons":"Discord.GuildIntegrationProperties.yml","Discord.GuildProperties":"Discord.GuildProperties.yml","Discord.GuildProperties.Username":"Discord.GuildProperties.yml","Discord.GuildProperties.Name":"Discord.GuildProperties.yml","Discord.GuildProperties.Region":"Discord.GuildProperties.yml","Discord.GuildProperties.RegionId":"Discord.GuildProperties.yml","Discord.GuildProperties.VerificationLevel":"Discord.GuildProperties.yml","Discord.GuildProperties.DefaultMessageNotifications":"Discord.GuildProperties.yml","Discord.GuildProperties.AfkTimeout":"Discord.GuildProperties.yml","Discord.GuildProperties.Icon":"Discord.GuildProperties.yml","Discord.GuildProperties.Splash":"Discord.GuildProperties.yml","Discord.GuildProperties.AfkChannel":"Discord.GuildProperties.yml","Discord.GuildProperties.AfkChannelId":"Discord.GuildProperties.yml","Discord.GuildProperties.Owner":"Discord.GuildProperties.yml","Discord.GuildProperties.OwnerId":"Discord.GuildProperties.yml","Discord.IBan":"Discord.IBan.yml","Discord.IBan.User":"Discord.IBan.yml","Discord.IBan.Reason":"Discord.IBan.yml","Discord.IGuild":"Discord.IGuild.yml","Discord.IGuild.Name":"Discord.IGuild.yml","Discord.IGuild.AFKTimeout":"Discord.IGuild.yml","Discord.IGuild.IsEmbeddable":"Discord.IGuild.yml","Discord.IGuild.DefaultMessageNotifications":"Discord.IGuild.yml","Discord.IGuild.MfaLevel":"Discord.IGuild.yml","Discord.IGuild.VerificationLevel":"Discord.IGuild.yml","Discord.IGuild.IconId":"Discord.IGuild.yml","Discord.IGuild.IconUrl":"Discord.IGuild.yml","Discord.IGuild.SplashId":"Discord.IGuild.yml","Discord.IGuild.SplashUrl":"Discord.IGuild.yml","Discord.IGuild.Available":"Discord.IGuild.yml","Discord.IGuild.AFKChannelId":"Discord.IGuild.yml","Discord.IGuild.DefaultChannelId":"Discord.IGuild.yml","Discord.IGuild.EmbedChannelId":"Discord.IGuild.yml","Discord.IGuild.OwnerId":"Discord.IGuild.yml","Discord.IGuild.VoiceRegionId":"Discord.IGuild.yml","Discord.IGuild.AudioClient":"Discord.IGuild.yml","Discord.IGuild.EveryoneRole":"Discord.IGuild.yml","Discord.IGuild.Emojis":"Discord.IGuild.yml","Discord.IGuild.Features":"Discord.IGuild.yml","Discord.IGuild.Roles":"Discord.IGuild.yml","Discord.IGuild.ModifyAsync(Action{Discord.GuildProperties},Discord.RequestOptions)":"Discord.IGuild.yml","Discord.IGuild.ModifyEmbedAsync(Action{Discord.GuildEmbedProperties},Discord.RequestOptions)":"Discord.IGuild.yml","Discord.IGuild.ModifyChannelsAsync(IEnumerable{Discord.BulkGuildChannelProperties},Discord.RequestOptions)":"Discord.IGuild.yml","Discord.IGuild.ModifyRolesAsync(IEnumerable{Discord.BulkRoleProperties},Discord.RequestOptions)":"Discord.IGuild.yml","Discord.IGuild.LeaveAsync(Discord.RequestOptions)":"Discord.IGuild.yml","Discord.IGuild.GetBansAsync(Discord.RequestOptions)":"Discord.IGuild.yml","Discord.IGuild.AddBanAsync(Discord.IUser,System.Int32,Discord.RequestOptions)":"Discord.IGuild.yml","Discord.IGuild.AddBanAsync(System.UInt64,System.Int32,Discord.RequestOptions)":"Discord.IGuild.yml","Discord.IGuild.RemoveBanAsync(Discord.IUser,Discord.RequestOptions)":"Discord.IGuild.yml","Discord.IGuild.RemoveBanAsync(System.UInt64,Discord.RequestOptions)":"Discord.IGuild.yml","Discord.IGuild.GetChannelsAsync(Discord.CacheMode,Discord.RequestOptions)":"Discord.IGuild.yml","Discord.IGuild.GetChannelAsync(System.UInt64,Discord.CacheMode,Discord.RequestOptions)":"Discord.IGuild.yml","Discord.IGuild.CreateTextChannelAsync(System.String,Discord.RequestOptions)":"Discord.IGuild.yml","Discord.IGuild.CreateVoiceChannelAsync(System.String,Discord.RequestOptions)":"Discord.IGuild.yml","Discord.IGuild.GetIntegrationsAsync(Discord.RequestOptions)":"Discord.IGuild.yml","Discord.IGuild.CreateIntegrationAsync(System.UInt64,System.String,Discord.RequestOptions)":"Discord.IGuild.yml","Discord.IGuild.GetInvitesAsync(Discord.RequestOptions)":"Discord.IGuild.yml","Discord.IGuild.GetRole(System.UInt64)":"Discord.IGuild.yml","Discord.IGuild.CreateRoleAsync(System.String,System.Nullable{Discord.GuildPermissions},System.Nullable{Discord.Color},System.Boolean,Discord.RequestOptions)":"Discord.IGuild.yml","Discord.IGuild.GetUsersAsync(Discord.CacheMode,Discord.RequestOptions)":"Discord.IGuild.yml","Discord.IGuild.GetUserAsync(System.UInt64,Discord.CacheMode,Discord.RequestOptions)":"Discord.IGuild.yml","Discord.IGuild.GetCurrentUserAsync(Discord.CacheMode,Discord.RequestOptions)":"Discord.IGuild.yml","Discord.IGuild.DownloadUsersAsync":"Discord.IGuild.yml","Discord.IGuild.PruneUsersAsync(System.Int32,System.Boolean,Discord.RequestOptions)":"Discord.IGuild.yml","Discord.IGuildIntegration":"Discord.IGuildIntegration.yml","Discord.IGuildIntegration.Id":"Discord.IGuildIntegration.yml","Discord.IGuildIntegration.Name":"Discord.IGuildIntegration.yml","Discord.IGuildIntegration.Type":"Discord.IGuildIntegration.yml","Discord.IGuildIntegration.IsEnabled":"Discord.IGuildIntegration.yml","Discord.IGuildIntegration.IsSyncing":"Discord.IGuildIntegration.yml","Discord.IGuildIntegration.ExpireBehavior":"Discord.IGuildIntegration.yml","Discord.IGuildIntegration.ExpireGracePeriod":"Discord.IGuildIntegration.yml","Discord.IGuildIntegration.SyncedAt":"Discord.IGuildIntegration.yml","Discord.IGuildIntegration.Account":"Discord.IGuildIntegration.yml","Discord.IGuildIntegration.Guild":"Discord.IGuildIntegration.yml","Discord.IGuildIntegration.GuildId":"Discord.IGuildIntegration.yml","Discord.IGuildIntegration.RoleId":"Discord.IGuildIntegration.yml","Discord.IGuildIntegration.User":"Discord.IGuildIntegration.yml","Discord.IntegrationAccount":"Discord.IntegrationAccount.yml","Discord.IntegrationAccount.Id":"Discord.IntegrationAccount.yml","Discord.IntegrationAccount.Name":"Discord.IntegrationAccount.yml","Discord.IntegrationAccount.ToString":"Discord.IntegrationAccount.yml","Discord.IUserGuild":"Discord.IUserGuild.yml","Discord.IUserGuild.Name":"Discord.IUserGuild.yml","Discord.IUserGuild.IconUrl":"Discord.IUserGuild.yml","Discord.IUserGuild.IsOwner":"Discord.IUserGuild.yml","Discord.IUserGuild.Permissions":"Discord.IUserGuild.yml","Discord.IVoiceRegion":"Discord.IVoiceRegion.yml","Discord.IVoiceRegion.Id":"Discord.IVoiceRegion.yml","Discord.IVoiceRegion.Name":"Discord.IVoiceRegion.yml","Discord.IVoiceRegion.IsVip":"Discord.IVoiceRegion.yml","Discord.IVoiceRegion.IsOptimal":"Discord.IVoiceRegion.yml","Discord.IVoiceRegion.SampleHostname":"Discord.IVoiceRegion.yml","Discord.IVoiceRegion.SamplePort":"Discord.IVoiceRegion.yml","Discord.MfaLevel":"Discord.MfaLevel.yml","Discord.MfaLevel.Disabled":"Discord.MfaLevel.yml","Discord.MfaLevel.Enabled":"Discord.MfaLevel.yml","Discord.PermissionTarget":"Discord.PermissionTarget.yml","Discord.PermissionTarget.Role":"Discord.PermissionTarget.yml","Discord.PermissionTarget.User":"Discord.PermissionTarget.yml","Discord.VerificationLevel":"Discord.VerificationLevel.yml","Discord.VerificationLevel.None":"Discord.VerificationLevel.yml","Discord.VerificationLevel.Low":"Discord.VerificationLevel.yml","Discord.VerificationLevel.Medium":"Discord.VerificationLevel.yml","Discord.VerificationLevel.High":"Discord.VerificationLevel.yml","Discord.IInvite":"Discord.IInvite.yml","Discord.IInvite.Code":"Discord.IInvite.yml","Discord.IInvite.Url":"Discord.IInvite.yml","Discord.IInvite.Channel":"Discord.IInvite.yml","Discord.IInvite.ChannelId":"Discord.IInvite.yml","Discord.IInvite.Guild":"Discord.IInvite.yml","Discord.IInvite.GuildId":"Discord.IInvite.yml","Discord.IInvite.AcceptAsync(Discord.RequestOptions)":"Discord.IInvite.yml","Discord.IInviteMetadata":"Discord.IInviteMetadata.yml","Discord.IInviteMetadata.Inviter":"Discord.IInviteMetadata.yml","Discord.IInviteMetadata.IsRevoked":"Discord.IInviteMetadata.yml","Discord.IInviteMetadata.IsTemporary":"Discord.IInviteMetadata.yml","Discord.IInviteMetadata.MaxAge":"Discord.IInviteMetadata.yml","Discord.IInviteMetadata.MaxUses":"Discord.IInviteMetadata.yml","Discord.IInviteMetadata.Uses":"Discord.IInviteMetadata.yml","Discord.IInviteMetadata.CreatedAt":"Discord.IInviteMetadata.yml","Discord.Embed":"Discord.Embed.yml","Discord.Embed.Type":"Discord.Embed.yml","Discord.Embed.Description":"Discord.Embed.yml","Discord.Embed.Url":"Discord.Embed.yml","Discord.Embed.Title":"Discord.Embed.yml","Discord.Embed.Timestamp":"Discord.Embed.yml","Discord.Embed.Color":"Discord.Embed.yml","Discord.Embed.Image":"Discord.Embed.yml","Discord.Embed.Video":"Discord.Embed.yml","Discord.Embed.Author":"Discord.Embed.yml","Discord.Embed.Footer":"Discord.Embed.yml","Discord.Embed.Provider":"Discord.Embed.yml","Discord.Embed.Thumbnail":"Discord.Embed.yml","Discord.Embed.Fields":"Discord.Embed.yml","Discord.Embed.ToString":"Discord.Embed.yml","Discord.EmbedAuthor":"Discord.EmbedAuthor.yml","Discord.EmbedAuthor.Name":"Discord.EmbedAuthor.yml","Discord.EmbedAuthor.Url":"Discord.EmbedAuthor.yml","Discord.EmbedAuthor.IconUrl":"Discord.EmbedAuthor.yml","Discord.EmbedAuthor.ProxyIconUrl":"Discord.EmbedAuthor.yml","Discord.EmbedAuthor.ToString":"Discord.EmbedAuthor.yml","Discord.EmbedField":"Discord.EmbedField.yml","Discord.EmbedField.Name":"Discord.EmbedField.yml","Discord.EmbedField.Value":"Discord.EmbedField.yml","Discord.EmbedField.Inline":"Discord.EmbedField.yml","Discord.EmbedField.ToString":"Discord.EmbedField.yml","Discord.EmbedFooter":"Discord.EmbedFooter.yml","Discord.EmbedFooter.Text":"Discord.EmbedFooter.yml","Discord.EmbedFooter.IconUrl":"Discord.EmbedFooter.yml","Discord.EmbedFooter.ProxyUrl":"Discord.EmbedFooter.yml","Discord.EmbedFooter.ToString":"Discord.EmbedFooter.yml","Discord.EmbedImage":"Discord.EmbedImage.yml","Discord.EmbedImage.Url":"Discord.EmbedImage.yml","Discord.EmbedImage.ProxyUrl":"Discord.EmbedImage.yml","Discord.EmbedImage.Height":"Discord.EmbedImage.yml","Discord.EmbedImage.Width":"Discord.EmbedImage.yml","Discord.EmbedImage.ToString":"Discord.EmbedImage.yml","Discord.EmbedProvider":"Discord.EmbedProvider.yml","Discord.EmbedProvider.Name":"Discord.EmbedProvider.yml","Discord.EmbedProvider.Url":"Discord.EmbedProvider.yml","Discord.EmbedProvider.ToString":"Discord.EmbedProvider.yml","Discord.EmbedThumbnail":"Discord.EmbedThumbnail.yml","Discord.EmbedThumbnail.Url":"Discord.EmbedThumbnail.yml","Discord.EmbedThumbnail.ProxyUrl":"Discord.EmbedThumbnail.yml","Discord.EmbedThumbnail.Height":"Discord.EmbedThumbnail.yml","Discord.EmbedThumbnail.Width":"Discord.EmbedThumbnail.yml","Discord.EmbedThumbnail.ToString":"Discord.EmbedThumbnail.yml","Discord.EmbedVideo":"Discord.EmbedVideo.yml","Discord.EmbedVideo.Url":"Discord.EmbedVideo.yml","Discord.EmbedVideo.Height":"Discord.EmbedVideo.yml","Discord.EmbedVideo.Width":"Discord.EmbedVideo.yml","Discord.EmbedVideo.ToString":"Discord.EmbedVideo.yml","Discord.Emoji":"Discord.Emoji.yml","Discord.Emoji.Id":"Discord.Emoji.yml","Discord.Emoji.Name":"Discord.Emoji.yml","Discord.Emoji.Url":"Discord.Emoji.yml","Discord.Emoji.Parse(System.String)":"Discord.Emoji.yml","Discord.Emoji.TryParse(System.String,Discord.Emoji@)":"Discord.Emoji.yml","Discord.Emoji.ToString":"Discord.Emoji.yml","Discord.IAttachment":"Discord.IAttachment.yml","Discord.IAttachment.Id":"Discord.IAttachment.yml","Discord.IAttachment.Filename":"Discord.IAttachment.yml","Discord.IAttachment.Url":"Discord.IAttachment.yml","Discord.IAttachment.ProxyUrl":"Discord.IAttachment.yml","Discord.IAttachment.Size":"Discord.IAttachment.yml","Discord.IAttachment.Height":"Discord.IAttachment.yml","Discord.IAttachment.Width":"Discord.IAttachment.yml","Discord.IEmbed":"Discord.IEmbed.yml","Discord.IEmbed.Url":"Discord.IEmbed.yml","Discord.IEmbed.Type":"Discord.IEmbed.yml","Discord.IEmbed.Title":"Discord.IEmbed.yml","Discord.IEmbed.Description":"Discord.IEmbed.yml","Discord.IEmbed.Timestamp":"Discord.IEmbed.yml","Discord.IEmbed.Color":"Discord.IEmbed.yml","Discord.IEmbed.Image":"Discord.IEmbed.yml","Discord.IEmbed.Video":"Discord.IEmbed.yml","Discord.IEmbed.Author":"Discord.IEmbed.yml","Discord.IEmbed.Footer":"Discord.IEmbed.yml","Discord.IEmbed.Provider":"Discord.IEmbed.yml","Discord.IEmbed.Thumbnail":"Discord.IEmbed.yml","Discord.IEmbed.Fields":"Discord.IEmbed.yml","Discord.IMessage":"Discord.IMessage.yml","Discord.IMessage.Type":"Discord.IMessage.yml","Discord.IMessage.IsTTS":"Discord.IMessage.yml","Discord.IMessage.IsPinned":"Discord.IMessage.yml","Discord.IMessage.IsWebhook":"Discord.IMessage.yml","Discord.IMessage.Content":"Discord.IMessage.yml","Discord.IMessage.Timestamp":"Discord.IMessage.yml","Discord.IMessage.EditedTimestamp":"Discord.IMessage.yml","Discord.IMessage.Channel":"Discord.IMessage.yml","Discord.IMessage.Author":"Discord.IMessage.yml","Discord.IMessage.WebhookId":"Discord.IMessage.yml","Discord.IMessage.Attachments":"Discord.IMessage.yml","Discord.IMessage.Embeds":"Discord.IMessage.yml","Discord.IMessage.Tags":"Discord.IMessage.yml","Discord.IMessage.MentionedChannelIds":"Discord.IMessage.yml","Discord.IMessage.MentionedRoleIds":"Discord.IMessage.yml","Discord.IMessage.MentionedUserIds":"Discord.IMessage.yml","Discord.IReaction":"Discord.IReaction.yml","Discord.IReaction.Emoji":"Discord.IReaction.yml","Discord.ISystemMessage":"Discord.ISystemMessage.yml","Discord.ITag":"Discord.ITag.yml","Discord.ITag.Index":"Discord.ITag.yml","Discord.ITag.Length":"Discord.ITag.yml","Discord.ITag.Type":"Discord.ITag.yml","Discord.ITag.Key":"Discord.ITag.yml","Discord.ITag.Value":"Discord.ITag.yml","Discord.IUserMessage":"Discord.IUserMessage.yml","Discord.IUserMessage.ModifyAsync(Action{Discord.MessageProperties},Discord.RequestOptions)":"Discord.IUserMessage.yml","Discord.IUserMessage.PinAsync(Discord.RequestOptions)":"Discord.IUserMessage.yml","Discord.IUserMessage.UnpinAsync(Discord.RequestOptions)":"Discord.IUserMessage.yml","Discord.IUserMessage.Reactions":"Discord.IUserMessage.yml","Discord.IUserMessage.AddReactionAsync(Discord.Emoji,Discord.RequestOptions)":"Discord.IUserMessage.yml","Discord.IUserMessage.AddReactionAsync(System.String,Discord.RequestOptions)":"Discord.IUserMessage.yml","Discord.IUserMessage.RemoveReactionAsync(Discord.Emoji,Discord.IUser,Discord.RequestOptions)":"Discord.IUserMessage.yml","Discord.IUserMessage.RemoveReactionAsync(System.String,Discord.IUser,Discord.RequestOptions)":"Discord.IUserMessage.yml","Discord.IUserMessage.RemoveAllReactionsAsync(Discord.RequestOptions)":"Discord.IUserMessage.yml","Discord.IUserMessage.GetReactionUsersAsync(System.String,System.Int32,System.Nullable{System.UInt64},Discord.RequestOptions)":"Discord.IUserMessage.yml","Discord.IUserMessage.Resolve(Discord.TagHandling,Discord.TagHandling,Discord.TagHandling,Discord.TagHandling,Discord.TagHandling)":"Discord.IUserMessage.yml","Discord.MessageProperties":"Discord.MessageProperties.yml","Discord.MessageProperties.Content":"Discord.MessageProperties.yml","Discord.MessageProperties.Embed":"Discord.MessageProperties.yml","Discord.MessageType":"Discord.MessageType.yml","Discord.MessageType.Default":"Discord.MessageType.yml","Discord.MessageType.RecipientAdd":"Discord.MessageType.yml","Discord.MessageType.RecipientRemove":"Discord.MessageType.yml","Discord.MessageType.Call":"Discord.MessageType.yml","Discord.MessageType.ChannelNameChange":"Discord.MessageType.yml","Discord.MessageType.ChannelIconChange":"Discord.MessageType.yml","Discord.MessageType.ChannelPinnedMessage":"Discord.MessageType.yml","Discord.Tag`1":"Discord.Tag-1.yml","Discord.Tag`1.Type":"Discord.Tag-1.yml","Discord.Tag`1.Index":"Discord.Tag-1.yml","Discord.Tag`1.Length":"Discord.Tag-1.yml","Discord.Tag`1.Key":"Discord.Tag-1.yml","Discord.Tag`1.Value":"Discord.Tag-1.yml","Discord.Tag`1.ToString":"Discord.Tag-1.yml","Discord.Tag`1.Discord#ITag#Value":"Discord.Tag-1.yml","Discord.TagHandling":"Discord.TagHandling.yml","Discord.TagHandling.Ignore":"Discord.TagHandling.yml","Discord.TagHandling.Remove":"Discord.TagHandling.yml","Discord.TagHandling.Name":"Discord.TagHandling.yml","Discord.TagHandling.NameNoPrefix":"Discord.TagHandling.yml","Discord.TagHandling.FullName":"Discord.TagHandling.yml","Discord.TagHandling.FullNameNoPrefix":"Discord.TagHandling.yml","Discord.TagHandling.Sanitize":"Discord.TagHandling.yml","Discord.TagType":"Discord.TagType.yml","Discord.TagType.UserMention":"Discord.TagType.yml","Discord.TagType.ChannelMention":"Discord.TagType.yml","Discord.TagType.RoleMention":"Discord.TagType.yml","Discord.TagType.EveryoneMention":"Discord.TagType.yml","Discord.TagType.HereMention":"Discord.TagType.yml","Discord.TagType.Emoji":"Discord.TagType.yml","Discord.ChannelPermission":"Discord.ChannelPermission.yml","Discord.ChannelPermission.CreateInstantInvite":"Discord.ChannelPermission.yml","Discord.ChannelPermission.ManageChannel":"Discord.ChannelPermission.yml","Discord.ChannelPermission.AddReactions":"Discord.ChannelPermission.yml","Discord.ChannelPermission.ReadMessages":"Discord.ChannelPermission.yml","Discord.ChannelPermission.SendMessages":"Discord.ChannelPermission.yml","Discord.ChannelPermission.SendTTSMessages":"Discord.ChannelPermission.yml","Discord.ChannelPermission.ManageMessages":"Discord.ChannelPermission.yml","Discord.ChannelPermission.EmbedLinks":"Discord.ChannelPermission.yml","Discord.ChannelPermission.AttachFiles":"Discord.ChannelPermission.yml","Discord.ChannelPermission.ReadMessageHistory":"Discord.ChannelPermission.yml","Discord.ChannelPermission.MentionEveryone":"Discord.ChannelPermission.yml","Discord.ChannelPermission.UseExternalEmojis":"Discord.ChannelPermission.yml","Discord.ChannelPermission.Connect":"Discord.ChannelPermission.yml","Discord.ChannelPermission.Speak":"Discord.ChannelPermission.yml","Discord.ChannelPermission.MuteMembers":"Discord.ChannelPermission.yml","Discord.ChannelPermission.DeafenMembers":"Discord.ChannelPermission.yml","Discord.ChannelPermission.MoveMembers":"Discord.ChannelPermission.yml","Discord.ChannelPermission.UseVAD":"Discord.ChannelPermission.yml","Discord.ChannelPermission.ManagePermissions":"Discord.ChannelPermission.yml","Discord.ChannelPermission.ManageWebhooks":"Discord.ChannelPermission.yml","Discord.ChannelPermissions":"Discord.ChannelPermissions.yml","Discord.ChannelPermissions.None":"Discord.ChannelPermissions.yml","Discord.ChannelPermissions.All(Discord.IChannel)":"Discord.ChannelPermissions.yml","Discord.ChannelPermissions.RawValue":"Discord.ChannelPermissions.yml","Discord.ChannelPermissions.CreateInstantInvite":"Discord.ChannelPermissions.yml","Discord.ChannelPermissions.ManageChannel":"Discord.ChannelPermissions.yml","Discord.ChannelPermissions.AddReactions":"Discord.ChannelPermissions.yml","Discord.ChannelPermissions.ReadMessages":"Discord.ChannelPermissions.yml","Discord.ChannelPermissions.SendMessages":"Discord.ChannelPermissions.yml","Discord.ChannelPermissions.SendTTSMessages":"Discord.ChannelPermissions.yml","Discord.ChannelPermissions.ManageMessages":"Discord.ChannelPermissions.yml","Discord.ChannelPermissions.EmbedLinks":"Discord.ChannelPermissions.yml","Discord.ChannelPermissions.AttachFiles":"Discord.ChannelPermissions.yml","Discord.ChannelPermissions.ReadMessageHistory":"Discord.ChannelPermissions.yml","Discord.ChannelPermissions.MentionEveryone":"Discord.ChannelPermissions.yml","Discord.ChannelPermissions.UseExternalEmojis":"Discord.ChannelPermissions.yml","Discord.ChannelPermissions.Connect":"Discord.ChannelPermissions.yml","Discord.ChannelPermissions.Speak":"Discord.ChannelPermissions.yml","Discord.ChannelPermissions.MuteMembers":"Discord.ChannelPermissions.yml","Discord.ChannelPermissions.DeafenMembers":"Discord.ChannelPermissions.yml","Discord.ChannelPermissions.MoveMembers":"Discord.ChannelPermissions.yml","Discord.ChannelPermissions.UseVAD":"Discord.ChannelPermissions.yml","Discord.ChannelPermissions.ManagePermissions":"Discord.ChannelPermissions.yml","Discord.ChannelPermissions.ManageWebhooks":"Discord.ChannelPermissions.yml","Discord.ChannelPermissions.#ctor(System.UInt64)":"Discord.ChannelPermissions.yml","Discord.ChannelPermissions.#ctor(System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean)":"Discord.ChannelPermissions.yml","Discord.ChannelPermissions.Modify(System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Boolean,System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean})":"Discord.ChannelPermissions.yml","Discord.ChannelPermissions.Has(Discord.ChannelPermission)":"Discord.ChannelPermissions.yml","Discord.ChannelPermissions.ToList":"Discord.ChannelPermissions.yml","Discord.ChannelPermissions.ToString":"Discord.ChannelPermissions.yml","Discord.GuildPermission":"Discord.GuildPermission.yml","Discord.GuildPermission.CreateInstantInvite":"Discord.GuildPermission.yml","Discord.GuildPermission.KickMembers":"Discord.GuildPermission.yml","Discord.GuildPermission.BanMembers":"Discord.GuildPermission.yml","Discord.GuildPermission.Administrator":"Discord.GuildPermission.yml","Discord.GuildPermission.ManageChannels":"Discord.GuildPermission.yml","Discord.GuildPermission.ManageGuild":"Discord.GuildPermission.yml","Discord.GuildPermission.AddReactions":"Discord.GuildPermission.yml","Discord.GuildPermission.ReadMessages":"Discord.GuildPermission.yml","Discord.GuildPermission.SendMessages":"Discord.GuildPermission.yml","Discord.GuildPermission.SendTTSMessages":"Discord.GuildPermission.yml","Discord.GuildPermission.ManageMessages":"Discord.GuildPermission.yml","Discord.GuildPermission.EmbedLinks":"Discord.GuildPermission.yml","Discord.GuildPermission.AttachFiles":"Discord.GuildPermission.yml","Discord.GuildPermission.ReadMessageHistory":"Discord.GuildPermission.yml","Discord.GuildPermission.MentionEveryone":"Discord.GuildPermission.yml","Discord.GuildPermission.UseExternalEmojis":"Discord.GuildPermission.yml","Discord.GuildPermission.Connect":"Discord.GuildPermission.yml","Discord.GuildPermission.Speak":"Discord.GuildPermission.yml","Discord.GuildPermission.MuteMembers":"Discord.GuildPermission.yml","Discord.GuildPermission.DeafenMembers":"Discord.GuildPermission.yml","Discord.GuildPermission.MoveMembers":"Discord.GuildPermission.yml","Discord.GuildPermission.UseVAD":"Discord.GuildPermission.yml","Discord.GuildPermission.ChangeNickname":"Discord.GuildPermission.yml","Discord.GuildPermission.ManageNicknames":"Discord.GuildPermission.yml","Discord.GuildPermission.ManageRoles":"Discord.GuildPermission.yml","Discord.GuildPermission.ManageWebhooks":"Discord.GuildPermission.yml","Discord.GuildPermission.ManageEmojis":"Discord.GuildPermission.yml","Discord.GuildPermissions":"Discord.GuildPermissions.yml","Discord.GuildPermissions.None":"Discord.GuildPermissions.yml","Discord.GuildPermissions.All":"Discord.GuildPermissions.yml","Discord.GuildPermissions.RawValue":"Discord.GuildPermissions.yml","Discord.GuildPermissions.CreateInstantInvite":"Discord.GuildPermissions.yml","Discord.GuildPermissions.BanMembers":"Discord.GuildPermissions.yml","Discord.GuildPermissions.KickMembers":"Discord.GuildPermissions.yml","Discord.GuildPermissions.Administrator":"Discord.GuildPermissions.yml","Discord.GuildPermissions.ManageChannels":"Discord.GuildPermissions.yml","Discord.GuildPermissions.ManageGuild":"Discord.GuildPermissions.yml","Discord.GuildPermissions.AddReactions":"Discord.GuildPermissions.yml","Discord.GuildPermissions.ReadMessages":"Discord.GuildPermissions.yml","Discord.GuildPermissions.SendMessages":"Discord.GuildPermissions.yml","Discord.GuildPermissions.SendTTSMessages":"Discord.GuildPermissions.yml","Discord.GuildPermissions.ManageMessages":"Discord.GuildPermissions.yml","Discord.GuildPermissions.EmbedLinks":"Discord.GuildPermissions.yml","Discord.GuildPermissions.AttachFiles":"Discord.GuildPermissions.yml","Discord.GuildPermissions.ReadMessageHistory":"Discord.GuildPermissions.yml","Discord.GuildPermissions.MentionEveryone":"Discord.GuildPermissions.yml","Discord.GuildPermissions.UseExternalEmojis":"Discord.GuildPermissions.yml","Discord.GuildPermissions.Connect":"Discord.GuildPermissions.yml","Discord.GuildPermissions.Speak":"Discord.GuildPermissions.yml","Discord.GuildPermissions.MuteMembers":"Discord.GuildPermissions.yml","Discord.GuildPermissions.DeafenMembers":"Discord.GuildPermissions.yml","Discord.GuildPermissions.MoveMembers":"Discord.GuildPermissions.yml","Discord.GuildPermissions.UseVAD":"Discord.GuildPermissions.yml","Discord.GuildPermissions.ChangeNickname":"Discord.GuildPermissions.yml","Discord.GuildPermissions.ManageNicknames":"Discord.GuildPermissions.yml","Discord.GuildPermissions.ManageRoles":"Discord.GuildPermissions.yml","Discord.GuildPermissions.ManageWebhooks":"Discord.GuildPermissions.yml","Discord.GuildPermissions.ManageEmojis":"Discord.GuildPermissions.yml","Discord.GuildPermissions.#ctor(System.UInt64)":"Discord.GuildPermissions.yml","Discord.GuildPermissions.#ctor(System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Boolean,System.Boolean,System.Boolean)":"Discord.GuildPermissions.yml","Discord.GuildPermissions.Modify(System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean},System.Nullable{System.Boolean})":"Discord.GuildPermissions.yml","Discord.GuildPermissions.Has(Discord.GuildPermission)":"Discord.GuildPermissions.yml","Discord.GuildPermissions.ToList":"Discord.GuildPermissions.yml","Discord.GuildPermissions.ToString":"Discord.GuildPermissions.yml","Discord.Overwrite":"Discord.Overwrite.yml","Discord.Overwrite.TargetId":"Discord.Overwrite.yml","Discord.Overwrite.TargetType":"Discord.Overwrite.yml","Discord.Overwrite.Permissions":"Discord.Overwrite.yml","Discord.Overwrite.#ctor(System.UInt64,Discord.PermissionTarget,Discord.OverwritePermissions)":"Discord.Overwrite.yml","Discord.OverwritePermissions":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.InheritAll":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.AllowAll(Discord.IChannel)":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.DenyAll(Discord.IChannel)":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.AllowValue":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.DenyValue":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.CreateInstantInvite":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.ManageChannel":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.AddReactions":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.ReadMessages":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.SendMessages":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.SendTTSMessages":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.ManageMessages":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.EmbedLinks":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.AttachFiles":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.ReadMessageHistory":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.MentionEveryone":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.UseExternalEmojis":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.Connect":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.Speak":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.MuteMembers":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.DeafenMembers":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.MoveMembers":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.UseVAD":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.ManagePermissions":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.ManageWebhooks":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.#ctor(System.UInt64,System.UInt64)":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.#ctor(Discord.PermValue,Discord.PermValue,Discord.PermValue,Discord.PermValue,Discord.PermValue,Discord.PermValue,Discord.PermValue,Discord.PermValue,Discord.PermValue,Discord.PermValue,Discord.PermValue,Discord.PermValue,Discord.PermValue,Discord.PermValue,Discord.PermValue,Discord.PermValue,Discord.PermValue,Discord.PermValue,Discord.PermValue,Discord.PermValue)":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.Modify(System.Nullable{Discord.PermValue},System.Nullable{Discord.PermValue},System.Nullable{Discord.PermValue},System.Nullable{Discord.PermValue},System.Nullable{Discord.PermValue},System.Nullable{Discord.PermValue},System.Nullable{Discord.PermValue},System.Nullable{Discord.PermValue},System.Nullable{Discord.PermValue},System.Nullable{Discord.PermValue},System.Nullable{Discord.PermValue},System.Nullable{Discord.PermValue},System.Nullable{Discord.PermValue},System.Nullable{Discord.PermValue},System.Nullable{Discord.PermValue},System.Nullable{Discord.PermValue},System.Nullable{Discord.PermValue},System.Nullable{Discord.PermValue},System.Nullable{Discord.PermValue},System.Nullable{Discord.PermValue})":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.ToAllowList":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.ToDenyList":"Discord.OverwritePermissions.yml","Discord.OverwritePermissions.ToString":"Discord.OverwritePermissions.yml","Discord.PermValue":"Discord.PermValue.yml","Discord.PermValue.Allow":"Discord.PermValue.yml","Discord.PermValue.Deny":"Discord.PermValue.yml","Discord.PermValue.Inherit":"Discord.PermValue.yml","Discord.BulkRoleProperties":"Discord.BulkRoleProperties.yml","Discord.BulkRoleProperties.Id":"Discord.BulkRoleProperties.yml","Discord.BulkRoleProperties.#ctor(System.UInt64)":"Discord.BulkRoleProperties.yml","Discord.Color":"Discord.Color.yml","Discord.Color.Default":"Discord.Color.yml","Discord.Color.RawValue":"Discord.Color.yml","Discord.Color.R":"Discord.Color.yml","Discord.Color.G":"Discord.Color.yml","Discord.Color.B":"Discord.Color.yml","Discord.Color.#ctor(System.UInt32)":"Discord.Color.yml","Discord.Color.#ctor(System.Byte,System.Byte,System.Byte)":"Discord.Color.yml","Discord.Color.#ctor(System.Single,System.Single,System.Single)":"Discord.Color.yml","Discord.Color.ToString":"Discord.Color.yml","Discord.IRole":"Discord.IRole.yml","Discord.IRole.Guild":"Discord.IRole.yml","Discord.IRole.Color":"Discord.IRole.yml","Discord.IRole.IsHoisted":"Discord.IRole.yml","Discord.IRole.IsManaged":"Discord.IRole.yml","Discord.IRole.IsMentionable":"Discord.IRole.yml","Discord.IRole.Name":"Discord.IRole.yml","Discord.IRole.Permissions":"Discord.IRole.yml","Discord.IRole.Position":"Discord.IRole.yml","Discord.IRole.ModifyAsync(Action{Discord.RoleProperties},Discord.RequestOptions)":"Discord.IRole.yml","Discord.RoleProperties":"Discord.RoleProperties.yml","Discord.RoleProperties.Name":"Discord.RoleProperties.yml","Discord.RoleProperties.Permissions":"Discord.RoleProperties.yml","Discord.RoleProperties.Position":"Discord.RoleProperties.yml","Discord.RoleProperties.Color":"Discord.RoleProperties.yml","Discord.RoleProperties.Hoist":"Discord.RoleProperties.yml","Discord.RoleProperties.Mentionable":"Discord.RoleProperties.yml","Discord.Game":"Discord.Game.yml","Discord.Game.Name":"Discord.Game.yml","Discord.Game.StreamUrl":"Discord.Game.yml","Discord.Game.StreamType":"Discord.Game.yml","Discord.Game.#ctor(System.String,System.String,Discord.StreamType)":"Discord.Game.yml","Discord.Game.ToString":"Discord.Game.yml","Discord.GuildUserProperties":"Discord.GuildUserProperties.yml","Discord.GuildUserProperties.Mute":"Discord.GuildUserProperties.yml","Discord.GuildUserProperties.Deaf":"Discord.GuildUserProperties.yml","Discord.GuildUserProperties.Nickname":"Discord.GuildUserProperties.yml","Discord.GuildUserProperties.Roles":"Discord.GuildUserProperties.yml","Discord.GuildUserProperties.RoleIds":"Discord.GuildUserProperties.yml","Discord.GuildUserProperties.Channel":"Discord.GuildUserProperties.yml","Discord.GuildUserProperties.ChannelId":"Discord.GuildUserProperties.yml","Discord.IConnection":"Discord.IConnection.yml","Discord.IConnection.Id":"Discord.IConnection.yml","Discord.IConnection.Type":"Discord.IConnection.yml","Discord.IConnection.Name":"Discord.IConnection.yml","Discord.IConnection.IsRevoked":"Discord.IConnection.yml","Discord.IConnection.IntegrationIds":"Discord.IConnection.yml","Discord.IGroupUser":"Discord.IGroupUser.yml","Discord.IGuildUser":"Discord.IGuildUser.yml","Discord.IGuildUser.JoinedAt":"Discord.IGuildUser.yml","Discord.IGuildUser.Nickname":"Discord.IGuildUser.yml","Discord.IGuildUser.GuildPermissions":"Discord.IGuildUser.yml","Discord.IGuildUser.Guild":"Discord.IGuildUser.yml","Discord.IGuildUser.GuildId":"Discord.IGuildUser.yml","Discord.IGuildUser.RoleIds":"Discord.IGuildUser.yml","Discord.IGuildUser.GetPermissions(Discord.IGuildChannel)":"Discord.IGuildUser.yml","Discord.IGuildUser.KickAsync(Discord.RequestOptions)":"Discord.IGuildUser.yml","Discord.IGuildUser.ModifyAsync(Action{Discord.GuildUserProperties},Discord.RequestOptions)":"Discord.IGuildUser.yml","Discord.IPresence":"Discord.IPresence.yml","Discord.IPresence.Game":"Discord.IPresence.yml","Discord.IPresence.Status":"Discord.IPresence.yml","Discord.ISelfUser":"Discord.ISelfUser.yml","Discord.ISelfUser.Email":"Discord.ISelfUser.yml","Discord.ISelfUser.IsVerified":"Discord.ISelfUser.yml","Discord.ISelfUser.IsMfaEnabled":"Discord.ISelfUser.yml","Discord.ISelfUser.ModifyAsync(Action{Discord.SelfUserProperties},Discord.RequestOptions)":"Discord.ISelfUser.yml","Discord.IUser":"Discord.IUser.yml","Discord.IUser.AvatarId":"Discord.IUser.yml","Discord.IUser.AvatarUrl":"Discord.IUser.yml","Discord.IUser.Discriminator":"Discord.IUser.yml","Discord.IUser.DiscriminatorValue":"Discord.IUser.yml","Discord.IUser.IsBot":"Discord.IUser.yml","Discord.IUser.Username":"Discord.IUser.yml","Discord.IUser.GetDMChannelAsync(Discord.CacheMode,Discord.RequestOptions)":"Discord.IUser.yml","Discord.IUser.CreateDMChannelAsync(Discord.RequestOptions)":"Discord.IUser.yml","Discord.IVoiceState":"Discord.IVoiceState.yml","Discord.IVoiceState.IsDeafened":"Discord.IVoiceState.yml","Discord.IVoiceState.IsMuted":"Discord.IVoiceState.yml","Discord.IVoiceState.IsSelfDeafened":"Discord.IVoiceState.yml","Discord.IVoiceState.IsSelfMuted":"Discord.IVoiceState.yml","Discord.IVoiceState.IsSuppressed":"Discord.IVoiceState.yml","Discord.IVoiceState.VoiceChannel":"Discord.IVoiceState.yml","Discord.IVoiceState.VoiceSessionId":"Discord.IVoiceState.yml","Discord.SelfUserProperties":"Discord.SelfUserProperties.yml","Discord.SelfUserProperties.Username":"Discord.SelfUserProperties.yml","Discord.SelfUserProperties.Avatar":"Discord.SelfUserProperties.yml","Discord.StreamType":"Discord.StreamType.yml","Discord.StreamType.NotStreaming":"Discord.StreamType.yml","Discord.StreamType.Twitch":"Discord.StreamType.yml","Discord.UserStatus":"Discord.UserStatus.yml","Discord.UserStatus.Unknown":"Discord.UserStatus.yml","Discord.UserStatus.Online":"Discord.UserStatus.yml","Discord.UserStatus.Idle":"Discord.UserStatus.yml","Discord.UserStatus.AFK":"Discord.UserStatus.yml","Discord.UserStatus.DoNotDisturb":"Discord.UserStatus.yml","Discord.UserStatus.Invisible":"Discord.UserStatus.yml","Discord.UserStatus.Offline":"Discord.UserStatus.yml","Discord.AsyncEnumerableExtensions":"Discord.AsyncEnumerableExtensions.yml","Discord.AsyncEnumerableExtensions.Flatten``1(IAsyncEnumerable{IReadOnlyCollection{``0}})":"Discord.AsyncEnumerableExtensions.yml","Discord.DiscordClientExtensions":"Discord.DiscordClientExtensions.yml","Discord.DiscordClientExtensions.GetPrivateChannelAsync(Discord.IDiscordClient,System.UInt64)":"Discord.DiscordClientExtensions.yml","Discord.DiscordClientExtensions.GetDMChannelAsync(Discord.IDiscordClient,System.UInt64)":"Discord.DiscordClientExtensions.yml","Discord.DiscordClientExtensions.GetDMChannelsAsync(Discord.IDiscordClient)":"Discord.DiscordClientExtensions.yml","Discord.DiscordClientExtensions.GetGroupChannelAsync(Discord.IDiscordClient,System.UInt64)":"Discord.DiscordClientExtensions.yml","Discord.DiscordClientExtensions.GetGroupChannelsAsync(Discord.IDiscordClient)":"Discord.DiscordClientExtensions.yml","Discord.DiscordClientExtensions.GetOptimalVoiceRegionAsync(Discord.IDiscordClient)":"Discord.DiscordClientExtensions.yml","Discord.GuildExtensions":"Discord.GuildExtensions.yml","Discord.GuildExtensions.GetTextChannelAsync(Discord.IGuild,System.UInt64)":"Discord.GuildExtensions.yml","Discord.GuildExtensions.GetTextChannelsAsync(Discord.IGuild)":"Discord.GuildExtensions.yml","Discord.GuildExtensions.GetVoiceChannelAsync(Discord.IGuild,System.UInt64)":"Discord.GuildExtensions.yml","Discord.GuildExtensions.GetVoiceChannelsAsync(Discord.IGuild)":"Discord.GuildExtensions.yml","Discord.GuildExtensions.GetAFKChannelAsync(Discord.IGuild)":"Discord.GuildExtensions.yml","Discord.GuildExtensions.GetDefaultChannelAsync(Discord.IGuild)":"Discord.GuildExtensions.yml","Discord.GuildExtensions.GetEmbedChannelAsync(Discord.IGuild)":"Discord.GuildExtensions.yml","Discord.GuildExtensions.GetOwnerAsync(Discord.IGuild)":"Discord.GuildExtensions.yml","Discord.GuildUserExtensions":"Discord.GuildUserExtensions.yml","Discord.GuildUserExtensions.AddRolesAsync(Discord.IGuildUser,Discord.IRole[])":"Discord.GuildUserExtensions.yml","Discord.GuildUserExtensions.AddRolesAsync(Discord.IGuildUser,IEnumerable{Discord.IRole})":"Discord.GuildUserExtensions.yml","Discord.GuildUserExtensions.RemoveRolesAsync(Discord.IGuildUser,Discord.IRole[])":"Discord.GuildUserExtensions.yml","Discord.GuildUserExtensions.RemoveRolesAsync(Discord.IGuildUser,IEnumerable{Discord.IRole})":"Discord.GuildUserExtensions.yml","Discord.GuildUserExtensions.ChangeRolesAsync(Discord.IGuildUser,IEnumerable{Discord.IRole},IEnumerable{Discord.IRole})":"Discord.GuildUserExtensions.yml","Discord.LogMessage":"Discord.LogMessage.yml","Discord.LogMessage.Severity":"Discord.LogMessage.yml","Discord.LogMessage.Source":"Discord.LogMessage.yml","Discord.LogMessage.Message":"Discord.LogMessage.yml","Discord.LogMessage.Exception":"Discord.LogMessage.yml","Discord.LogMessage.#ctor(Discord.LogSeverity,System.String,System.String,Exception)":"Discord.LogMessage.yml","Discord.LogMessage.ToString":"Discord.LogMessage.yml","Discord.LogMessage.ToString(StringBuilder,System.Boolean,System.Boolean,DateTimeKind,System.Nullable{System.Int32})":"Discord.LogMessage.yml","Discord.LogSeverity":"Discord.LogSeverity.yml","Discord.LogSeverity.Critical":"Discord.LogSeverity.yml","Discord.LogSeverity.Error":"Discord.LogSeverity.yml","Discord.LogSeverity.Warning":"Discord.LogSeverity.yml","Discord.LogSeverity.Info":"Discord.LogSeverity.yml","Discord.LogSeverity.Verbose":"Discord.LogSeverity.yml","Discord.LogSeverity.Debug":"Discord.LogSeverity.yml","Discord.RpcException":"Discord.RpcException.yml","Discord.RpcException.ErrorCode":"Discord.RpcException.yml","Discord.RpcException.Reason":"Discord.RpcException.yml","Discord.RpcException.#ctor(System.Int32,System.String)":"Discord.RpcException.yml","Discord.MentionUtils":"Discord.MentionUtils.yml","Discord.MentionUtils.MentionUser(System.UInt64)":"Discord.MentionUtils.yml","Discord.MentionUtils.MentionChannel(System.UInt64)":"Discord.MentionUtils.yml","Discord.MentionUtils.MentionRole(System.UInt64)":"Discord.MentionUtils.yml","Discord.MentionUtils.ParseUser(System.String)":"Discord.MentionUtils.yml","Discord.MentionUtils.TryParseUser(System.String,System.UInt64@)":"Discord.MentionUtils.yml","Discord.MentionUtils.ParseChannel(System.String)":"Discord.MentionUtils.yml","Discord.MentionUtils.TryParseChannel(System.String,System.UInt64@)":"Discord.MentionUtils.yml","Discord.MentionUtils.ParseRole(System.String)":"Discord.MentionUtils.yml","Discord.MentionUtils.TryParseRole(System.String,System.UInt64@)":"Discord.MentionUtils.yml","Discord.Optional`1":"Discord.Optional-1.yml","Discord.Optional`1.Unspecified":"Discord.Optional-1.yml","Discord.Optional`1.Value":"Discord.Optional-1.yml","Discord.Optional`1.IsSpecified":"Discord.Optional-1.yml","Discord.Optional`1.#ctor(`0)":"Discord.Optional-1.yml","Discord.Optional`1.GetValueOrDefault":"Discord.Optional-1.yml","Discord.Optional`1.GetValueOrDefault(`0)":"Discord.Optional-1.yml","Discord.Optional`1.Equals(System.Object)":"Discord.Optional-1.yml","Discord.Optional`1.GetHashCode":"Discord.Optional-1.yml","Discord.Optional`1.ToString":"Discord.Optional-1.yml","Discord.Optional`1.op_Implicit(`0)~Discord.Optional{`0}":"Discord.Optional-1.yml","Discord.Optional`1.op_Explicit(Discord.Optional{`0})~`0":"Discord.Optional-1.yml","Discord.Optional":"Discord.Optional.yml","Discord.Optional.Create``1":"Discord.Optional.yml","Discord.Optional.Create``1(``0)":"Discord.Optional.yml","Discord.ChannelType":"Discord.ChannelType.yml","Discord.ChannelType.Text":"Discord.ChannelType.yml","Discord.ChannelType.DM":"Discord.ChannelType.yml","Discord.ChannelType.Voice":"Discord.ChannelType.yml","Discord.ChannelType.Group":"Discord.ChannelType.yml","Discord.RestGuildEmbed":"Discord.RestGuildEmbed.yml","Discord.RestGuildEmbed.IsEnabled":"Discord.RestGuildEmbed.yml","Discord.RestGuildEmbed.ChannelId":"Discord.RestGuildEmbed.yml","Discord.RestGuildEmbed.ToString":"Discord.RestGuildEmbed.yml","Discord.RestVoiceRegion":"Discord.RestVoiceRegion.yml","Discord.RestVoiceRegion.Name":"Discord.RestVoiceRegion.yml","Discord.RestVoiceRegion.IsVip":"Discord.RestVoiceRegion.yml","Discord.RestVoiceRegion.IsOptimal":"Discord.RestVoiceRegion.yml","Discord.RestVoiceRegion.SampleHostname":"Discord.RestVoiceRegion.yml","Discord.RestVoiceRegion.SamplePort":"Discord.RestVoiceRegion.yml","Discord.RestVoiceRegion.ToString":"Discord.RestVoiceRegion.yml","Discord.Attachment":"Discord.Attachment.yml","Discord.Attachment.Id":"Discord.Attachment.yml","Discord.Attachment.Filename":"Discord.Attachment.yml","Discord.Attachment.Url":"Discord.Attachment.yml","Discord.Attachment.ProxyUrl":"Discord.Attachment.yml","Discord.Attachment.Size":"Discord.Attachment.yml","Discord.Attachment.Height":"Discord.Attachment.yml","Discord.Attachment.Width":"Discord.Attachment.yml","Discord.Attachment.ToString":"Discord.Attachment.yml","Discord.EmbedBuilder":"Discord.EmbedBuilder.yml","Discord.EmbedBuilder.#ctor":"Discord.EmbedBuilder.yml","Discord.EmbedBuilder.Title":"Discord.EmbedBuilder.yml","Discord.EmbedBuilder.Description":"Discord.EmbedBuilder.yml","Discord.EmbedBuilder.Url":"Discord.EmbedBuilder.yml","Discord.EmbedBuilder.ThumbnailUrl":"Discord.EmbedBuilder.yml","Discord.EmbedBuilder.ImageUrl":"Discord.EmbedBuilder.yml","Discord.EmbedBuilder.Timestamp":"Discord.EmbedBuilder.yml","Discord.EmbedBuilder.Color":"Discord.EmbedBuilder.yml","Discord.EmbedBuilder.Author":"Discord.EmbedBuilder.yml","Discord.EmbedBuilder.Footer":"Discord.EmbedBuilder.yml","Discord.EmbedBuilder.WithTitle(System.String)":"Discord.EmbedBuilder.yml","Discord.EmbedBuilder.WithDescription(System.String)":"Discord.EmbedBuilder.yml","Discord.EmbedBuilder.WithUrl(System.String)":"Discord.EmbedBuilder.yml","Discord.EmbedBuilder.WithThumbnailUrl(System.String)":"Discord.EmbedBuilder.yml","Discord.EmbedBuilder.WithImageUrl(System.String)":"Discord.EmbedBuilder.yml","Discord.EmbedBuilder.WithCurrentTimestamp":"Discord.EmbedBuilder.yml","Discord.EmbedBuilder.WithTimestamp(DateTimeOffset)":"Discord.EmbedBuilder.yml","Discord.EmbedBuilder.WithColor(Color)":"Discord.EmbedBuilder.yml","Discord.EmbedBuilder.WithAuthor(Discord.EmbedAuthorBuilder)":"Discord.EmbedBuilder.yml","Discord.EmbedBuilder.WithAuthor(Action{Discord.EmbedAuthorBuilder})":"Discord.EmbedBuilder.yml","Discord.EmbedBuilder.WithFooter(Discord.EmbedFooterBuilder)":"Discord.EmbedBuilder.yml","Discord.EmbedBuilder.WithFooter(Action{Discord.EmbedFooterBuilder})":"Discord.EmbedBuilder.yml","Discord.EmbedBuilder.AddField(Action{Discord.EmbedFieldBuilder})":"Discord.EmbedBuilder.yml","Discord.EmbedBuilder.Build":"Discord.EmbedBuilder.yml","Discord.EmbedBuilder.op_Implicit(Discord.EmbedBuilder)~Embed":"Discord.EmbedBuilder.yml","Discord.EmbedFieldBuilder":"Discord.EmbedFieldBuilder.yml","Discord.EmbedFieldBuilder.Name":"Discord.EmbedFieldBuilder.yml","Discord.EmbedFieldBuilder.Value":"Discord.EmbedFieldBuilder.yml","Discord.EmbedFieldBuilder.IsInline":"Discord.EmbedFieldBuilder.yml","Discord.EmbedFieldBuilder.#ctor":"Discord.EmbedFieldBuilder.yml","Discord.EmbedFieldBuilder.WithName(System.String)":"Discord.EmbedFieldBuilder.yml","Discord.EmbedFieldBuilder.WithValue(System.String)":"Discord.EmbedFieldBuilder.yml","Discord.EmbedFieldBuilder.WithIsInline(System.Boolean)":"Discord.EmbedFieldBuilder.yml","Discord.EmbedFieldBuilder.Build":"Discord.EmbedFieldBuilder.yml","Discord.EmbedAuthorBuilder":"Discord.EmbedAuthorBuilder.yml","Discord.EmbedAuthorBuilder.Name":"Discord.EmbedAuthorBuilder.yml","Discord.EmbedAuthorBuilder.Url":"Discord.EmbedAuthorBuilder.yml","Discord.EmbedAuthorBuilder.IconUrl":"Discord.EmbedAuthorBuilder.yml","Discord.EmbedAuthorBuilder.#ctor":"Discord.EmbedAuthorBuilder.yml","Discord.EmbedAuthorBuilder.WithName(System.String)":"Discord.EmbedAuthorBuilder.yml","Discord.EmbedAuthorBuilder.WithUrl(System.String)":"Discord.EmbedAuthorBuilder.yml","Discord.EmbedAuthorBuilder.WithIconUrl(System.String)":"Discord.EmbedAuthorBuilder.yml","Discord.EmbedAuthorBuilder.Build":"Discord.EmbedAuthorBuilder.yml","Discord.EmbedFooterBuilder":"Discord.EmbedFooterBuilder.yml","Discord.EmbedFooterBuilder.Text":"Discord.EmbedFooterBuilder.yml","Discord.EmbedFooterBuilder.IconUrl":"Discord.EmbedFooterBuilder.yml","Discord.EmbedFooterBuilder.#ctor":"Discord.EmbedFooterBuilder.yml","Discord.EmbedFooterBuilder.WithText(System.String)":"Discord.EmbedFooterBuilder.yml","Discord.EmbedFooterBuilder.WithIconUrl(System.String)":"Discord.EmbedFooterBuilder.yml","Discord.EmbedFooterBuilder.Build":"Discord.EmbedFooterBuilder.yml","Discord.RestConnection":"Discord.RestConnection.yml","Discord.RestConnection.Id":"Discord.RestConnection.yml","Discord.RestConnection.Type":"Discord.RestConnection.yml","Discord.RestConnection.Name":"Discord.RestConnection.yml","Discord.RestConnection.IsRevoked":"Discord.RestConnection.yml","Discord.RestConnection.IntegrationIds":"Discord.RestConnection.yml","Discord.RestConnection.ToString":"Discord.RestConnection.yml","Discord.Rest":"Discord.Rest.yml","Discord.Rest.BaseDiscordClient":"Discord.Rest.BaseDiscordClient.yml","Discord.Rest.BaseDiscordClient.Log":"Discord.Rest.BaseDiscordClient.yml","Discord.Rest.BaseDiscordClient.LoggedIn":"Discord.Rest.BaseDiscordClient.yml","Discord.Rest.BaseDiscordClient.LoggedOut":"Discord.Rest.BaseDiscordClient.yml","Discord.Rest.BaseDiscordClient.LoginState":"Discord.Rest.BaseDiscordClient.yml","Discord.Rest.BaseDiscordClient.CurrentUser":"Discord.Rest.BaseDiscordClient.yml","Discord.Rest.BaseDiscordClient.LoginAsync(TokenType,System.String,System.Boolean)":"Discord.Rest.BaseDiscordClient.yml","Discord.Rest.BaseDiscordClient.OnLoginAsync(TokenType,System.String)":"Discord.Rest.BaseDiscordClient.yml","Discord.Rest.BaseDiscordClient.LogoutAsync":"Discord.Rest.BaseDiscordClient.yml","Discord.Rest.BaseDiscordClient.OnLogoutAsync":"Discord.Rest.BaseDiscordClient.yml","Discord.Rest.BaseDiscordClient.Dispose":"Discord.Rest.BaseDiscordClient.yml","Discord.Rest.DiscordRestClient":"Discord.Rest.DiscordRestClient.yml","Discord.Rest.DiscordRestClient.CurrentUser":"Discord.Rest.DiscordRestClient.yml","Discord.Rest.DiscordRestClient.#ctor":"Discord.Rest.DiscordRestClient.yml","Discord.Rest.DiscordRestClient.#ctor(Discord.Rest.DiscordRestConfig)":"Discord.Rest.DiscordRestClient.yml","Discord.Rest.DiscordRestClient.OnLoginAsync(TokenType,System.String)":"Discord.Rest.DiscordRestClient.yml","Discord.Rest.DiscordRestClient.OnLogoutAsync":"Discord.Rest.DiscordRestClient.yml","Discord.Rest.DiscordRestClient.GetApplicationInfoAsync":"Discord.Rest.DiscordRestClient.yml","Discord.Rest.DiscordRestClient.GetChannelAsync(System.UInt64)":"Discord.Rest.DiscordRestClient.yml","Discord.Rest.DiscordRestClient.GetPrivateChannelsAsync":"Discord.Rest.DiscordRestClient.yml","Discord.Rest.DiscordRestClient.GetConnectionsAsync":"Discord.Rest.DiscordRestClient.yml","Discord.Rest.DiscordRestClient.GetInviteAsync(System.String)":"Discord.Rest.DiscordRestClient.yml","Discord.Rest.DiscordRestClient.GetGuildAsync(System.UInt64)":"Discord.Rest.DiscordRestClient.yml","Discord.Rest.DiscordRestClient.GetGuildEmbedAsync(System.UInt64)":"Discord.Rest.DiscordRestClient.yml","Discord.Rest.DiscordRestClient.GetGuildSummariesAsync":"Discord.Rest.DiscordRestClient.yml","Discord.Rest.DiscordRestClient.GetGuildsAsync":"Discord.Rest.DiscordRestClient.yml","Discord.Rest.DiscordRestClient.CreateGuildAsync(System.String,IVoiceRegion,Stream)":"Discord.Rest.DiscordRestClient.yml","Discord.Rest.DiscordRestClient.GetUserAsync(System.UInt64)":"Discord.Rest.DiscordRestClient.yml","Discord.Rest.DiscordRestClient.GetGuildUserAsync(System.UInt64,System.UInt64)":"Discord.Rest.DiscordRestClient.yml","Discord.Rest.DiscordRestClient.GetVoiceRegionsAsync":"Discord.Rest.DiscordRestClient.yml","Discord.Rest.DiscordRestClient.GetVoiceRegionAsync(System.String)":"Discord.Rest.DiscordRestClient.yml","Discord.Rest.DiscordRestConfig":"Discord.Rest.DiscordRestConfig.yml","Discord.Rest.DiscordRestConfig.UserAgent":"Discord.Rest.DiscordRestConfig.yml","Discord.Rest.DiscordRestConfig.RestClientProvider":"Discord.Rest.DiscordRestConfig.yml","Discord.Rest.RestApplication":"Discord.Rest.RestApplication.yml","Discord.Rest.RestApplication._iconId":"Discord.Rest.RestApplication.yml","Discord.Rest.RestApplication.Name":"Discord.Rest.RestApplication.yml","Discord.Rest.RestApplication.Description":"Discord.Rest.RestApplication.yml","Discord.Rest.RestApplication.RPCOrigins":"Discord.Rest.RestApplication.yml","Discord.Rest.RestApplication.Flags":"Discord.Rest.RestApplication.yml","Discord.Rest.RestApplication.Owner":"Discord.Rest.RestApplication.yml","Discord.Rest.RestApplication.CreatedAt":"Discord.Rest.RestApplication.yml","Discord.Rest.RestApplication.IconUrl":"Discord.Rest.RestApplication.yml","Discord.Rest.RestApplication.UpdateAsync":"Discord.Rest.RestApplication.yml","Discord.Rest.RestApplication.ToString":"Discord.Rest.RestApplication.yml","Discord.Rest.RestEntity`1":"Discord.Rest.RestEntity-1.yml","Discord.Rest.RestEntity`1.Discord":"Discord.Rest.RestEntity-1.yml","Discord.Rest.RestEntity`1.Id":"Discord.Rest.RestEntity-1.yml","Discord.Rest.IRestAudioChannel":"Discord.Rest.IRestAudioChannel.yml","Discord.Rest.IRestMessageChannel":"Discord.Rest.IRestMessageChannel.yml","Discord.Rest.IRestMessageChannel.SendMessageAsync(System.String,System.Boolean,Embed,RequestOptions)":"Discord.Rest.IRestMessageChannel.yml","Discord.Rest.IRestMessageChannel.SendFileAsync(Stream,System.String,System.String,System.Boolean,RequestOptions)":"Discord.Rest.IRestMessageChannel.yml","Discord.Rest.IRestMessageChannel.GetMessageAsync(System.UInt64,RequestOptions)":"Discord.Rest.IRestMessageChannel.yml","Discord.Rest.IRestMessageChannel.GetMessagesAsync(System.Int32,RequestOptions)":"Discord.Rest.IRestMessageChannel.yml","Discord.Rest.IRestMessageChannel.GetMessagesAsync(System.UInt64,Direction,System.Int32,RequestOptions)":"Discord.Rest.IRestMessageChannel.yml","Discord.Rest.IRestMessageChannel.GetMessagesAsync(IMessage,Direction,System.Int32,RequestOptions)":"Discord.Rest.IRestMessageChannel.yml","Discord.Rest.IRestMessageChannel.GetPinnedMessagesAsync(RequestOptions)":"Discord.Rest.IRestMessageChannel.yml","Discord.Rest.IRestPrivateChannel":"Discord.Rest.IRestPrivateChannel.yml","Discord.Rest.IRestPrivateChannel.Recipients":"Discord.Rest.IRestPrivateChannel.yml","Discord.Rest.RestChannel":"Discord.Rest.RestChannel.yml","Discord.Rest.RestChannel.CreatedAt":"Discord.Rest.RestChannel.yml","Discord.Rest.RestChannel.UpdateAsync(RequestOptions)":"Discord.Rest.RestChannel.yml","Discord.Rest.RestDMChannel":"Discord.Rest.RestDMChannel.yml","Discord.Rest.RestDMChannel.CurrentUser":"Discord.Rest.RestDMChannel.yml","Discord.Rest.RestDMChannel.Recipient":"Discord.Rest.RestDMChannel.yml","Discord.Rest.RestDMChannel.Users":"Discord.Rest.RestDMChannel.yml","Discord.Rest.RestDMChannel.UpdateAsync(RequestOptions)":"Discord.Rest.RestDMChannel.yml","Discord.Rest.RestDMChannel.CloseAsync(RequestOptions)":"Discord.Rest.RestDMChannel.yml","Discord.Rest.RestDMChannel.GetUser(System.UInt64)":"Discord.Rest.RestDMChannel.yml","Discord.Rest.RestDMChannel.GetMessageAsync(System.UInt64,RequestOptions)":"Discord.Rest.RestDMChannel.yml","Discord.Rest.RestDMChannel.GetMessagesAsync(System.Int32,RequestOptions)":"Discord.Rest.RestDMChannel.yml","Discord.Rest.RestDMChannel.GetMessagesAsync(System.UInt64,Direction,System.Int32,RequestOptions)":"Discord.Rest.RestDMChannel.yml","Discord.Rest.RestDMChannel.GetMessagesAsync(IMessage,Direction,System.Int32,RequestOptions)":"Discord.Rest.RestDMChannel.yml","Discord.Rest.RestDMChannel.GetPinnedMessagesAsync(RequestOptions)":"Discord.Rest.RestDMChannel.yml","Discord.Rest.RestDMChannel.SendMessageAsync(System.String,System.Boolean,Embed,RequestOptions)":"Discord.Rest.RestDMChannel.yml","Discord.Rest.RestDMChannel.SendFileAsync(Stream,System.String,System.String,System.Boolean,RequestOptions)":"Discord.Rest.RestDMChannel.yml","Discord.Rest.RestDMChannel.DeleteMessagesAsync(IEnumerable{IMessage},RequestOptions)":"Discord.Rest.RestDMChannel.yml","Discord.Rest.RestDMChannel.TriggerTypingAsync(RequestOptions)":"Discord.Rest.RestDMChannel.yml","Discord.Rest.RestDMChannel.EnterTypingState(RequestOptions)":"Discord.Rest.RestDMChannel.yml","Discord.Rest.RestDMChannel.ToString":"Discord.Rest.RestDMChannel.yml","Discord.Rest.RestDMChannel.Discord#Rest#IRestPrivateChannel#Recipients":"Discord.Rest.RestDMChannel.yml","Discord.Rest.RestGroupChannel":"Discord.Rest.RestGroupChannel.yml","Discord.Rest.RestGroupChannel.Name":"Discord.Rest.RestGroupChannel.yml","Discord.Rest.RestGroupChannel.Users":"Discord.Rest.RestGroupChannel.yml","Discord.Rest.RestGroupChannel.Recipients":"Discord.Rest.RestGroupChannel.yml","Discord.Rest.RestGroupChannel.UpdateAsync(RequestOptions)":"Discord.Rest.RestGroupChannel.yml","Discord.Rest.RestGroupChannel.LeaveAsync(RequestOptions)":"Discord.Rest.RestGroupChannel.yml","Discord.Rest.RestGroupChannel.GetUser(System.UInt64)":"Discord.Rest.RestGroupChannel.yml","Discord.Rest.RestGroupChannel.GetMessageAsync(System.UInt64,RequestOptions)":"Discord.Rest.RestGroupChannel.yml","Discord.Rest.RestGroupChannel.GetMessagesAsync(System.Int32,RequestOptions)":"Discord.Rest.RestGroupChannel.yml","Discord.Rest.RestGroupChannel.GetMessagesAsync(System.UInt64,Direction,System.Int32,RequestOptions)":"Discord.Rest.RestGroupChannel.yml","Discord.Rest.RestGroupChannel.GetMessagesAsync(IMessage,Direction,System.Int32,RequestOptions)":"Discord.Rest.RestGroupChannel.yml","Discord.Rest.RestGroupChannel.GetPinnedMessagesAsync(RequestOptions)":"Discord.Rest.RestGroupChannel.yml","Discord.Rest.RestGroupChannel.SendMessageAsync(System.String,System.Boolean,Embed,RequestOptions)":"Discord.Rest.RestGroupChannel.yml","Discord.Rest.RestGroupChannel.SendFileAsync(Stream,System.String,System.String,System.Boolean,RequestOptions)":"Discord.Rest.RestGroupChannel.yml","Discord.Rest.RestGroupChannel.DeleteMessagesAsync(IEnumerable{IMessage},RequestOptions)":"Discord.Rest.RestGroupChannel.yml","Discord.Rest.RestGroupChannel.TriggerTypingAsync(RequestOptions)":"Discord.Rest.RestGroupChannel.yml","Discord.Rest.RestGroupChannel.EnterTypingState(RequestOptions)":"Discord.Rest.RestGroupChannel.yml","Discord.Rest.RestGroupChannel.ToString":"Discord.Rest.RestGroupChannel.yml","Discord.Rest.RestGroupChannel.Discord#Rest#IRestPrivateChannel#Recipients":"Discord.Rest.RestGroupChannel.yml","Discord.Rest.RestGuildChannel":"Discord.Rest.RestGuildChannel.yml","Discord.Rest.RestGuildChannel.PermissionOverwrites":"Discord.Rest.RestGuildChannel.yml","Discord.Rest.RestGuildChannel.Name":"Discord.Rest.RestGuildChannel.yml","Discord.Rest.RestGuildChannel.Position":"Discord.Rest.RestGuildChannel.yml","Discord.Rest.RestGuildChannel.GuildId":"Discord.Rest.RestGuildChannel.yml","Discord.Rest.RestGuildChannel.UpdateAsync(RequestOptions)":"Discord.Rest.RestGuildChannel.yml","Discord.Rest.RestGuildChannel.ModifyAsync(Action{GuildChannelProperties},RequestOptions)":"Discord.Rest.RestGuildChannel.yml","Discord.Rest.RestGuildChannel.DeleteAsync(RequestOptions)":"Discord.Rest.RestGuildChannel.yml","Discord.Rest.RestGuildChannel.GetPermissionOverwrite(IUser)":"Discord.Rest.RestGuildChannel.yml","Discord.Rest.RestGuildChannel.GetPermissionOverwrite(IRole)":"Discord.Rest.RestGuildChannel.yml","Discord.Rest.RestGuildChannel.AddPermissionOverwriteAsync(IUser,OverwritePermissions,RequestOptions)":"Discord.Rest.RestGuildChannel.yml","Discord.Rest.RestGuildChannel.AddPermissionOverwriteAsync(IRole,OverwritePermissions,RequestOptions)":"Discord.Rest.RestGuildChannel.yml","Discord.Rest.RestGuildChannel.RemovePermissionOverwriteAsync(IUser,RequestOptions)":"Discord.Rest.RestGuildChannel.yml","Discord.Rest.RestGuildChannel.RemovePermissionOverwriteAsync(IRole,RequestOptions)":"Discord.Rest.RestGuildChannel.yml","Discord.Rest.RestGuildChannel.GetInvitesAsync(RequestOptions)":"Discord.Rest.RestGuildChannel.yml","Discord.Rest.RestGuildChannel.CreateInviteAsync(System.Nullable{System.Int32},System.Nullable{System.Int32},System.Boolean,RequestOptions)":"Discord.Rest.RestGuildChannel.yml","Discord.Rest.RestGuildChannel.ToString":"Discord.Rest.RestGuildChannel.yml","Discord.Rest.RestTextChannel":"Discord.Rest.RestTextChannel.yml","Discord.Rest.RestTextChannel.Topic":"Discord.Rest.RestTextChannel.yml","Discord.Rest.RestTextChannel.Mention":"Discord.Rest.RestTextChannel.yml","Discord.Rest.RestTextChannel.ModifyAsync(Action{TextChannelProperties},RequestOptions)":"Discord.Rest.RestTextChannel.yml","Discord.Rest.RestTextChannel.GetUserAsync(System.UInt64,RequestOptions)":"Discord.Rest.RestTextChannel.yml","Discord.Rest.RestTextChannel.GetUsersAsync(RequestOptions)":"Discord.Rest.RestTextChannel.yml","Discord.Rest.RestTextChannel.GetMessageAsync(System.UInt64,RequestOptions)":"Discord.Rest.RestTextChannel.yml","Discord.Rest.RestTextChannel.GetMessagesAsync(System.Int32,RequestOptions)":"Discord.Rest.RestTextChannel.yml","Discord.Rest.RestTextChannel.GetMessagesAsync(System.UInt64,Direction,System.Int32,RequestOptions)":"Discord.Rest.RestTextChannel.yml","Discord.Rest.RestTextChannel.GetMessagesAsync(IMessage,Direction,System.Int32,RequestOptions)":"Discord.Rest.RestTextChannel.yml","Discord.Rest.RestTextChannel.GetPinnedMessagesAsync(RequestOptions)":"Discord.Rest.RestTextChannel.yml","Discord.Rest.RestTextChannel.SendMessageAsync(System.String,System.Boolean,Embed,RequestOptions)":"Discord.Rest.RestTextChannel.yml","Discord.Rest.RestTextChannel.SendFileAsync(Stream,System.String,System.String,System.Boolean,RequestOptions)":"Discord.Rest.RestTextChannel.yml","Discord.Rest.RestTextChannel.DeleteMessagesAsync(IEnumerable{IMessage},RequestOptions)":"Discord.Rest.RestTextChannel.yml","Discord.Rest.RestTextChannel.TriggerTypingAsync(RequestOptions)":"Discord.Rest.RestTextChannel.yml","Discord.Rest.RestTextChannel.EnterTypingState(RequestOptions)":"Discord.Rest.RestTextChannel.yml","Discord.Rest.RestVoiceChannel":"Discord.Rest.RestVoiceChannel.yml","Discord.Rest.RestVoiceChannel.Bitrate":"Discord.Rest.RestVoiceChannel.yml","Discord.Rest.RestVoiceChannel.UserLimit":"Discord.Rest.RestVoiceChannel.yml","Discord.Rest.RestVoiceChannel.ModifyAsync(Action{VoiceChannelProperties},RequestOptions)":"Discord.Rest.RestVoiceChannel.yml","Discord.Rest.RestBan":"Discord.Rest.RestBan.yml","Discord.Rest.RestBan.User":"Discord.Rest.RestBan.yml","Discord.Rest.RestBan.Reason":"Discord.Rest.RestBan.yml","Discord.Rest.RestBan.ToString":"Discord.Rest.RestBan.yml","Discord.Rest.RestGuild":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.Name":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.AFKTimeout":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.IsEmbeddable":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.VerificationLevel":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.MfaLevel":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.DefaultMessageNotifications":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.AFKChannelId":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.EmbedChannelId":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.OwnerId":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.VoiceRegionId":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.IconId":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.SplashId":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.CreatedAt":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.DefaultChannelId":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.IconUrl":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.SplashUrl":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.EveryoneRole":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.Roles":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.Emojis":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.Features":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.UpdateAsync(RequestOptions)":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.DeleteAsync(RequestOptions)":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.ModifyAsync(Action{GuildProperties},RequestOptions)":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.ModifyEmbedAsync(Action{GuildEmbedProperties},RequestOptions)":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.ModifyChannelsAsync(IEnumerable{BulkGuildChannelProperties},RequestOptions)":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.ModifyRolesAsync(IEnumerable{BulkRoleProperties},RequestOptions)":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.LeaveAsync(RequestOptions)":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.GetBansAsync(RequestOptions)":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.AddBanAsync(IUser,System.Int32,RequestOptions)":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.AddBanAsync(System.UInt64,System.Int32,RequestOptions)":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.RemoveBanAsync(IUser,RequestOptions)":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.RemoveBanAsync(System.UInt64,RequestOptions)":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.GetChannelsAsync(RequestOptions)":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.GetChannelAsync(System.UInt64,RequestOptions)":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.CreateTextChannelAsync(System.String,RequestOptions)":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.CreateVoiceChannelAsync(System.String,RequestOptions)":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.GetIntegrationsAsync(RequestOptions)":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.CreateIntegrationAsync(System.UInt64,System.String,RequestOptions)":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.GetInvitesAsync(RequestOptions)":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.GetRole(System.UInt64)":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.CreateRoleAsync(System.String,System.Nullable{GuildPermissions},System.Nullable{Color},System.Boolean,RequestOptions)":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.GetUsersAsync(RequestOptions)":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.GetUserAsync(System.UInt64,RequestOptions)":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.GetCurrentUserAsync(RequestOptions)":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.PruneUsersAsync(System.Int32,System.Boolean,RequestOptions)":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuild.ToString":"Discord.Rest.RestGuild.yml","Discord.Rest.RestGuildIntegration":"Discord.Rest.RestGuildIntegration.yml","Discord.Rest.RestGuildIntegration.Name":"Discord.Rest.RestGuildIntegration.yml","Discord.Rest.RestGuildIntegration.Type":"Discord.Rest.RestGuildIntegration.yml","Discord.Rest.RestGuildIntegration.IsEnabled":"Discord.Rest.RestGuildIntegration.yml","Discord.Rest.RestGuildIntegration.IsSyncing":"Discord.Rest.RestGuildIntegration.yml","Discord.Rest.RestGuildIntegration.ExpireBehavior":"Discord.Rest.RestGuildIntegration.yml","Discord.Rest.RestGuildIntegration.ExpireGracePeriod":"Discord.Rest.RestGuildIntegration.yml","Discord.Rest.RestGuildIntegration.GuildId":"Discord.Rest.RestGuildIntegration.yml","Discord.Rest.RestGuildIntegration.RoleId":"Discord.Rest.RestGuildIntegration.yml","Discord.Rest.RestGuildIntegration.User":"Discord.Rest.RestGuildIntegration.yml","Discord.Rest.RestGuildIntegration.Account":"Discord.Rest.RestGuildIntegration.yml","Discord.Rest.RestGuildIntegration.SyncedAt":"Discord.Rest.RestGuildIntegration.yml","Discord.Rest.RestGuildIntegration.DeleteAsync":"Discord.Rest.RestGuildIntegration.yml","Discord.Rest.RestGuildIntegration.ModifyAsync(Action{GuildIntegrationProperties})":"Discord.Rest.RestGuildIntegration.yml","Discord.Rest.RestGuildIntegration.SyncAsync":"Discord.Rest.RestGuildIntegration.yml","Discord.Rest.RestGuildIntegration.ToString":"Discord.Rest.RestGuildIntegration.yml","Discord.Rest.RestUserGuild":"Discord.Rest.RestUserGuild.yml","Discord.Rest.RestUserGuild.Name":"Discord.Rest.RestUserGuild.yml","Discord.Rest.RestUserGuild.IsOwner":"Discord.Rest.RestUserGuild.yml","Discord.Rest.RestUserGuild.Permissions":"Discord.Rest.RestUserGuild.yml","Discord.Rest.RestUserGuild.CreatedAt":"Discord.Rest.RestUserGuild.yml","Discord.Rest.RestUserGuild.IconUrl":"Discord.Rest.RestUserGuild.yml","Discord.Rest.RestUserGuild.LeaveAsync(RequestOptions)":"Discord.Rest.RestUserGuild.yml","Discord.Rest.RestUserGuild.DeleteAsync(RequestOptions)":"Discord.Rest.RestUserGuild.yml","Discord.Rest.RestUserGuild.ToString":"Discord.Rest.RestUserGuild.yml","Discord.Rest.RestInvite":"Discord.Rest.RestInvite.yml","Discord.Rest.RestInvite.ChannelName":"Discord.Rest.RestInvite.yml","Discord.Rest.RestInvite.GuildName":"Discord.Rest.RestInvite.yml","Discord.Rest.RestInvite.ChannelId":"Discord.Rest.RestInvite.yml","Discord.Rest.RestInvite.GuildId":"Discord.Rest.RestInvite.yml","Discord.Rest.RestInvite.Code":"Discord.Rest.RestInvite.yml","Discord.Rest.RestInvite.Url":"Discord.Rest.RestInvite.yml","Discord.Rest.RestInvite.UpdateAsync(RequestOptions)":"Discord.Rest.RestInvite.yml","Discord.Rest.RestInvite.DeleteAsync(RequestOptions)":"Discord.Rest.RestInvite.yml","Discord.Rest.RestInvite.AcceptAsync(RequestOptions)":"Discord.Rest.RestInvite.yml","Discord.Rest.RestInvite.ToString":"Discord.Rest.RestInvite.yml","Discord.Rest.RestInviteMetadata":"Discord.Rest.RestInviteMetadata.yml","Discord.Rest.RestInviteMetadata.IsRevoked":"Discord.Rest.RestInviteMetadata.yml","Discord.Rest.RestInviteMetadata.IsTemporary":"Discord.Rest.RestInviteMetadata.yml","Discord.Rest.RestInviteMetadata.MaxAge":"Discord.Rest.RestInviteMetadata.yml","Discord.Rest.RestInviteMetadata.MaxUses":"Discord.Rest.RestInviteMetadata.yml","Discord.Rest.RestInviteMetadata.Uses":"Discord.Rest.RestInviteMetadata.yml","Discord.Rest.RestInviteMetadata.Inviter":"Discord.Rest.RestInviteMetadata.yml","Discord.Rest.RestInviteMetadata.CreatedAt":"Discord.Rest.RestInviteMetadata.yml","Discord.Rest.RestMessage":"Discord.Rest.RestMessage.yml","Discord.Rest.RestMessage.Channel":"Discord.Rest.RestMessage.yml","Discord.Rest.RestMessage.Author":"Discord.Rest.RestMessage.yml","Discord.Rest.RestMessage.Content":"Discord.Rest.RestMessage.yml","Discord.Rest.RestMessage.CreatedAt":"Discord.Rest.RestMessage.yml","Discord.Rest.RestMessage.IsTTS":"Discord.Rest.RestMessage.yml","Discord.Rest.RestMessage.IsPinned":"Discord.Rest.RestMessage.yml","Discord.Rest.RestMessage.EditedTimestamp":"Discord.Rest.RestMessage.yml","Discord.Rest.RestMessage.Attachments":"Discord.Rest.RestMessage.yml","Discord.Rest.RestMessage.Embeds":"Discord.Rest.RestMessage.yml","Discord.Rest.RestMessage.MentionedChannelIds":"Discord.Rest.RestMessage.yml","Discord.Rest.RestMessage.MentionedRoleIds":"Discord.Rest.RestMessage.yml","Discord.Rest.RestMessage.MentionedUsers":"Discord.Rest.RestMessage.yml","Discord.Rest.RestMessage.Tags":"Discord.Rest.RestMessage.yml","Discord.Rest.RestMessage.WebhookId":"Discord.Rest.RestMessage.yml","Discord.Rest.RestMessage.IsWebhook":"Discord.Rest.RestMessage.yml","Discord.Rest.RestMessage.Timestamp":"Discord.Rest.RestMessage.yml","Discord.Rest.RestMessage.UpdateAsync(RequestOptions)":"Discord.Rest.RestMessage.yml","Discord.Rest.RestMessage.DeleteAsync(RequestOptions)":"Discord.Rest.RestMessage.yml","Discord.Rest.RestMessage.ToString":"Discord.Rest.RestMessage.yml","Discord.Rest.RestReaction":"Discord.Rest.RestReaction.yml","Discord.Rest.RestReaction.Emoji":"Discord.Rest.RestReaction.yml","Discord.Rest.RestReaction.Count":"Discord.Rest.RestReaction.yml","Discord.Rest.RestReaction.Me":"Discord.Rest.RestReaction.yml","Discord.Rest.RestSystemMessage":"Discord.Rest.RestSystemMessage.yml","Discord.Rest.RestSystemMessage.Type":"Discord.Rest.RestSystemMessage.yml","Discord.Rest.RestUserMessage":"Discord.Rest.RestUserMessage.yml","Discord.Rest.RestUserMessage.IsTTS":"Discord.Rest.RestUserMessage.yml","Discord.Rest.RestUserMessage.IsPinned":"Discord.Rest.RestUserMessage.yml","Discord.Rest.RestUserMessage.WebhookId":"Discord.Rest.RestUserMessage.yml","Discord.Rest.RestUserMessage.EditedTimestamp":"Discord.Rest.RestUserMessage.yml","Discord.Rest.RestUserMessage.Attachments":"Discord.Rest.RestUserMessage.yml","Discord.Rest.RestUserMessage.Embeds":"Discord.Rest.RestUserMessage.yml","Discord.Rest.RestUserMessage.MentionedChannelIds":"Discord.Rest.RestUserMessage.yml","Discord.Rest.RestUserMessage.MentionedRoleIds":"Discord.Rest.RestUserMessage.yml","Discord.Rest.RestUserMessage.MentionedUsers":"Discord.Rest.RestUserMessage.yml","Discord.Rest.RestUserMessage.Tags":"Discord.Rest.RestUserMessage.yml","Discord.Rest.RestUserMessage.Reactions":"Discord.Rest.RestUserMessage.yml","Discord.Rest.RestUserMessage.ModifyAsync(Action{MessageProperties},RequestOptions)":"Discord.Rest.RestUserMessage.yml","Discord.Rest.RestUserMessage.AddReactionAsync(Emoji,RequestOptions)":"Discord.Rest.RestUserMessage.yml","Discord.Rest.RestUserMessage.AddReactionAsync(System.String,RequestOptions)":"Discord.Rest.RestUserMessage.yml","Discord.Rest.RestUserMessage.RemoveReactionAsync(Emoji,IUser,RequestOptions)":"Discord.Rest.RestUserMessage.yml","Discord.Rest.RestUserMessage.RemoveReactionAsync(System.String,IUser,RequestOptions)":"Discord.Rest.RestUserMessage.yml","Discord.Rest.RestUserMessage.RemoveAllReactionsAsync(RequestOptions)":"Discord.Rest.RestUserMessage.yml","Discord.Rest.RestUserMessage.GetReactionUsersAsync(System.String,System.Int32,System.Nullable{System.UInt64},RequestOptions)":"Discord.Rest.RestUserMessage.yml","Discord.Rest.RestUserMessage.PinAsync(RequestOptions)":"Discord.Rest.RestUserMessage.yml","Discord.Rest.RestUserMessage.UnpinAsync(RequestOptions)":"Discord.Rest.RestUserMessage.yml","Discord.Rest.RestUserMessage.Resolve(System.Int32,TagHandling,TagHandling,TagHandling,TagHandling,TagHandling)":"Discord.Rest.RestUserMessage.yml","Discord.Rest.RestUserMessage.Resolve(TagHandling,TagHandling,TagHandling,TagHandling,TagHandling)":"Discord.Rest.RestUserMessage.yml","Discord.Rest.RestRole":"Discord.Rest.RestRole.yml","Discord.Rest.RestRole.Color":"Discord.Rest.RestRole.yml","Discord.Rest.RestRole.IsHoisted":"Discord.Rest.RestRole.yml","Discord.Rest.RestRole.IsManaged":"Discord.Rest.RestRole.yml","Discord.Rest.RestRole.IsMentionable":"Discord.Rest.RestRole.yml","Discord.Rest.RestRole.Name":"Discord.Rest.RestRole.yml","Discord.Rest.RestRole.Permissions":"Discord.Rest.RestRole.yml","Discord.Rest.RestRole.Position":"Discord.Rest.RestRole.yml","Discord.Rest.RestRole.CreatedAt":"Discord.Rest.RestRole.yml","Discord.Rest.RestRole.IsEveryone":"Discord.Rest.RestRole.yml","Discord.Rest.RestRole.Mention":"Discord.Rest.RestRole.yml","Discord.Rest.RestRole.ModifyAsync(Action{RoleProperties},RequestOptions)":"Discord.Rest.RestRole.yml","Discord.Rest.RestRole.DeleteAsync(RequestOptions)":"Discord.Rest.RestRole.yml","Discord.Rest.RestRole.CompareTo(IRole)":"Discord.Rest.RestRole.yml","Discord.Rest.RestRole.ToString":"Discord.Rest.RestRole.yml","Discord.Rest.RestGroupUser":"Discord.Rest.RestGroupUser.yml","Discord.Rest.RestGuildUser":"Discord.Rest.RestGuildUser.yml","Discord.Rest.RestGuildUser.Nickname":"Discord.Rest.RestGuildUser.yml","Discord.Rest.RestGuildUser.IsDeafened":"Discord.Rest.RestGuildUser.yml","Discord.Rest.RestGuildUser.IsMuted":"Discord.Rest.RestGuildUser.yml","Discord.Rest.RestGuildUser.GuildId":"Discord.Rest.RestGuildUser.yml","Discord.Rest.RestGuildUser.GuildPermissions":"Discord.Rest.RestGuildUser.yml","Discord.Rest.RestGuildUser.RoleIds":"Discord.Rest.RestGuildUser.yml","Discord.Rest.RestGuildUser.JoinedAt":"Discord.Rest.RestGuildUser.yml","Discord.Rest.RestGuildUser.UpdateAsync(RequestOptions)":"Discord.Rest.RestGuildUser.yml","Discord.Rest.RestGuildUser.ModifyAsync(Action{GuildUserProperties},RequestOptions)":"Discord.Rest.RestGuildUser.yml","Discord.Rest.RestGuildUser.KickAsync(RequestOptions)":"Discord.Rest.RestGuildUser.yml","Discord.Rest.RestGuildUser.GetPermissions(IGuildChannel)":"Discord.Rest.RestGuildUser.yml","Discord.Rest.RestSelfUser":"Discord.Rest.RestSelfUser.yml","Discord.Rest.RestSelfUser.Email":"Discord.Rest.RestSelfUser.yml","Discord.Rest.RestSelfUser.IsVerified":"Discord.Rest.RestSelfUser.yml","Discord.Rest.RestSelfUser.IsMfaEnabled":"Discord.Rest.RestSelfUser.yml","Discord.Rest.RestSelfUser.UpdateAsync(RequestOptions)":"Discord.Rest.RestSelfUser.yml","Discord.Rest.RestSelfUser.ModifyAsync(Action{SelfUserProperties},RequestOptions)":"Discord.Rest.RestSelfUser.yml","Discord.Rest.RestUser":"Discord.Rest.RestUser.yml","Discord.Rest.RestUser.IsBot":"Discord.Rest.RestUser.yml","Discord.Rest.RestUser.Username":"Discord.Rest.RestUser.yml","Discord.Rest.RestUser.DiscriminatorValue":"Discord.Rest.RestUser.yml","Discord.Rest.RestUser.AvatarId":"Discord.Rest.RestUser.yml","Discord.Rest.RestUser.AvatarUrl":"Discord.Rest.RestUser.yml","Discord.Rest.RestUser.CreatedAt":"Discord.Rest.RestUser.yml","Discord.Rest.RestUser.Discriminator":"Discord.Rest.RestUser.yml","Discord.Rest.RestUser.Mention":"Discord.Rest.RestUser.yml","Discord.Rest.RestUser.Game":"Discord.Rest.RestUser.yml","Discord.Rest.RestUser.Status":"Discord.Rest.RestUser.yml","Discord.Rest.RestUser.UpdateAsync(RequestOptions)":"Discord.Rest.RestUser.yml","Discord.Rest.RestUser.CreateDMChannelAsync(RequestOptions)":"Discord.Rest.RestUser.yml","Discord.Rest.RestUser.ToString":"Discord.Rest.RestUser.yml"} \ No newline at end of file diff --git a/docs/docfx.json b/docs/docfx.json index e0b5514cd..42d6bbfee 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -5,7 +5,7 @@ { "src": "..", "files": [ - "src/**project.json" + "src/**/*.cs" ], "exclude": [ "**/obj/**", @@ -66,7 +66,7 @@ "default" ], "globalMetadata": { - "_appFooter": "Discord.Net (c) 2015-2016" + "_appFooter": "Discord.Net (c) 2015-2017" }, "noLangKeyword": false } diff --git a/docs/filterConfig.yml b/docs/filterConfig.yml index 79ea54ae0..715b39606 100644 --- a/docs/filterConfig.yml +++ b/docs/filterConfig.yml @@ -6,4 +6,6 @@ apiRules: - exclude: uidRegex: ^Discord\.Net\.Converters$ - exclude: - uidRegex: ^Discord\.Net.*$ \ No newline at end of file + uidRegex: ^Discord\.Net.*$ +- exclude: + uidRegex: ^RegexAnalyzer$ \ No newline at end of file From b3c6a065001c2aba05abf56f2c3e0a4838ef90d4 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Mon, 3 Apr 2017 16:26:29 -0400 Subject: [PATCH 095/243] Revamp docs page on logging --- docs/CONTRIBUTING.md | 40 +++++++++++++++++++++++++++---- docs/docfx.json | 3 ++- docs/guides/installing.md | 2 +- docs/guides/intro.md | 7 ++++-- docs/guides/logging.md | 43 +++++++++++++++++++++++++++++----- docs/guides/samples/logging.cs | 41 ++++++++++++++++---------------- 6 files changed, 101 insertions(+), 35 deletions(-) diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index f19e7c297..695475dfa 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -1,15 +1,45 @@ # Contributing to Docs -I don't really have any strict conditions for writing documentation, but just keep these few guidelines in mind: +I don't really have any strict conditions for writing documentation, +but just keep these few guidelines in mind: * Keep code samples in the `guides/samples` folder -* When referencing an object in the API, link to it's page in the API documentation. +* When referencing an object in the API, link to it's page in the +API documentation. * Documentation should be written in clear and proper English* -\* If anyone is interested in translating documentation into other languages, please open an issue or contact me on Discord (`foxbot#0282`). +\* If anyone is interested in translating documentation into other +languages, please open an issue or contact me on +Discord (`foxbot#0282`). + +### Layout + +Documentation should be written in a FAQ/Wiki style format. + +Recommended reads: + +* http://docs.microsoft.com +* http://flask.pocoo.org/docs/0.12/ + +Style consistencies: + +* Use a ruler set at 70 characters +* Links should use long syntax + +Example of long link syntax: + +``` +Please consult the [API Documentation] for more information. + +[API Documentation]: xref:System.String +``` ### Compiling -Documentation is compiled into a static site using [DocFx](https://dotnet.github.io/docfx/). We currently use version 2.8 +Documentation is compiled into a static site using [DocFx]. +We currently use the most recent build off the dev branch. + +After making changes, compile your changes into the static site with +`docfx`. You can also view your changes live with `docfx --serve`. -After making changes, compile your changes into the static site with `docfx`. You can also view your changes live with `docfx --serve`. \ No newline at end of file +[DocFx]: https://dotnet.github.io/docfx/ \ No newline at end of file diff --git a/docs/docfx.json b/docs/docfx.json index 42d6bbfee..3c0b0611e 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -42,7 +42,8 @@ "resource": [ { "files": [ - "images/**" + "**/images/**", + "**/samples/**" ], "exclude": [ "obj/**", diff --git a/docs/guides/installing.md b/docs/guides/installing.md index fa1280929..02c7cdab7 100644 --- a/docs/guides/installing.md +++ b/docs/guides/installing.md @@ -11,7 +11,7 @@ Optionally, you may compile from source and install yourself. Currently, Discord.Net targets [.NET Standard] 1.3, and offers support for .NET Standard 1.1. If your application will be targeting .NET Standard 1.1, -please see the [additional steps](#installing-on-.net-standard-11). +please see the [additional steps](#installing-on-net-standard-11). Since Discord.Net is built on the .NET Standard, it is also recommended to create applications using [.NET Core], though you are not required to. When diff --git a/docs/guides/intro.md b/docs/guides/intro.md index 314f2c32e..e0b41e22a 100644 --- a/docs/guides/intro.md +++ b/docs/guides/intro.md @@ -34,7 +34,9 @@ through the OAuth2 flow. 1. Open your bot's application on the [Discord Applications Portal] 2. Retrieve the app's **Client ID**. + ![Step 2](images/intro-client-id.png) + 3. Create an OAuth2 authorization URL `https://discordapp.com/oauth2/authorize?client_id=&scope=bot` 4. Open the authorization URL in your browser @@ -45,6 +47,7 @@ Only servers where you have the `MANAGE_SERVER` permission will be present in this list. 6. Click authorize + ![Step 6](images/intro-add-bot.png) ## Connecting to Discord @@ -120,7 +123,7 @@ on your bot's application page on the [Discord Applications Portal]). >[!IMPORTANT] Your bot's token can be used to gain total access to your bot, so -**do __NOT__ share this token with anyone!**. It may behoove you to +**do __NOT__ share this token with anyone!** It may behoove you to store this token in an external file if you plan on distributing the source code for your bot. @@ -157,7 +160,7 @@ for how to fix this. [TAP]: https://docs.microsoft.com/en-us/dotnet/articles/csharp/async [API Documentation]: xref:Discord.Rest.BaseDiscordClient#Discord_Rest_BaseDiscordClient_Log [DiscordSocketClient]: xref:Discord.WebSocket.DiscordSocketClient -[installing guide]: installing.md#installing-on-.net-standard-11 +[installing guide]: installing.md#installing-on-net-standard-11 ### Handling a 'ping' diff --git a/docs/guides/logging.md b/docs/guides/logging.md index 97fed3a52..1592dfc72 100644 --- a/docs/guides/logging.md +++ b/docs/guides/logging.md @@ -1,11 +1,42 @@ -# Using the Logger +--- +title: Logging +--- -Discord.Net will automatically output log messages through the [Log](xref:Discord.DiscordClient#Discord_DiscordClient_Log) event. +Discord.Net's clients provide a [Log] event that all messages will be +disbatched over. -## Usage +For more information about events in Discord.Net, see the [Events] +section. -To handle Log Messages through Discord.Net's Logger, hook into the [Log](xref:Discord.DiscordClient#Discord_DiscordClient_Log) event. +[Log]: xref:Discord.Rest.BaseDiscordClient#Discord_Rest_BaseDiscordClient_Log +[Events]: events.md -The @Discord.LogMessage object has a custom `ToString` method attached to it, when outputting log messages, it is reccomended you use this, instead of building your own output message. +### Usage -[!code-csharp[](samples/logging.cs)] \ No newline at end of file +To receive log events, simply hook the discord client's log method +to a Task with a single parameter of type [LogMessage] + +It is recommended that you use an established function instead of a +lambda for handling logs, because most [addons] accept a reference +to a logging function to write their own messages. + +### Usage in Commands + +Discord.Net's [CommandService] also provides a log event, identical +in signature to other log events. + +Data logged through this event is typically coupled with a +[CommandException], where information about the command's context +and error can be found and handled. + +#### Samples + +[!code-csharp[Logging Sample](samples/logging.cs)] + +#### Tips + +Due to the nature of Discord.Net's event system, all log event +handlers will be executed synchronously on the gateway thread. If your +log output will be dumped to a Web API (e.g. Sentry), you are advised +to wrap your output in a `Task.Run` so the gateway thread does not +become blocked while waiting for logging data to be written. \ No newline at end of file diff --git a/docs/guides/samples/logging.cs b/docs/guides/samples/logging.cs index fd72daf2b..a2ddf7b90 100644 --- a/docs/guides/samples/logging.cs +++ b/docs/guides/samples/logging.cs @@ -1,28 +1,29 @@ using Discord; -using Discord.Rest; +using Discord.WebSocket; public class Program { - private DiscordSocketClient _client; - static void Main(string[] args) => new Program().Start().GetAwaiter().GetResult(); - - public async Task Start() - { - _client = new DiscordSocketClient(new DiscordSocketConfig() { + private DiscordSocketClient _client; + static void Main(string[] args) => new Program().MainAsync().GetAwaiter().GetResult(); + + public async Task MainAsync() + { + _client = new DiscordSocketClient(new DiscordSocketConfig + { LogLevel = LogSeverity.Info - }); + }); - _client.Log += Log; + _client.Log += Log; - await _client.LoginAsync(TokenType.Bot, "bot token"); - await _client.ConnectAsync(); - - await Task.Delay(-1); - } + await _client.LoginAsync(TokenType.Bot, "bot token"); + await _client.StartAsync(); + + await Task.Delay(-1); + } - private Task Log(LogMessage message) - { - Console.WriteLine(message.ToString()); - return Task.CompletedTask; - } -} + private Task Log(LogMessage message) + { + Console.WriteLine(message.ToString()); + return Task.CompletedTask; + } +} \ No newline at end of file From b4c3427ed1bf349f51409b66415e57a2b97bbe2b Mon Sep 17 00:00:00 2001 From: Christopher F Date: Mon, 3 Apr 2017 16:26:58 -0400 Subject: [PATCH 096/243] Deleted FAQ this is sloppy and doesn't properly explain anything --- docs/guides/samples.md | 20 -------------------- docs/guides/samples/faq/avatar.cs | 5 ----- docs/guides/samples/faq/send_message.cs | 6 ------ docs/guides/samples/faq/status.cs | 5 ----- 4 files changed, 36 deletions(-) delete mode 100644 docs/guides/samples.md delete mode 100644 docs/guides/samples/faq/avatar.cs delete mode 100644 docs/guides/samples/faq/send_message.cs delete mode 100644 docs/guides/samples/faq/status.cs diff --git a/docs/guides/samples.md b/docs/guides/samples.md deleted file mode 100644 index 4406f2f1e..000000000 --- a/docs/guides/samples.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -title: Samples ---- - -# Samples - ->[!NOTE] ->All of these samples assume you have `_client` defined as a `DiscordSocketClient`. - -#### Changing the bot's avatar - -[!code-csharp[Bot Avatar](samples/faq/avatar.cs)] - -#### Changing the bot's status - -[!code-csharp[Bot Status](samples/faq/status.cs)] - -#### Sending a message to a channel - -[!code-csharp[Message to Channel](samples/faq/send_message.cs)] diff --git a/docs/guides/samples/faq/avatar.cs b/docs/guides/samples/faq/avatar.cs deleted file mode 100644 index d3995cf0c..000000000 --- a/docs/guides/samples/faq/avatar.cs +++ /dev/null @@ -1,5 +0,0 @@ -public async Task ChangeAvatar() -{ - var fileStream = new FileStream("./newAvatar.png", FileMode.Open); - await _client.CurrentUser.ModifyAsync(x => x.Avatar = fileStream); -} \ No newline at end of file diff --git a/docs/guides/samples/faq/send_message.cs b/docs/guides/samples/faq/send_message.cs deleted file mode 100644 index d7ecf5131..000000000 --- a/docs/guides/samples/faq/send_message.cs +++ /dev/null @@ -1,6 +0,0 @@ -public async Task SendMessageToChannel(ulong ChannelId) -{ - var channel = _client.GetChannel(ChannelId) as SocketMessageChannel; - await channel?.SendMessageAsync("aaaaaaaaahhh!!!") - /* ^ This question mark is used to indicate that 'channel' may sometimes be null, and in cases that it is null, we will do nothing here. */ -} \ No newline at end of file diff --git a/docs/guides/samples/faq/status.cs b/docs/guides/samples/faq/status.cs deleted file mode 100644 index 18906c53b..000000000 --- a/docs/guides/samples/faq/status.cs +++ /dev/null @@ -1,5 +0,0 @@ -public async Task ModifyStatus() -{ - await _client.SetStatusAsync(UserStatus.Idle); - await _client.SetGameAsync("Type !help for help"); -} From cba07cbf2b431300570957c38be660d2db732936 Mon Sep 17 00:00:00 2001 From: RogueException Date: Mon, 3 Apr 2017 17:34:16 -0300 Subject: [PATCH 097/243] Bumped version to rc2 --- Discord.Net.targets | 2 +- src/Discord.Net/Discord.Net.nuspec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Discord.Net.targets b/Discord.Net.targets index 15591afc2..a4558b069 100644 --- a/Discord.Net.targets +++ b/Discord.Net.targets @@ -1,7 +1,7 @@ 1.0.0 - rc + rc2 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 e83cec52c..3516bd208 100644 --- a/src/Discord.Net/Discord.Net.nuspec +++ b/src/Discord.Net/Discord.Net.nuspec @@ -2,7 +2,7 @@ Discord.Net - 1.0.0-rc$suffix$ + 1.0.0-rc2$suffix$ Discord.Net RogueException RogueException From 5ade1e387bb8ea808a9d858328e2d3db23fe0663 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Mon, 3 Apr 2017 16:36:26 -0400 Subject: [PATCH 098/243] Restructure documentation layout --- docs/CONTRIBUTING.md | 1 + docs/guides/{ => commands}/commands.md | 0 .../{ => commands}/samples/command_handler.cs | 0 .../samples/dependency_map_setup.cs | 0 .../samples/dependency_module.cs | 0 .../{ => commands}/samples/empty-module.cs | 0 docs/guides/{ => commands}/samples/groups.cs | 0 docs/guides/{ => commands}/samples/module.cs | 0 .../{ => commands}/samples/require_owner.cs | 0 .../{ => commands}/samples/typereader.cs | 0 docs/guides/{ => concepts}/events.md | 0 docs/guides/{ => concepts}/logging.md | 0 docs/guides/{ => concepts}/samples/logging.cs | 0 .../images/install-vs-deps.png | Bin .../images/install-vs-nuget.png | Bin .../images/intro-add-bot.png | Bin .../images/intro-client-id.png | Bin .../images/intro-create-app.png | Bin .../images/intro-create-bot.png | Bin .../images/intro-token.png | Bin .../{ => getting_started}/installing.md | 0 docs/guides/{ => getting_started}/intro.md | 0 .../samples/intro/async-context.cs | 0 .../samples/intro/client.cs | 0 .../samples/intro/complete.cs | 0 .../samples/intro/logging.cs | 0 .../samples/intro/message.cs | 0 .../samples/intro/structure.cs | 0 .../{ => getting_started}/samples/netstd11.cs | 0 .../samples/nuget.config | 0 .../samples/project.csproj | 0 .../{ => getting_started}/terminology.md | 0 docs/{ => guides/migrating}/migrating.md | 0 .../migrating => migrating/samples}/event.cs | 0 .../samples}/sync_event.cs | 0 docs/guides/toc.yml | 31 +++++++++--------- .../samples/audio_create_ffmpeg.cs | 0 .../{ => voice}/samples/audio_ffmpeg.cs | 0 .../{ => voice}/samples/joining_audio.cs | 0 .../{voice.md => voice/sending-voice.md} | 21 +++--------- docs/toc.yml | 2 -- 41 files changed, 22 insertions(+), 33 deletions(-) rename docs/guides/{ => commands}/commands.md (100%) rename docs/guides/{ => commands}/samples/command_handler.cs (100%) rename docs/guides/{ => commands}/samples/dependency_map_setup.cs (100%) rename docs/guides/{ => commands}/samples/dependency_module.cs (100%) rename docs/guides/{ => commands}/samples/empty-module.cs (100%) rename docs/guides/{ => commands}/samples/groups.cs (100%) rename docs/guides/{ => commands}/samples/module.cs (100%) rename docs/guides/{ => commands}/samples/require_owner.cs (100%) rename docs/guides/{ => commands}/samples/typereader.cs (100%) rename docs/guides/{ => concepts}/events.md (100%) rename docs/guides/{ => concepts}/logging.md (100%) rename docs/guides/{ => concepts}/samples/logging.cs (100%) rename docs/guides/{ => getting_started}/images/install-vs-deps.png (100%) rename docs/guides/{ => getting_started}/images/install-vs-nuget.png (100%) rename docs/guides/{ => getting_started}/images/intro-add-bot.png (100%) rename docs/guides/{ => getting_started}/images/intro-client-id.png (100%) rename docs/guides/{ => getting_started}/images/intro-create-app.png (100%) rename docs/guides/{ => getting_started}/images/intro-create-bot.png (100%) rename docs/guides/{ => getting_started}/images/intro-token.png (100%) rename docs/guides/{ => getting_started}/installing.md (100%) rename docs/guides/{ => getting_started}/intro.md (100%) rename docs/guides/{ => getting_started}/samples/intro/async-context.cs (100%) rename docs/guides/{ => getting_started}/samples/intro/client.cs (100%) rename docs/guides/{ => getting_started}/samples/intro/complete.cs (100%) rename docs/guides/{ => getting_started}/samples/intro/logging.cs (100%) rename docs/guides/{ => getting_started}/samples/intro/message.cs (100%) rename docs/guides/{ => getting_started}/samples/intro/structure.cs (100%) rename docs/guides/{ => getting_started}/samples/netstd11.cs (100%) rename docs/guides/{ => getting_started}/samples/nuget.config (100%) rename docs/guides/{ => getting_started}/samples/project.csproj (100%) rename docs/guides/{ => getting_started}/terminology.md (100%) rename docs/{ => guides/migrating}/migrating.md (100%) rename docs/guides/{samples/migrating => migrating/samples}/event.cs (100%) rename docs/guides/{samples/migrating => migrating/samples}/sync_event.cs (100%) rename docs/guides/{ => voice}/samples/audio_create_ffmpeg.cs (100%) rename docs/guides/{ => voice}/samples/audio_ffmpeg.cs (100%) rename docs/guides/{ => voice}/samples/joining_audio.cs (100%) rename docs/guides/{voice.md => voice/sending-voice.md} (86%) diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 695475dfa..296b6d1cb 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -25,6 +25,7 @@ Style consistencies: * Use a ruler set at 70 characters * Links should use long syntax +* Pages should be short and concise, not broad and long Example of long link syntax: diff --git a/docs/guides/commands.md b/docs/guides/commands/commands.md similarity index 100% rename from docs/guides/commands.md rename to docs/guides/commands/commands.md diff --git a/docs/guides/samples/command_handler.cs b/docs/guides/commands/samples/command_handler.cs similarity index 100% rename from docs/guides/samples/command_handler.cs rename to docs/guides/commands/samples/command_handler.cs diff --git a/docs/guides/samples/dependency_map_setup.cs b/docs/guides/commands/samples/dependency_map_setup.cs similarity index 100% rename from docs/guides/samples/dependency_map_setup.cs rename to docs/guides/commands/samples/dependency_map_setup.cs diff --git a/docs/guides/samples/dependency_module.cs b/docs/guides/commands/samples/dependency_module.cs similarity index 100% rename from docs/guides/samples/dependency_module.cs rename to docs/guides/commands/samples/dependency_module.cs diff --git a/docs/guides/samples/empty-module.cs b/docs/guides/commands/samples/empty-module.cs similarity index 100% rename from docs/guides/samples/empty-module.cs rename to docs/guides/commands/samples/empty-module.cs diff --git a/docs/guides/samples/groups.cs b/docs/guides/commands/samples/groups.cs similarity index 100% rename from docs/guides/samples/groups.cs rename to docs/guides/commands/samples/groups.cs diff --git a/docs/guides/samples/module.cs b/docs/guides/commands/samples/module.cs similarity index 100% rename from docs/guides/samples/module.cs rename to docs/guides/commands/samples/module.cs diff --git a/docs/guides/samples/require_owner.cs b/docs/guides/commands/samples/require_owner.cs similarity index 100% rename from docs/guides/samples/require_owner.cs rename to docs/guides/commands/samples/require_owner.cs diff --git a/docs/guides/samples/typereader.cs b/docs/guides/commands/samples/typereader.cs similarity index 100% rename from docs/guides/samples/typereader.cs rename to docs/guides/commands/samples/typereader.cs diff --git a/docs/guides/events.md b/docs/guides/concepts/events.md similarity index 100% rename from docs/guides/events.md rename to docs/guides/concepts/events.md diff --git a/docs/guides/logging.md b/docs/guides/concepts/logging.md similarity index 100% rename from docs/guides/logging.md rename to docs/guides/concepts/logging.md diff --git a/docs/guides/samples/logging.cs b/docs/guides/concepts/samples/logging.cs similarity index 100% rename from docs/guides/samples/logging.cs rename to docs/guides/concepts/samples/logging.cs diff --git a/docs/guides/images/install-vs-deps.png b/docs/guides/getting_started/images/install-vs-deps.png similarity index 100% rename from docs/guides/images/install-vs-deps.png rename to docs/guides/getting_started/images/install-vs-deps.png diff --git a/docs/guides/images/install-vs-nuget.png b/docs/guides/getting_started/images/install-vs-nuget.png similarity index 100% rename from docs/guides/images/install-vs-nuget.png rename to docs/guides/getting_started/images/install-vs-nuget.png diff --git a/docs/guides/images/intro-add-bot.png b/docs/guides/getting_started/images/intro-add-bot.png similarity index 100% rename from docs/guides/images/intro-add-bot.png rename to docs/guides/getting_started/images/intro-add-bot.png diff --git a/docs/guides/images/intro-client-id.png b/docs/guides/getting_started/images/intro-client-id.png similarity index 100% rename from docs/guides/images/intro-client-id.png rename to docs/guides/getting_started/images/intro-client-id.png diff --git a/docs/guides/images/intro-create-app.png b/docs/guides/getting_started/images/intro-create-app.png similarity index 100% rename from docs/guides/images/intro-create-app.png rename to docs/guides/getting_started/images/intro-create-app.png diff --git a/docs/guides/images/intro-create-bot.png b/docs/guides/getting_started/images/intro-create-bot.png similarity index 100% rename from docs/guides/images/intro-create-bot.png rename to docs/guides/getting_started/images/intro-create-bot.png diff --git a/docs/guides/images/intro-token.png b/docs/guides/getting_started/images/intro-token.png similarity index 100% rename from docs/guides/images/intro-token.png rename to docs/guides/getting_started/images/intro-token.png diff --git a/docs/guides/installing.md b/docs/guides/getting_started/installing.md similarity index 100% rename from docs/guides/installing.md rename to docs/guides/getting_started/installing.md diff --git a/docs/guides/intro.md b/docs/guides/getting_started/intro.md similarity index 100% rename from docs/guides/intro.md rename to docs/guides/getting_started/intro.md diff --git a/docs/guides/samples/intro/async-context.cs b/docs/guides/getting_started/samples/intro/async-context.cs similarity index 100% rename from docs/guides/samples/intro/async-context.cs rename to docs/guides/getting_started/samples/intro/async-context.cs diff --git a/docs/guides/samples/intro/client.cs b/docs/guides/getting_started/samples/intro/client.cs similarity index 100% rename from docs/guides/samples/intro/client.cs rename to docs/guides/getting_started/samples/intro/client.cs diff --git a/docs/guides/samples/intro/complete.cs b/docs/guides/getting_started/samples/intro/complete.cs similarity index 100% rename from docs/guides/samples/intro/complete.cs rename to docs/guides/getting_started/samples/intro/complete.cs diff --git a/docs/guides/samples/intro/logging.cs b/docs/guides/getting_started/samples/intro/logging.cs similarity index 100% rename from docs/guides/samples/intro/logging.cs rename to docs/guides/getting_started/samples/intro/logging.cs diff --git a/docs/guides/samples/intro/message.cs b/docs/guides/getting_started/samples/intro/message.cs similarity index 100% rename from docs/guides/samples/intro/message.cs rename to docs/guides/getting_started/samples/intro/message.cs diff --git a/docs/guides/samples/intro/structure.cs b/docs/guides/getting_started/samples/intro/structure.cs similarity index 100% rename from docs/guides/samples/intro/structure.cs rename to docs/guides/getting_started/samples/intro/structure.cs diff --git a/docs/guides/samples/netstd11.cs b/docs/guides/getting_started/samples/netstd11.cs similarity index 100% rename from docs/guides/samples/netstd11.cs rename to docs/guides/getting_started/samples/netstd11.cs diff --git a/docs/guides/samples/nuget.config b/docs/guides/getting_started/samples/nuget.config similarity index 100% rename from docs/guides/samples/nuget.config rename to docs/guides/getting_started/samples/nuget.config diff --git a/docs/guides/samples/project.csproj b/docs/guides/getting_started/samples/project.csproj similarity index 100% rename from docs/guides/samples/project.csproj rename to docs/guides/getting_started/samples/project.csproj diff --git a/docs/guides/terminology.md b/docs/guides/getting_started/terminology.md similarity index 100% rename from docs/guides/terminology.md rename to docs/guides/getting_started/terminology.md diff --git a/docs/migrating.md b/docs/guides/migrating/migrating.md similarity index 100% rename from docs/migrating.md rename to docs/guides/migrating/migrating.md diff --git a/docs/guides/samples/migrating/event.cs b/docs/guides/migrating/samples/event.cs similarity index 100% rename from docs/guides/samples/migrating/event.cs rename to docs/guides/migrating/samples/event.cs diff --git a/docs/guides/samples/migrating/sync_event.cs b/docs/guides/migrating/samples/sync_event.cs similarity index 100% rename from docs/guides/samples/migrating/sync_event.cs rename to docs/guides/migrating/samples/sync_event.cs diff --git a/docs/guides/toc.yml b/docs/guides/toc.yml index 308293d1e..4a2c4d2da 100644 --- a/docs/guides/toc.yml +++ b/docs/guides/toc.yml @@ -1,17 +1,18 @@ - -- name: Installing - href: installing.md - name: Getting Started - href: intro.md -- name: Terminology - href: terminology.md -- name: Logging - href: logging.md -- name: Commands - href: commands.md + items: + - name: Installation + href: getting_started/installing.md + - name: Your First Bot + href: getting_started/intro.md + - name: Terminology + href: getting_started/terminology.md +- name: Basic Concepts + items: + - name: Logging Data + href: concepts/logging.md + - name: Working with Events + href: concepts/events.md + - name: Entities +- name: The Command Service - name: Voice - href: voice.md -- name: Events - href: events.md -- name: Code Samples - href: samples.md \ No newline at end of file +- name: Migrating from 0.9 \ No newline at end of file diff --git a/docs/guides/samples/audio_create_ffmpeg.cs b/docs/guides/voice/samples/audio_create_ffmpeg.cs similarity index 100% rename from docs/guides/samples/audio_create_ffmpeg.cs rename to docs/guides/voice/samples/audio_create_ffmpeg.cs diff --git a/docs/guides/samples/audio_ffmpeg.cs b/docs/guides/voice/samples/audio_ffmpeg.cs similarity index 100% rename from docs/guides/samples/audio_ffmpeg.cs rename to docs/guides/voice/samples/audio_ffmpeg.cs diff --git a/docs/guides/samples/joining_audio.cs b/docs/guides/voice/samples/joining_audio.cs similarity index 100% rename from docs/guides/samples/joining_audio.cs rename to docs/guides/voice/samples/joining_audio.cs diff --git a/docs/guides/voice.md b/docs/guides/voice/sending-voice.md similarity index 86% rename from docs/guides/voice.md rename to docs/guides/voice/sending-voice.md index 1f09069f5..f1ca3d0a5 100644 --- a/docs/guides/voice.md +++ b/docs/guides/voice/sending-voice.md @@ -1,24 +1,13 @@ -# Voice +--- +title: Sending Voice +--- **Information on this page is subject to change!** >[!WARNING] ->Audio in 1.0 is incomplete. Most of the below documentation is untested. +>Audio in 1.0 is in progress -## Installation - -To use Audio, you must first configure your [DiscordSocketClient] -with Audio support. - -In your [DiscordSocketConfig], set `AudioMode` to the appropriate -[AudioMode] for your bot. For most bots, you will only need to use -`AudioMode.Outgoing`. - -[DiscordSocketClient]: xref:Discord.WebSocket.DiscordSocketClient -[DiscordSocketConfig]: xref:Discord.WebSocket.DiscordSocketConfig -[AudioMode]: xref:Discord.Audio.AudioMode - -### Dependencies +## Installing Audio requires two native libraries, `libsodium` and `opus`. Both of these libraries must be placed in the runtime directory of your diff --git a/docs/toc.yml b/docs/toc.yml index 47a8a22c1..c08e708bf 100644 --- a/docs/toc.yml +++ b/docs/toc.yml @@ -1,8 +1,6 @@ - name: Guides href: guides/ -- name: Migrating - href: migrating.md - name: API Documentation href: api/ homepage: api/index.md From f3b89376865df61284c3d5136eda13cb80207724 Mon Sep 17 00:00:00 2001 From: RogueException Date: Mon, 3 Apr 2017 17:38:28 -0300 Subject: [PATCH 099/243] Added TryReadFrame and AvailableFrames to AudioInStream --- src/Discord.Net.Core/Audio/AudioInStream.cs | 2 ++ .../Audio/Streams/InputStream.cs | 27 ++++++++++++++----- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/Discord.Net.Core/Audio/AudioInStream.cs b/src/Discord.Net.Core/Audio/AudioInStream.cs index 6503474e5..86f1d8935 100644 --- a/src/Discord.Net.Core/Audio/AudioInStream.cs +++ b/src/Discord.Net.Core/Audio/AudioInStream.cs @@ -10,8 +10,10 @@ namespace Discord.Audio public override bool CanRead => true; public override bool CanSeek => false; public override bool CanWrite => true; + public abstract int AvailableFrames { get; } public abstract Task ReadFrameAsync(CancellationToken cancelToken); + public abstract bool TryReadFrame(CancellationToken cancelToken, out RTPFrame frame); public RTPFrame ReadFrame() { diff --git a/src/Discord.Net.WebSocket/Audio/Streams/InputStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/InputStream.cs index 14bb18851..e29302fa0 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/InputStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/InputStream.cs @@ -8,7 +8,7 @@ namespace Discord.Audio.Streams /// Reads the payload from an RTP frame public class InputStream : AudioInStream { - private const int MaxFrames = 100; + private const int MaxFrames = 100; //1-2 Seconds private ConcurrentQueue _frames; private SemaphoreSlim _signal; @@ -20,6 +20,7 @@ namespace Discord.Audio.Streams public override bool CanRead => !_isDisposed; public override bool CanSeek => false; public override bool CanWrite => false; + public override int AvailableFrames => _signal.CurrentCount; public InputStream() { @@ -27,14 +28,17 @@ namespace Discord.Audio.Streams _signal = new SemaphoreSlim(0, MaxFrames); } - public override async Task ReadFrameAsync(CancellationToken cancelToken) + public override bool TryReadFrame(CancellationToken cancelToken, out RTPFrame frame) { cancelToken.ThrowIfCancellationRequested(); - RTPFrame frame; - await _signal.WaitAsync(cancelToken).ConfigureAwait(false); - _frames.TryDequeue(out frame); - return frame; + if (_signal.Wait(0)) + { + _frames.TryDequeue(out frame); + return true; + } + frame = default(RTPFrame); + return false; } public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancelToken) { @@ -46,6 +50,15 @@ namespace Discord.Audio.Streams Buffer.BlockCopy(frame.Payload, 0, buffer, offset, frame.Payload.Length); return frame.Payload.Length; } + public override async Task ReadFrameAsync(CancellationToken cancelToken) + { + cancelToken.ThrowIfCancellationRequested(); + + RTPFrame frame; + await _signal.WaitAsync(cancelToken).ConfigureAwait(false); + _frames.TryDequeue(out frame); + return frame; + } public void WriteHeader(ushort seq, uint timestamp) { @@ -59,7 +72,7 @@ namespace Discord.Audio.Streams { cancelToken.ThrowIfCancellationRequested(); - if (_frames.Count > MaxFrames) //1-2 seconds + if (_signal.CurrentCount >= MaxFrames) //1-2 seconds { _hasHeader = false; return Task.Delay(0); //Buffer overloaded From 3a60c58697b6940b9e99f8c6804c795146f95c4a Mon Sep 17 00:00:00 2001 From: Christopher F Date: Mon, 3 Apr 2017 18:46:00 -0400 Subject: [PATCH 100/243] Rewrite all concepts documentation hello RC documentation --- docs/guides/commands/commands.md | 4 + docs/guides/concepts/connections.md | 58 ++++++++++++++ docs/guides/concepts/entities.md | 71 +++++++++++++++++ docs/guides/concepts/events.md | 86 +++++++++++++++++---- docs/guides/concepts/samples/connections.cs | 23 ++++++ docs/guides/concepts/samples/entities.cs | 13 ++++ docs/guides/concepts/samples/events.cs | 31 ++++++++ docs/guides/getting_started/intro.md | 2 +- docs/guides/toc.yml | 9 +++ docs/guides/voice/sending-voice.md | 3 +- docs/index.md | 2 +- 11 files changed, 284 insertions(+), 18 deletions(-) create mode 100644 docs/guides/concepts/connections.md create mode 100644 docs/guides/concepts/entities.md create mode 100644 docs/guides/concepts/samples/connections.cs create mode 100644 docs/guides/concepts/samples/entities.cs create mode 100644 docs/guides/concepts/samples/events.cs diff --git a/docs/guides/commands/commands.md b/docs/guides/commands/commands.md index 8f1a34db9..6dd595861 100644 --- a/docs/guides/commands/commands.md +++ b/docs/guides/commands/commands.md @@ -1,5 +1,9 @@ # The Command Service +>[!WARNING] +>This article is out of date, and has not been rewritten yet. +Information is not guaranteed to be accurate. + [Discord.Commands](xref:Discord.Commands) provides an Attribute-based Command Parser. diff --git a/docs/guides/concepts/connections.md b/docs/guides/concepts/connections.md new file mode 100644 index 000000000..30e5e55cd --- /dev/null +++ b/docs/guides/concepts/connections.md @@ -0,0 +1,58 @@ +--- +title: Managing Connections +--- + +In Discord.Net, once a client has been started, it will automatically +maintain a connection to Discord's gateway, until it is manually +stopped. + +### Usage + +To start a connection, invoke the `StartAsync` method on a client that +supports a WebSocket connection. + +These clients include the [DiscordSocketClient] and +[DiscordRpcClient], as well as Audio clients. + +To end a connection, invoke the `StopAsync` method. This will +gracefully close any open WebSocket or UdpSocket connections. + +Since the Start/Stop methods only signal to an underlying connection +manager that a connection needs to be started, **they return before a +connection is actually made.** + +As a result, you will need to hook into one of the connection-state +based events to have an accurate representation of when a client is +ready for use. + +All clients provide a `Connected` and `Disconnected` event, which is +raised respectively when a connection opens or closes. In the case of +the DiscordSocketClient, this does **not** mean that the client is +ready to be used. + +A separate event, `Ready`, is provided on DiscordSocketClient, which +is raised only when the client has finished guild stream or guild +sync, and has a complete guild cache. + +[DiscordSocketClient]: xref:Discord.WebSocket.DiscordSocketClient +[DiscordRpcClient]: xref:Discord.Rpc.DiscordRpcClient + +### Samples + +[!code-csharp[Connection Sample](samples/events.cs)] + +### Tips + +Avoid running long-running code on the gateway! If you deadlock the +gateway (as explained in [events]), the connection manager will be +unable to recover and reconnect. + +Assuming the client disconnected because of a fault on Discord's end, +and not a deadlock on your end, we will always attempt to reconnect +and resume a connection. + +Don't worry about trying to maintain your own connections, the +connection manager is designed to be bulletproof and never fail - if +your client doesn't manage to reconnect, you've found a bug! + +[events]: events.md \ No newline at end of file diff --git a/docs/guides/concepts/entities.md b/docs/guides/concepts/entities.md new file mode 100644 index 000000000..b753293bb --- /dev/null +++ b/docs/guides/concepts/entities.md @@ -0,0 +1,71 @@ +--- +title: Entities +--- + +>[!NOTE] +This article is written with the Socket variants of entities in mind, +not the general interfaces or Rest/Rpc entities. + +Discord.Net provides a versatile entity system for navigating the +Discord API. + +### Inheritance + +Due to the nature of the Discord API, some entities are designed with +multiple variants, for example, `SocketUser` and `SocketGuildUser`. + +All models will contain the most detailed version of an entity +possible, even if the type is less detailed. + +For example, in the case of the `MessageReceived` event, a +`SocketMessage` is passed in with a channel property of type +`SocketMessageChannel`. All messages come from channels capable of +messaging, so this is the only variant of a channel that can cover +every single case. + +But that doesn't mean a message _can't_ come from a +`SocketTextChannel`, which is a message channel in a guild. To +retrieve information about a guild from a message entity, you will +need to cast its channel object to a `SocketTextChannel`. + +### Navigation + +All socket entities have navigation properties on them, which allow +you to easily navigate to an entity's parent or children. As explained +above, you will sometimes need to cast to a more detailed version of +an entity to navigate to its parent. + +All socket entities have a `Discord` property, which will allow you +to access the parent `DiscordSocketClient`. + +### Accessing Entities + +The most basic forms of entities, `SocketGuild`, `SocketUser`, and +`SocketChannel` can be pulled from the DiscordSocketClient's global +cache, and can be retrieved using the respective `GetXXX` method on +DiscordSocketClient. + +>[!TIP] +It is **vital** that you use the proper IDs for an entity when using +a GetXXX method. It is recommended that you enable Discord's +_developer mode_ to allow easy access to entity IDs, found in +Settings > Appearance > Advanced + +More detailed versions of entities can be pulled from the basic +entities, e.g. `SocketGuild.GetUser`, which returns a +`SocketGuildUser`, or `SocketGuild.GetChannel`, which returns a +`SocketGuildChannel`. Again, you may need to cast these objects to get +a variant of the type that you need. + +### Samples + +[!code-csharp[Entity Sample](samples/entities.cs)] + +### Tips + +Avoid using boxing-casts to coerce entities into a variant, use the +`as` keyword, and a null-conditional operator. + +This allows you to write safer code, and avoid InvalidCastExceptions. + +For example, `(message.Author as SocketGuildUser)?.Nickname`. \ No newline at end of file diff --git a/docs/guides/concepts/events.md b/docs/guides/concepts/events.md index b10dc7648..f2dfb00f0 100644 --- a/docs/guides/concepts/events.md +++ b/docs/guides/concepts/events.md @@ -1,28 +1,84 @@ --- -title: Events +title: Working with Events --- -# Events +Events in Discord.Net are consumed in a similar manner to the standard +convention, with the exception that every event must be of the type +`System.Threading.Tasks.Task`, and instead of using EventArgs, the +event's parameters are passed directly into the handler. -Messages from Discord are exposed via events, and follow a pattern of `Func<[event params], Task>`, which allows you to easily create either async or sync event handlers. +This allows for events to be handled in an async context directly, +instead of relying on async void. -To hook into events, you must be using the @Discord.WebSocket.DiscordSocketClient, which provides WebSocket capabilities, necessary for receiving events. +### Usage ->[!NOTE] ->The gateway will wait for all registered handlers of an event to finish before raising the next event. As a result of this, it is reccomended that if you need to perform any heavy work in an event handler, it is done on its own thread or Task. +To receive data from an event, hook into it using C#'s delegate +event pattern. -**For further documentation of all events**, it is reccomended to look at the [Events Section](xref:Discord.WebSocket.DiscordSocketClient#events) on the API documentation of @Discord.WebSocket.DiscordSocketClient +You may opt either to hook an event to an anonymous function (lambda) +or a named function. -## Connection State +### Safety -Connection Events will be raised when the Connection State of your client changes. +All events are designed to be thread-safe, in that events are executed +synchronously off the gateway task, in the same context as the gateway +task. -[DiscordSocketClient.Connected](xref:Discord.WebSocket.DiscordSocketClient#Discord_WebSocket_DiscordSocketClient_Connected) and [Disconnected](xref:Discord.WebSocket.DiscordSocketClient#Discord_WebSocket_DiscordSocketClient_Disconnected) are raised when the Gateway Socket connects or disconnects, respectively. +As a side effect, this makes it possible to deadlock the gateway task, +and kill a connection. As a general rule of thumb, any task that takes +longer than three seconds should **not** be awaited directly in the +context of an event, but should be wrapped in a `Task.Run` or +offloaded to another task. ->[!WARNING] ->You should not use DiscordClient.Connected to run code when your client first connects to Discord. The client has not received and parsed the READY event and guild stream yet, and will have an incomplete or empty cache. +This also means that you should not await a task that requests data +from Discord's gateway in the same context of an event. Since the +gateway will wait on all invoked event handlers to finish before +processing any additional data from the gateway, this will create +a deadlock that will be impossible to recover from. -[DiscordSocketClient.Ready](xref:Discord.WebSocket.DiscordSocketClient#Discord_WebSocket_DiscordSocketClient_Ready) is raised when the `READY` packet is parsed and received from Discord. +Exceptions in commands will be swallowed by the gateway and logged out +through the client's log method. ->[!NOTE] ->The [DiscordSocketClient.ConnectAsync](xref:Discord.WebSocket.DiscordSocketClient#Discord_WebSocket_DiscordSocketClient_ConnectAsync_System_Boolean_) method will not return until the READY packet has been processed. By default, it also will not return until the guild stream has finished. This means it is safe to run bot code directly after awaiting the ConnectAsync method. \ No newline at end of file +### Common Patterns + +As you may know, events in Discord.Net are only given a signature of +`Func`. There is no room for predefined argument names, +so you must either consult IntelliSense, or view the API documentation +directly. + +That being said, there are a variety of common patterns that allow you +to infer what the parameters in an event mean. + +#### Entity, Entity + +An event handler with a signature of `Func` +typically means that the first object will be a clone of the entity +_before_ a change was made, and the latter object will be an attached +model of the entity _after_ the change was made. + +This pattern is typically only found on `EntityUpdated` events. + +#### Cacheable + +An event handler with a signature of `Func` +means that the `before` state of the entity was not provided by the +API, so it can either be pulled from the client's cache, or +downloaded from the API. + +See the documentation for [Cacheable] for more information on this +object. + +[Cacheable]: xref:Discord.Cacheable`2 + +### Samples + +[!code-csharp[Event Sample](samples/events.cs)] + +### Tips + +Many events relating to a Message entity, e.g. `MessageUpdated` +and `ReactionAdded` rely on the client's message cache, which is +**not** enabled by default. Set the `MessageCacheSize` flag in +[DiscordSocketConfig] to enable it. + +[DiscordSocketConfig]: xref:Discord.WebSocket.DiscordSocketConfig \ No newline at end of file diff --git a/docs/guides/concepts/samples/connections.cs b/docs/guides/concepts/samples/connections.cs new file mode 100644 index 000000000..f96251a39 --- /dev/null +++ b/docs/guides/concepts/samples/connections.cs @@ -0,0 +1,23 @@ +using Discord; +using Discord.WebSocket; + +public class Program +{ + private DiscordSocketClient _client; + static void Main(string[] args) => new Program().MainAsync().GetAwaiter().GetResult(); + + public async Task MainAsync() + { + _client = new DiscordSocketClient(); + + await _client.LoginAsync(TokenType.Bot, "bot token"); + await _client.StartAsync(); + + Console.WriteLine("Press any key to exit..."); + Console.ReadKey(); + + await _client.StopAsync(); + // Wait a little for the client to finish disconnecting before allowing the program to return + await Task.Delay(500); + } +} \ No newline at end of file diff --git a/docs/guides/concepts/samples/entities.cs b/docs/guides/concepts/samples/entities.cs new file mode 100644 index 000000000..7655c44e9 --- /dev/null +++ b/docs/guides/concepts/samples/entities.cs @@ -0,0 +1,13 @@ +public string GetChannelTopic(ulong id) +{ + var channel = client.GetChannel(81384956881809408) as SocketTextChannel; + if (channel == null) return ""; + return channel.Topic; +} + +public string GuildOwner(SocketChannel channel) +{ + var guild = (channel as SocketGuildChannel)?.Guild; + if (guild == null) return ""; + return Context.Guild.Owner.Username; +} \ No newline at end of file diff --git a/docs/guides/concepts/samples/events.cs b/docs/guides/concepts/samples/events.cs new file mode 100644 index 000000000..c662b51a9 --- /dev/null +++ b/docs/guides/concepts/samples/events.cs @@ -0,0 +1,31 @@ +using Discord; +using Discord.WebSocket; + +public class Program +{ + private DiscordSocketClient _client; + static void Main(string[] args) => new Program().MainAsync().GetAwaiter().GetResult(); + + public async Task MainAsync() + { + _client = new DiscordSocketClient(); + + await _client.LoginAsync(TokenType.Bot, "bot token"); + await _client.StartAsync(); + + _client.MessageUpdated += MessageUpdated; + _client.Ready += () => + { + Console.WriteLine("Bot is connected!"); + return Task.CompletedTask; + } + + await Task.Delay(-1); + } + + private async Task MessageUpdated(Cacheable before, SocketMessage after, ISocketMessageChannel channel) + { + var message = await before.GetOrDownloadAsync(); + Console.WriteLine($"{message} -> {after}"); + } +} \ No newline at end of file diff --git a/docs/guides/getting_started/intro.md b/docs/guides/getting_started/intro.md index e0b41e22a..8bcfa9086 100644 --- a/docs/guides/getting_started/intro.md +++ b/docs/guides/getting_started/intro.md @@ -205,7 +205,7 @@ For your reference, you may view the [completed program]. [MessageReceived]: xref:Discord.WebSocket.DiscordSocketClient#Discord_WebSocket_DiscordSocketClient_MessageReceived [SocketMessage]: xref:Discord.WebSocket.SocketMessage -[SocketMessageChannel]: xref:Discord.WebSocket.SocketMessageChannel +[SocketMessageChannel]: xref:Discord.WebSocket.ISocketMessageChannel [completed program]: samples/intro/complete.cs # Building a bot with commands diff --git a/docs/guides/toc.yml b/docs/guides/toc.yml index 4a2c4d2da..2e3a61e19 100644 --- a/docs/guides/toc.yml +++ b/docs/guides/toc.yml @@ -12,7 +12,16 @@ href: concepts/logging.md - name: Working with Events href: concepts/events.md + - name: Managing Connections + href: concepts/connections.md - name: Entities + href: concepts/entities.md - name: The Command Service + items: + - name: Command Guide + href: commands/commands.md - name: Voice + items: + - name: Voice Guide + href: voice/sending-voice.md - name: Migrating from 0.9 \ No newline at end of file diff --git a/docs/guides/voice/sending-voice.md b/docs/guides/voice/sending-voice.md index f1ca3d0a5..c3ec8d9d7 100644 --- a/docs/guides/voice/sending-voice.md +++ b/docs/guides/voice/sending-voice.md @@ -5,7 +5,8 @@ title: Sending Voice **Information on this page is subject to change!** >[!WARNING] ->Audio in 1.0 is in progress +>This article is out of date, and has not been rewritten yet. +Information is not guaranteed to be accurate. ## Installing diff --git a/docs/index.md b/docs/index.md index 3f0393d4f..ef9ecdfdd 100644 --- a/docs/index.md +++ b/docs/index.md @@ -3,7 +3,7 @@ Discord.Net is an asynchronous, multiplatform .NET Library used to interface with the [Discord API](https://discordapp.com/). -If this is your first time using Discord.Net, you should refer to the [Intro](guides/intro.md) for tutorials. +If this is your first time using Discord.Net, you should refer to the [Intro](guides/getting_started/intro.md) for tutorials. More experienced users might refer to the [API Documentation](api/index.md) for a breakdown of the individuals objects in the library. For additional resources: From e49122ea7e7371f22799714e1c69f4e3f4e86d78 Mon Sep 17 00:00:00 2001 From: RogueException Date: Mon, 3 Apr 2017 19:59:03 -0300 Subject: [PATCH 101/243] Automatically toggle speaking boolean --- src/Discord.Net.WebSocket/Audio/AudioClient.cs | 2 +- .../Audio/Streams/BufferedWriteStream.cs | 3 +++ .../Audio/Streams/OutputStream.cs | 10 ++++++++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net.WebSocket/Audio/AudioClient.cs b/src/Discord.Net.WebSocket/Audio/AudioClient.cs index d5e9895a0..899951f60 100644 --- a/src/Discord.Net.WebSocket/Audio/AudioClient.cs +++ b/src/Discord.Net.WebSocket/Audio/AudioClient.cs @@ -239,7 +239,7 @@ namespace Discord.Audio throw new InvalidOperationException($"Discord selected an unexpected mode: {data.Mode}"); _secretKey = data.SecretKey; - await ApiClient.SendSetSpeaking(true).ConfigureAwait(false); + //await ApiClient.SendSetSpeaking(true).ConfigureAwait(false); var _ = _connection.CompleteAsync(); } diff --git a/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs index 5c402785e..4fc50b593 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs @@ -78,6 +78,7 @@ namespace Discord.Audio.Streams Frame frame; if (_queuedFrames.TryDequeue(out frame)) { + await _next.SetSpeakingAsync(true).ConfigureAwait(false); await _next.WriteAsync(frame.Buffer, 0, frame.Bytes).ConfigureAwait(false); _bufferPool.Enqueue(frame.Buffer); _queueLock.Release(); @@ -93,6 +94,8 @@ namespace Discord.Audio.Streams { if (_silenceFrames++ < MaxSilenceFrames) await _next.WriteAsync(_silenceFrame, 0, _silenceFrame.Length).ConfigureAwait(false); + else + await _next.SetSpeakingAsync(false).ConfigureAwait(false); nextTick += _ticksPerFrame; } #if DEBUG diff --git a/src/Discord.Net.WebSocket/Audio/Streams/OutputStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/OutputStream.cs index 6238e93b4..14072317a 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/OutputStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/OutputStream.cs @@ -6,6 +6,8 @@ namespace Discord.Audio.Streams /// Wraps an IAudioClient, sending voice data on write. public class OutputStream : AudioOutStream { + private bool _isSpeaking; + private readonly DiscordVoiceAPIClient _client; public OutputStream(IAudioClient client) : this((client as AudioClient).ApiClient) { } @@ -14,6 +16,14 @@ namespace Discord.Audio.Streams _client = client; } + public async Task SetSpeakingAsync(bool isSpeaking) + { + if (_isSpeaking != isSpeaking) + { + await _client.SendSetSpeaking(isSpeaking).ConfigureAwait(false); + _isSpeaking = isSpeaking; + } + } public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancelToken) { cancelToken.ThrowIfCancellationRequested(); From 91b61768f947758b0aa119312f85900cef44010a Mon Sep 17 00:00:00 2001 From: RogueException Date: Mon, 3 Apr 2017 20:31:16 -0300 Subject: [PATCH 102/243] Call SetSpeaking directly from BufferedWriteStream --- src/Discord.Net.WebSocket/Audio/AudioClient.cs | 4 ++-- .../Audio/Streams/BufferedWriteStream.cs | 12 +++++++----- .../Audio/Streams/OutputStream.cs | 10 ---------- 3 files changed, 9 insertions(+), 17 deletions(-) diff --git a/src/Discord.Net.WebSocket/Audio/AudioClient.cs b/src/Discord.Net.WebSocket/Audio/AudioClient.cs index 899951f60..8c49a73b9 100644 --- a/src/Discord.Net.WebSocket/Audio/AudioClient.cs +++ b/src/Discord.Net.WebSocket/Audio/AudioClient.cs @@ -136,7 +136,7 @@ namespace Discord.Audio var outputStream = new OutputStream(ApiClient); var sodiumEncrypter = new SodiumEncryptStream(outputStream, _secretKey); var rtpWriter = new RTPWriteStream(sodiumEncrypter, samplesPerFrame, _ssrc); - return new BufferedWriteStream(rtpWriter, samplesPerFrame, bufferMillis, _connection.CancelToken, _audioLogger); + return new BufferedWriteStream(rtpWriter, this, samplesPerFrame, bufferMillis, _connection.CancelToken, _audioLogger); } public AudioOutStream CreateDirectOpusStream(int samplesPerFrame) { @@ -151,7 +151,7 @@ namespace Discord.Audio var outputStream = new OutputStream(ApiClient); var sodiumEncrypter = new SodiumEncryptStream(outputStream, _secretKey); var rtpWriter = new RTPWriteStream(sodiumEncrypter, samplesPerFrame, _ssrc); - var bufferedStream = new BufferedWriteStream(rtpWriter, samplesPerFrame, bufferMillis, _connection.CancelToken, _audioLogger); + var bufferedStream = new BufferedWriteStream(rtpWriter, this, samplesPerFrame, bufferMillis, _connection.CancelToken, _audioLogger); return new OpusEncodeStream(bufferedStream, channels, samplesPerFrame, bitrate ?? (96 * 1024), application); } public AudioOutStream CreateDirectPCMStream(AudioApplication application, int samplesPerFrame, int channels, int? bitrate) diff --git a/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs index 4fc50b593..8e3a661ba 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs @@ -25,6 +25,7 @@ namespace Discord.Audio.Streams private static readonly byte[] _silenceFrame = new byte[0]; + private readonly AudioClient _client; private readonly AudioOutStream _next; private readonly CancellationTokenSource _cancelTokenSource; private readonly CancellationToken _cancelToken; @@ -37,12 +38,13 @@ namespace Discord.Audio.Streams private bool _isPreloaded; private int _silenceFrames; - public BufferedWriteStream(AudioOutStream next, int samplesPerFrame, int bufferMillis, CancellationToken cancelToken, int maxFrameSize = 1500) - : this(next, samplesPerFrame, bufferMillis, cancelToken, null, maxFrameSize) { } - internal BufferedWriteStream(AudioOutStream next, int samplesPerFrame, int bufferMillis, CancellationToken cancelToken, Logger logger, int maxFrameSize = 1500) + public BufferedWriteStream(AudioOutStream next, IAudioClient client, int samplesPerFrame, int bufferMillis, CancellationToken cancelToken, int maxFrameSize = 1500) + : this(next, client as AudioClient, samplesPerFrame, bufferMillis, cancelToken, null, maxFrameSize) { } + internal BufferedWriteStream(AudioOutStream next, AudioClient client, int samplesPerFrame, int bufferMillis, CancellationToken cancelToken, Logger logger, int maxFrameSize = 1500) { //maxFrameSize = 1275 was too limiting at 128kbps,2ch,60ms _next = next; + _client = client; _ticksPerFrame = samplesPerFrame / 48; _logger = logger; _queueLength = (bufferMillis + (_ticksPerFrame - 1)) / _ticksPerFrame; //Round up @@ -78,7 +80,7 @@ namespace Discord.Audio.Streams Frame frame; if (_queuedFrames.TryDequeue(out frame)) { - await _next.SetSpeakingAsync(true).ConfigureAwait(false); + await _client.ApiClient.SendSetSpeaking(true).ConfigureAwait(false); await _next.WriteAsync(frame.Buffer, 0, frame.Bytes).ConfigureAwait(false); _bufferPool.Enqueue(frame.Buffer); _queueLock.Release(); @@ -95,7 +97,7 @@ namespace Discord.Audio.Streams if (_silenceFrames++ < MaxSilenceFrames) await _next.WriteAsync(_silenceFrame, 0, _silenceFrame.Length).ConfigureAwait(false); else - await _next.SetSpeakingAsync(false).ConfigureAwait(false); + await _client.ApiClient.SendSetSpeaking(false).ConfigureAwait(false); nextTick += _ticksPerFrame; } #if DEBUG diff --git a/src/Discord.Net.WebSocket/Audio/Streams/OutputStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/OutputStream.cs index 14072317a..6238e93b4 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/OutputStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/OutputStream.cs @@ -6,8 +6,6 @@ namespace Discord.Audio.Streams /// Wraps an IAudioClient, sending voice data on write. public class OutputStream : AudioOutStream { - private bool _isSpeaking; - private readonly DiscordVoiceAPIClient _client; public OutputStream(IAudioClient client) : this((client as AudioClient).ApiClient) { } @@ -16,14 +14,6 @@ namespace Discord.Audio.Streams _client = client; } - public async Task SetSpeakingAsync(bool isSpeaking) - { - if (_isSpeaking != isSpeaking) - { - await _client.SendSetSpeaking(isSpeaking).ConfigureAwait(false); - _isSpeaking = isSpeaking; - } - } public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancelToken) { cancelToken.ThrowIfCancellationRequested(); From d7928622f3436b49877a8ad97809a2a6d0db5cbc Mon Sep 17 00:00:00 2001 From: RogueException Date: Mon, 3 Apr 2017 20:41:05 -0300 Subject: [PATCH 103/243] Guild presence should update global. Cleaned up. --- .../Entities/Users/SocketGlobalUser.cs | 6 ++++++ .../Entities/Users/SocketGuildUser.cs | 7 ++++--- src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs | 6 ------ 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs index 496ca7073..0cd5f749e 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using Model = Discord.API.User; +using PresenceModel = Discord.API.Presence; namespace Discord.WebSocket { @@ -47,6 +48,11 @@ namespace Discord.WebSocket } } + internal void Update(ClientState state, PresenceModel model) + { + Presence = SocketPresence.Create(model); + } + internal new SocketGlobalUser Clone() => MemberwiseClone() as SocketGlobalUser; } } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs index 63dc64dbf..f3c9166c4 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs @@ -99,12 +99,13 @@ namespace Discord.WebSocket if (model.Roles.IsSpecified) UpdateRoles(model.Roles.Value); } - internal override void Update(ClientState state, PresenceModel model) - => Update(state, model, true); internal void Update(ClientState state, PresenceModel model, bool updatePresence) { if (updatePresence) - base.Update(state, model); + { + Presence = SocketPresence.Create(model); + GlobalUser.Update(state, model); + } if (model.Nick.IsSpecified) Nickname = model.Nick.Value; if (model.Roles.IsSpecified) diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs index 12f1b2b30..da15ccbf9 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs @@ -2,7 +2,6 @@ using System; using System.Threading.Tasks; using Model = Discord.API.User; -using PresenceModel = Discord.API.Presence; namespace Discord.WebSocket { @@ -55,11 +54,6 @@ namespace Discord.WebSocket } return hasChanges; } - internal virtual void Update(ClientState state, PresenceModel model) - { - Presence = SocketPresence.Create(model); - //Update(state, model.User); - } public Task CreateDMChannelAsync(RequestOptions options = null) => UserHelper.CreateDMChannelAsync(this, Discord, options); From f8142a7744034a0076afa02a2a5044f2c087869b Mon Sep 17 00:00:00 2001 From: RogueException Date: Mon, 3 Apr 2017 21:26:38 -0300 Subject: [PATCH 104/243] Fixed metapackage's references --- src/Discord.Net/Discord.Net.nuspec | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Discord.Net/Discord.Net.nuspec b/src/Discord.Net/Discord.Net.nuspec index 3516bd208..2ce2c6c0f 100644 --- a/src/Discord.Net/Discord.Net.nuspec +++ b/src/Discord.Net/Discord.Net.nuspec @@ -13,20 +13,20 @@ false - - - - - - + + + + + + - - - - - - + + + + + + From ac0a31c3befca19ab0860cf830b0fe2d4b4e0533 Mon Sep 17 00:00:00 2001 From: RogueException Date: Mon, 3 Apr 2017 23:57:11 -0300 Subject: [PATCH 105/243] Send speaking during audio connect --- src/Discord.Net.WebSocket/Audio/AudioClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.WebSocket/Audio/AudioClient.cs b/src/Discord.Net.WebSocket/Audio/AudioClient.cs index 8c49a73b9..f1ba4782e 100644 --- a/src/Discord.Net.WebSocket/Audio/AudioClient.cs +++ b/src/Discord.Net.WebSocket/Audio/AudioClient.cs @@ -239,7 +239,7 @@ namespace Discord.Audio throw new InvalidOperationException($"Discord selected an unexpected mode: {data.Mode}"); _secretKey = data.SecretKey; - //await ApiClient.SendSetSpeaking(true).ConfigureAwait(false); + await ApiClient.SendSetSpeaking(false).ConfigureAwait(false); var _ = _connection.CompleteAsync(); } From c49118e25feea8aca48383a2ebb4d5d2226ca0bf Mon Sep 17 00:00:00 2001 From: RogueException Date: Tue, 4 Apr 2017 00:47:34 -0300 Subject: [PATCH 106/243] Fixed several audio stream issues --- src/Discord.Net.Core/Audio/AudioInStream.cs | 34 +++------------- src/Discord.Net.Core/Audio/AudioOutStream.cs | 29 +------------- src/Discord.Net.Core/Audio/AudioStream.cs | 40 +++++++++++++++++++ .../Audio/AudioClient.cs | 20 +++++----- .../Audio/Streams/BufferedWriteStream.cs | 6 +-- .../Audio/Streams/OpusDecodeStream.cs | 4 +- .../Audio/Streams/OpusEncodeStream.cs | 4 +- .../Audio/Streams/RTPReadStream.cs | 11 +++-- .../Audio/Streams/RTPWriteStream.cs | 4 +- .../Audio/Streams/SodiumDecryptStream.cs | 18 ++++----- .../Audio/Streams/SodiumEncryptStream.cs | 17 ++++---- 11 files changed, 89 insertions(+), 98 deletions(-) create mode 100644 src/Discord.Net.Core/Audio/AudioStream.cs diff --git a/src/Discord.Net.Core/Audio/AudioInStream.cs b/src/Discord.Net.Core/Audio/AudioInStream.cs index 86f1d8935..656c0bc48 100644 --- a/src/Discord.Net.Core/Audio/AudioInStream.cs +++ b/src/Discord.Net.Core/Audio/AudioInStream.cs @@ -1,43 +1,19 @@ using System; -using System.IO; using System.Threading; using System.Threading.Tasks; namespace Discord.Audio { - public abstract class AudioInStream : Stream + public abstract class AudioInStream : AudioStream { - public override bool CanRead => true; - public override bool CanSeek => false; - public override bool CanWrite => true; public abstract int AvailableFrames { get; } + public override bool CanRead => true; + public override bool CanWrite => true; + public abstract Task ReadFrameAsync(CancellationToken cancelToken); public abstract bool TryReadFrame(CancellationToken cancelToken, out RTPFrame frame); - public RTPFrame ReadFrame() - { - return ReadFrameAsync(CancellationToken.None).GetAwaiter().GetResult(); - } - public override int Read(byte[] buffer, int offset, int count) - { - return ReadAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult(); - } - public override void Write(byte[] buffer, int offset, int count) - { - WriteAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult(); - } - - public override void Flush() { throw new NotSupportedException(); } - - public override long Length { get { throw new NotSupportedException(); } } - public override long Position - { - get { throw new NotSupportedException(); } - set { throw new NotSupportedException(); } - } - - public override void SetLength(long value) { throw new NotSupportedException(); } - public override long Seek(long offset, SeekOrigin origin) { throw new NotSupportedException(); } + public override Task FlushAsync(CancellationToken cancelToken) { throw new NotSupportedException(); } } } diff --git a/src/Discord.Net.Core/Audio/AudioOutStream.cs b/src/Discord.Net.Core/Audio/AudioOutStream.cs index 2b4b012ee..7019ba8cd 100644 --- a/src/Discord.Net.Core/Audio/AudioOutStream.cs +++ b/src/Discord.Net.Core/Audio/AudioOutStream.cs @@ -1,39 +1,12 @@ using System; using System.IO; -using System.Threading; -using System.Threading.Tasks; namespace Discord.Audio { - public abstract class AudioOutStream : Stream + public abstract class AudioOutStream : AudioStream { - public override bool CanRead => false; - public override bool CanSeek => false; public override bool CanWrite => true; - public override void Write(byte[] buffer, int offset, int count) - { - WriteAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult(); - } - public override void Flush() - { - FlushAsync(CancellationToken.None).GetAwaiter().GetResult(); - } - public void Clear() - { - ClearAsync(CancellationToken.None).GetAwaiter().GetResult(); - } - - public virtual Task ClearAsync(CancellationToken cancellationToken) { return Task.Delay(0); } - //public virtual Task WriteSilenceAsync(CancellationToken cancellationToken) { return Task.Delay(0); } - - public override long Length { get { throw new NotSupportedException(); } } - public override long Position - { - get { throw new NotSupportedException(); } - set { throw new NotSupportedException(); } - } - public override int Read(byte[] buffer, int offset, int count) { throw new NotSupportedException(); } public override void SetLength(long value) { throw new NotSupportedException(); } public override long Seek(long offset, SeekOrigin origin) { throw new NotSupportedException(); } diff --git a/src/Discord.Net.Core/Audio/AudioStream.cs b/src/Discord.Net.Core/Audio/AudioStream.cs new file mode 100644 index 000000000..224409f8a --- /dev/null +++ b/src/Discord.Net.Core/Audio/AudioStream.cs @@ -0,0 +1,40 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Audio +{ + public abstract class AudioStream : Stream + { + public override bool CanRead => false; + public override bool CanSeek => false; + public override bool CanWrite => false; + + public override void Write(byte[] buffer, int offset, int count) + { + WriteAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult(); + } + public override void Flush() + { + FlushAsync(CancellationToken.None).GetAwaiter().GetResult(); + } + public void Clear() + { + ClearAsync(CancellationToken.None).GetAwaiter().GetResult(); + } + + public virtual Task ClearAsync(CancellationToken cancellationToken) { return Task.Delay(0); } + + public override long Length { get { throw new NotSupportedException(); } } + public override long Position + { + get { throw new NotSupportedException(); } + set { throw new NotSupportedException(); } + } + + public override int Read(byte[] buffer, int offset, int count) { throw new NotSupportedException(); } + public override void SetLength(long value) { throw new NotSupportedException(); } + public override long Seek(long offset, SeekOrigin origin) { throw new NotSupportedException(); } + } +} diff --git a/src/Discord.Net.WebSocket/Audio/AudioClient.cs b/src/Discord.Net.WebSocket/Audio/AudioClient.cs index f1ba4782e..717393951 100644 --- a/src/Discord.Net.WebSocket/Audio/AudioClient.cs +++ b/src/Discord.Net.WebSocket/Audio/AudioClient.cs @@ -42,12 +42,12 @@ namespace Discord.Audio private string _url, _sessionId, _token; private ulong _userId; private uint _ssrc; - private byte[] _secretKey; public SocketGuild Guild { get; } public DiscordVoiceAPIClient ApiClient { get; private set; } public int Latency { get; private set; } public ulong ChannelId { get; internal set; } + internal byte[] SecretKey { get; private set; } private DiscordSocketClient Discord => Guild.Discord; public ConnectionState ConnectionState => _connection.State; @@ -134,7 +134,7 @@ namespace Discord.Audio { CheckSamplesPerFrame(samplesPerFrame); var outputStream = new OutputStream(ApiClient); - var sodiumEncrypter = new SodiumEncryptStream(outputStream, _secretKey); + var sodiumEncrypter = new SodiumEncryptStream( outputStream, this); var rtpWriter = new RTPWriteStream(sodiumEncrypter, samplesPerFrame, _ssrc); return new BufferedWriteStream(rtpWriter, this, samplesPerFrame, bufferMillis, _connection.CancelToken, _audioLogger); } @@ -142,14 +142,14 @@ namespace Discord.Audio { CheckSamplesPerFrame(samplesPerFrame); var outputStream = new OutputStream(ApiClient); - var sodiumEncrypter = new SodiumEncryptStream(outputStream, _secretKey); + var sodiumEncrypter = new SodiumEncryptStream(outputStream, this); return new RTPWriteStream(sodiumEncrypter, samplesPerFrame, _ssrc); } public AudioOutStream CreatePCMStream(AudioApplication application, int samplesPerFrame, int channels, int? bitrate, int bufferMillis) { CheckSamplesPerFrame(samplesPerFrame); var outputStream = new OutputStream(ApiClient); - var sodiumEncrypter = new SodiumEncryptStream(outputStream, _secretKey); + var sodiumEncrypter = new SodiumEncryptStream(outputStream, this); var rtpWriter = new RTPWriteStream(sodiumEncrypter, samplesPerFrame, _ssrc); var bufferedStream = new BufferedWriteStream(rtpWriter, this, samplesPerFrame, bufferMillis, _connection.CancelToken, _audioLogger); return new OpusEncodeStream(bufferedStream, channels, samplesPerFrame, bitrate ?? (96 * 1024), application); @@ -158,7 +158,7 @@ namespace Discord.Audio { CheckSamplesPerFrame(samplesPerFrame); var outputStream = new OutputStream(ApiClient); - var sodiumEncrypter = new SodiumEncryptStream(outputStream, _secretKey); + var sodiumEncrypter = new SodiumEncryptStream(outputStream, this); var rtpWriter = new RTPWriteStream(sodiumEncrypter, samplesPerFrame, _ssrc); return new OpusEncodeStream(rtpWriter, channels, samplesPerFrame, bitrate ?? (96 * 1024), application); } @@ -175,8 +175,10 @@ namespace Discord.Audio if (!_streams.ContainsKey(userId)) { var readerStream = new InputStream(); - var writerStream = new OpusDecodeStream(new RTPReadStream(readerStream, _secretKey)); - _streams.TryAdd(userId, new StreamPair(readerStream, writerStream)); + var opusDecoder = new OpusDecodeStream(readerStream); + var rtpReader = new RTPReadStream(readerStream, opusDecoder); + var decryptStream = new SodiumDecryptStream(rtpReader, this); + _streams.TryAdd(userId, new StreamPair(readerStream, decryptStream)); await _streamCreatedEvent.InvokeAsync(userId, readerStream); } } @@ -238,7 +240,7 @@ namespace Discord.Audio if (data.Mode != DiscordVoiceAPIClient.Mode) throw new InvalidOperationException($"Discord selected an unexpected mode: {data.Mode}"); - _secretKey = data.SecretKey; + SecretKey = data.SecretKey; await ApiClient.SendSetSpeaking(false).ConfigureAwait(false); var _ = _connection.CompleteAsync(); @@ -335,7 +337,7 @@ namespace Discord.Audio await _audioLogger.DebugAsync($"Malformed Frame", ex).ConfigureAwait(false); return; } - await _audioLogger.DebugAsync($"Received {packet.Length} bytes from user {userId}").ConfigureAwait(false); + //await _audioLogger.DebugAsync($"Received {packet.Length} bytes from user {userId}").ConfigureAwait(false); } } diff --git a/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs index 8e3a661ba..d603f61a5 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs @@ -26,7 +26,7 @@ namespace Discord.Audio.Streams private static readonly byte[] _silenceFrame = new byte[0]; private readonly AudioClient _client; - private readonly AudioOutStream _next; + private readonly AudioStream _next; private readonly CancellationTokenSource _cancelTokenSource; private readonly CancellationToken _cancelToken; private readonly Task _task; @@ -38,9 +38,9 @@ namespace Discord.Audio.Streams private bool _isPreloaded; private int _silenceFrames; - public BufferedWriteStream(AudioOutStream next, IAudioClient client, int samplesPerFrame, int bufferMillis, CancellationToken cancelToken, int maxFrameSize = 1500) + public BufferedWriteStream(AudioStream next, IAudioClient client, int samplesPerFrame, int bufferMillis, CancellationToken cancelToken, int maxFrameSize = 1500) : this(next, client as AudioClient, samplesPerFrame, bufferMillis, cancelToken, null, maxFrameSize) { } - internal BufferedWriteStream(AudioOutStream next, AudioClient client, int samplesPerFrame, int bufferMillis, CancellationToken cancelToken, Logger logger, int maxFrameSize = 1500) + internal BufferedWriteStream(AudioStream next, AudioClient client, int samplesPerFrame, int bufferMillis, CancellationToken cancelToken, Logger logger, int maxFrameSize = 1500) { //maxFrameSize = 1275 was too limiting at 128kbps,2ch,60ms _next = next; diff --git a/src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs index 2dc5a8781..713814c74 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs @@ -8,11 +8,11 @@ namespace Discord.Audio.Streams { public const int SampleRate = OpusEncodeStream.SampleRate; - private readonly AudioOutStream _next; + private readonly AudioStream _next; private readonly byte[] _buffer; private readonly OpusDecoder _decoder; - public OpusDecodeStream(AudioOutStream next, int channels = OpusConverter.MaxChannels, int bufferSize = 4000) + public OpusDecodeStream(AudioStream next, int channels = OpusConverter.MaxChannels, int bufferSize = 4000) { _next = next; _buffer = new byte[bufferSize]; diff --git a/src/Discord.Net.WebSocket/Audio/Streams/OpusEncodeStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/OpusEncodeStream.cs index ada8311fe..ac6284c91 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/OpusEncodeStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/OpusEncodeStream.cs @@ -9,7 +9,7 @@ namespace Discord.Audio.Streams { public const int SampleRate = 48000; - private readonly AudioOutStream _next; + private readonly AudioStream _next; private readonly OpusEncoder _encoder; private readonly byte[] _buffer; @@ -17,7 +17,7 @@ namespace Discord.Audio.Streams private byte[] _partialFrameBuffer; private int _partialFramePos; - public OpusEncodeStream(AudioOutStream next, int channels, int samplesPerFrame, int bitrate, AudioApplication application, int bufferSize = 4000) + public OpusEncodeStream(AudioStream next, int channels, int samplesPerFrame, int bitrate, AudioApplication application, int bufferSize = 4000) { _next = next; _encoder = new OpusEncoder(SampleRate, channels, bitrate, application); diff --git a/src/Discord.Net.WebSocket/Audio/Streams/RTPReadStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/RTPReadStream.cs index b4aad9430..7fcef03e2 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/RTPReadStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/RTPReadStream.cs @@ -9,20 +9,19 @@ namespace Discord.Audio.Streams public class RTPReadStream : AudioOutStream { private readonly InputStream _queue; - private readonly AudioOutStream _next; - private readonly byte[] _buffer, _nonce, _secretKey; + private readonly AudioStream _next; + private readonly byte[] _buffer, _nonce; public override bool CanRead => true; public override bool CanSeek => false; public override bool CanWrite => true; - public RTPReadStream(InputStream queue, byte[] secretKey, int bufferSize = 4000) - : this(queue, null, secretKey, bufferSize) { } - public RTPReadStream(InputStream queue, AudioOutStream next, byte[] secretKey, int bufferSize = 4000) + public RTPReadStream(InputStream queue, int bufferSize = 4000) + : this(queue, null, bufferSize) { } + public RTPReadStream(InputStream queue, AudioStream next, int bufferSize = 4000) { _queue = queue; _next = next; - _secretKey = secretKey; _buffer = new byte[bufferSize]; _nonce = new byte[24]; } diff --git a/src/Discord.Net.WebSocket/Audio/Streams/RTPWriteStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/RTPWriteStream.cs index 836cb4852..b8d58c997 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/RTPWriteStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/RTPWriteStream.cs @@ -7,14 +7,14 @@ namespace Discord.Audio.Streams /// Wraps data in an RTP frame public class RTPWriteStream : AudioOutStream { - private readonly AudioOutStream _next; + private readonly AudioStream _next; private readonly byte[] _header; private int _samplesPerFrame; private uint _ssrc, _timestamp = 0; protected readonly byte[] _buffer; - public RTPWriteStream(AudioOutStream next, int samplesPerFrame, uint ssrc, int bufferSize = 4000) + public RTPWriteStream(AudioStream next, int samplesPerFrame, uint ssrc, int bufferSize = 4000) { _next = next; _samplesPerFrame = samplesPerFrame; diff --git a/src/Discord.Net.WebSocket/Audio/Streams/SodiumDecryptStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/SodiumDecryptStream.cs index f1421d28b..9ed849a5e 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/SodiumDecryptStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/SodiumDecryptStream.cs @@ -7,18 +7,18 @@ namespace Discord.Audio.Streams /// Decrypts an RTP frame using libsodium public class SodiumDecryptStream : AudioOutStream { - private readonly AudioOutStream _next; - private readonly byte[] _buffer, _nonce, _secretKey; + private readonly AudioClient _client; + private readonly AudioStream _next; + private readonly byte[] _nonce; public override bool CanRead => true; public override bool CanSeek => false; public override bool CanWrite => true; - public SodiumDecryptStream(AudioOutStream next, byte[] secretKey, int bufferSize = 4000) + public SodiumDecryptStream(AudioStream next, IAudioClient client) { _next = next; - _secretKey = secretKey; - _buffer = new byte[bufferSize]; + _client = (AudioClient)client; _nonce = new byte[24]; } @@ -26,11 +26,11 @@ namespace Discord.Audio.Streams { cancelToken.ThrowIfCancellationRequested(); - Buffer.BlockCopy(buffer, 0, _nonce, 0, 12); //Copy RTP header to nonce - count = SecretBox.Decrypt(buffer, offset, count, _buffer, 0, _nonce, _secretKey); + if (_client.SecretKey == null) + return; - var newBuffer = new byte[count]; - Buffer.BlockCopy(_buffer, 0, newBuffer, 0, count); + Buffer.BlockCopy(buffer, 0, _nonce, 0, 12); //Copy RTP header to nonce + count = SecretBox.Decrypt(buffer, offset + 12, count - 12, buffer, offset + 12, _nonce, _client.SecretKey); await _next.WriteAsync(buffer, 0, count + 12, cancelToken).ConfigureAwait(false); } diff --git a/src/Discord.Net.WebSocket/Audio/Streams/SodiumEncryptStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/SodiumEncryptStream.cs index 90bc35e9d..b00a7f403 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/SodiumEncryptStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/SodiumEncryptStream.cs @@ -7,16 +7,14 @@ namespace Discord.Audio.Streams /// Encrypts an RTP frame using libsodium public class SodiumEncryptStream : AudioOutStream { - private readonly AudioOutStream _next; - private readonly byte[] _nonce, _secretKey; + private readonly AudioClient _client; + private readonly AudioStream _next; + private readonly byte[] _nonce; - //protected readonly byte[] _buffer; - - public SodiumEncryptStream(AudioOutStream next, byte[] secretKey/*, int bufferSize = 4000*/) + public SodiumEncryptStream(AudioStream next, IAudioClient client) { _next = next; - _secretKey = secretKey; - //_buffer = new byte[bufferSize]; //TODO: Can Sodium do an in-place encrypt? + _client = (AudioClient)client; _nonce = new byte[24]; } @@ -24,8 +22,11 @@ namespace Discord.Audio.Streams { cancelToken.ThrowIfCancellationRequested(); + if (_client.SecretKey == null) + return; + Buffer.BlockCopy(buffer, offset, _nonce, 0, 12); //Copy nonce from RTP header - count = SecretBox.Encrypt(buffer, offset + 12, count - 12, buffer, 12, _nonce, _secretKey); + count = SecretBox.Encrypt(buffer, offset + 12, count - 12, buffer, 12, _nonce, _client.SecretKey); await _next.WriteAsync(buffer, 0, count + 12, cancelToken).ConfigureAwait(false); } From eed0598f99c2c757ad590cf42a023d73dd766080 Mon Sep 17 00:00:00 2001 From: RogueException Date: Tue, 4 Apr 2017 00:59:16 -0300 Subject: [PATCH 107/243] Destroy audio stream when a user disconnects --- src/Discord.Net.WebSocket/DiscordSocketClient.cs | 2 +- src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index e4f88ef45..f2f540d54 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -1440,7 +1440,7 @@ namespace Discord.WebSocket } else { - before = guild.RemoveVoiceState(data.UserId) ?? SocketVoiceState.Default; + before = await guild.RemoveVoiceStateAsync(data.UserId).ConfigureAwait(false) ?? SocketVoiceState.Default; after = SocketVoiceState.Create(null, data); } diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index 8193a971f..6345b8ddb 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -471,11 +471,15 @@ namespace Discord.WebSocket return voiceState; return null; } - internal SocketVoiceState? RemoveVoiceState(ulong id) + internal async Task RemoveVoiceStateAsync(ulong id) { SocketVoiceState voiceState; if (_voiceStates.TryRemove(id, out voiceState)) + { + if (_audioClient != null) + await _audioClient.RemoveInputStreamAsync(id).ConfigureAwait(false); //User changed channels, end their stream return voiceState; + } return null; } From 14f0535a4331b549b34d3fae4fd0764c95dcad8c Mon Sep 17 00:00:00 2001 From: RogueException Date: Wed, 5 Apr 2017 23:53:20 -0300 Subject: [PATCH 108/243] Improved typereader not found message --- src/Discord.Net.Commands/Builders/ParameterBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.Commands/Builders/ParameterBuilder.cs b/src/Discord.Net.Commands/Builders/ParameterBuilder.cs index c9801f458..6761033b0 100644 --- a/src/Discord.Net.Commands/Builders/ParameterBuilder.cs +++ b/src/Discord.Net.Commands/Builders/ParameterBuilder.cs @@ -93,7 +93,7 @@ namespace Discord.Commands.Builders internal ParameterInfo Build(CommandInfo info) { if (TypeReader == null) - throw new InvalidOperationException($"No default TypeReader found, one must be specified"); + throw new InvalidOperationException($"No type reader found for type {ParameterType.Name}, one must be specified"); return new ParameterInfo(this, info, Command.Module.Service); } From 61922283780f590e6893f99739b38b61ec480d00 Mon Sep 17 00:00:00 2001 From: RogueException Date: Thu, 6 Apr 2017 00:03:34 -0300 Subject: [PATCH 109/243] Raise GuildMembersDownloaded for non-large guilds --- src/Discord.Net.WebSocket/DiscordSocketClient.cs | 8 +++++++- src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs | 8 ++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index f2f540d54..96eccacfe 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -545,6 +545,12 @@ namespace Discord.WebSocket if (unavailableGuilds != 0) _unavailableGuilds = unavailableGuilds - 1; await GuildAvailableAsync(guild).ConfigureAwait(false); + + if (guild.DownloadedMemberCount >= guild.MemberCount && !guild.DownloaderPromise.IsCompleted) + { + guild.CompleteDownloadUsers(); + await TimedInvokeAsync(_guildMembersDownloadedEvent, nameof(GuildMembersDownloaded), guild).ConfigureAwait(false); + } } else { @@ -879,7 +885,7 @@ namespace Discord.WebSocket foreach (var memberModel in data.Members) guild.AddOrUpdateUser(memberModel); - if (guild.DownloadedMemberCount >= guild.MemberCount) //Finished downloading for there + if (guild.DownloadedMemberCount >= guild.MemberCount && !guild.DownloaderPromise.IsCompleted) { guild.CompleteDownloadUsers(); await TimedInvokeAsync(_guildMembersDownloadedEvent, nameof(GuildMembersDownloaded), guild).ConfigureAwait(false); diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index 6345b8ddb..1c2ee1847 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -190,8 +190,8 @@ namespace Discord.WebSocket if (Discord.ApiClient.AuthTokenType != TokenType.User) { var _ = _syncPromise.TrySetResultAsync(true); - if (!model.Large) - _ = _downloaderPromise.TrySetResultAsync(true); + /*if (!model.Large) + _ = _downloaderPromise.TrySetResultAsync(true);*/ } } internal void Update(ClientState state, Model model) @@ -258,8 +258,8 @@ namespace Discord.WebSocket _members = members; var _ = _syncPromise.TrySetResultAsync(true); - if (!model.Large) - _ = _downloaderPromise.TrySetResultAsync(true); + /*if (!model.Large) + _ = _downloaderPromise.TrySetResultAsync(true);*/ } internal void Update(ClientState state, EmojiUpdateModel model) From 653502c3715474f55c830ac620b9760bfdbb5c7c Mon Sep 17 00:00:00 2001 From: RogueException Date: Thu, 6 Apr 2017 00:10:25 -0300 Subject: [PATCH 110/243] Changed GuildUser.Roles to ReadOnlyCollection --- src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs index f3c9166c4..b92559a40 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs @@ -36,7 +36,7 @@ namespace Discord.WebSocket public bool IsDeafened => VoiceState?.IsDeafened ?? false; public bool IsMuted => VoiceState?.IsMuted ?? false; public DateTimeOffset? JoinedAt => DateTimeUtils.FromTicks(_joinedAtTicks); - public IEnumerable Roles + public IReadOnlyCollection Roles => _roleIds.Select(id => Guild.GetRole(id)).Where(x => x != null).ToReadOnlyCollection(() => _roleIds.Length); public SocketVoiceChannel VoiceChannel => VoiceState?.VoiceChannel; public string VoiceSessionId => VoiceState?.VoiceSessionId ?? ""; From 0d361def934a08fd1f0b31ee0ac11520d60a7420 Mon Sep 17 00:00:00 2001 From: RogueException Date: Thu, 6 Apr 2017 16:18:21 -0300 Subject: [PATCH 111/243] Fixed a couple incoming audio bugs --- src/Discord.Net.WebSocket/Audio/Opus/OpusDecoder.cs | 4 ++-- src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs | 2 +- src/Discord.Net.WebSocket/DiscordVoiceApiClient.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Discord.Net.WebSocket/Audio/Opus/OpusDecoder.cs b/src/Discord.Net.WebSocket/Audio/Opus/OpusDecoder.cs index b2ecf5987..c3e90789d 100644 --- a/src/Discord.Net.WebSocket/Audio/Opus/OpusDecoder.cs +++ b/src/Discord.Net.WebSocket/Audio/Opus/OpusDecoder.cs @@ -32,11 +32,11 @@ namespace Discord.Audio int result = 0; fixed (byte* inPtr = input) fixed (byte* outPtr = output) - result = Decode(_ptr, inPtr + inputOffset, inputCount, outPtr + outputOffset, (output.Length - outputOffset) / SampleSize / MaxChannels, 0); + result = Decode(_ptr, inPtr + inputOffset, inputCount, outPtr + outputOffset, (output.Length - outputOffset) / SampleSize, 0); //TODO: Enable FEC if (result < 0) throw new Exception($"Opus Error: {(OpusError)result}"); - return result; + return result * 4; } protected override void Dispose(bool disposing) diff --git a/src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs index 713814c74..6c9d8b233 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs @@ -12,7 +12,7 @@ namespace Discord.Audio.Streams private readonly byte[] _buffer; private readonly OpusDecoder _decoder; - public OpusDecodeStream(AudioStream next, int channels = OpusConverter.MaxChannels, int bufferSize = 4000) + public OpusDecodeStream(AudioStream next, int channels = OpusConverter.MaxChannels, int bufferSize = 5760 * 4) { _next = next; _buffer = new byte[bufferSize]; diff --git a/src/Discord.Net.WebSocket/DiscordVoiceApiClient.cs b/src/Discord.Net.WebSocket/DiscordVoiceApiClient.cs index fe5283ef3..f66050bc1 100644 --- a/src/Discord.Net.WebSocket/DiscordVoiceApiClient.cs +++ b/src/Discord.Net.WebSocket/DiscordVoiceApiClient.cs @@ -54,7 +54,7 @@ namespace Discord.Audio _udp = udpSocketProvider(); _udp.ReceivedDatagram += async (data, index, count) => { - if (index != 0) + if (index != 0 || count != data.Length) { var newData = new byte[count]; Buffer.BlockCopy(data, index, newData, 0, count); From 33cd1a6c9f8c17157066bbd25fa401de237eac29 Mon Sep 17 00:00:00 2001 From: RogueException Date: Thu, 6 Apr 2017 20:29:42 -0300 Subject: [PATCH 112/243] Scan base types during DI injection --- .../Utilities/ReflectionUtils.cs | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs b/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs index 2eaa6a882..5c817183b 100644 --- a/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs +++ b/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs @@ -1,14 +1,32 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Reflection; namespace Discord.Commands { - internal class ReflectionUtils + internal static class ReflectionUtils { + private static readonly TypeInfo objectTypeInfo = typeof(object).GetTypeInfo(); + internal static T CreateObject(TypeInfo typeInfo, CommandService service, IDependencyMap map = null) => CreateBuilder(typeInfo, service)(map); + private static System.Reflection.PropertyInfo[] GetProperties(TypeInfo typeInfo) + { + var result = new List(); + while (typeInfo != objectTypeInfo) + { + foreach (var prop in typeInfo.DeclaredProperties) + { + if (prop.SetMethod?.IsPublic == true && prop.GetCustomAttribute() == null) + result.Add(prop); + } + typeInfo = typeInfo.BaseType.GetTypeInfo(); + } + return result.ToArray(); + } + internal static Func CreateBuilder(TypeInfo typeInfo, CommandService service) { var constructors = typeInfo.DeclaredConstructors.Where(x => !x.IsStatic).ToArray(); @@ -19,7 +37,7 @@ namespace Discord.Commands var constructor = constructors[0]; System.Reflection.ParameterInfo[] parameters = constructor.GetParameters(); - System.Reflection.PropertyInfo[] properties = typeInfo.DeclaredProperties + System.Reflection.PropertyInfo[] properties = GetProperties(typeInfo) .Where(p => p.SetMethod?.IsPublic == true && p.GetCustomAttribute() == null) .ToArray(); From cbb38bd101ce142a35bb8673c888a0f216a4a8cf Mon Sep 17 00:00:00 2001 From: RogueException Date: Thu, 6 Apr 2017 20:30:11 -0300 Subject: [PATCH 113/243] Hide Entity.Discord property --- src/Discord.Net.Rest/Entities/RestEntity.cs | 2 +- src/Discord.Net.Rpc/Entities/RpcEntity.cs | 2 +- src/Discord.Net.WebSocket/Entities/SocketEntity.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Discord.Net.Rest/Entities/RestEntity.cs b/src/Discord.Net.Rest/Entities/RestEntity.cs index f893600ba..2b1bb888c 100644 --- a/src/Discord.Net.Rest/Entities/RestEntity.cs +++ b/src/Discord.Net.Rest/Entities/RestEntity.cs @@ -5,7 +5,7 @@ namespace Discord.Rest public abstract class RestEntity : IEntity where T : IEquatable { - public BaseDiscordClient Discord { get; } + internal BaseDiscordClient Discord { get; } public T Id { get; } internal RestEntity(BaseDiscordClient discord, T id) diff --git a/src/Discord.Net.Rpc/Entities/RpcEntity.cs b/src/Discord.Net.Rpc/Entities/RpcEntity.cs index e5b26cbe7..3827175bb 100644 --- a/src/Discord.Net.Rpc/Entities/RpcEntity.cs +++ b/src/Discord.Net.Rpc/Entities/RpcEntity.cs @@ -5,7 +5,7 @@ namespace Discord.Rpc public abstract class RpcEntity : IEntity where T : IEquatable { - public DiscordRpcClient Discord { get; } + internal DiscordRpcClient Discord { get; } public T Id { get; } internal RpcEntity(DiscordRpcClient discord, T id) diff --git a/src/Discord.Net.WebSocket/Entities/SocketEntity.cs b/src/Discord.Net.WebSocket/Entities/SocketEntity.cs index db1b7dc4f..c8e14fb6c 100644 --- a/src/Discord.Net.WebSocket/Entities/SocketEntity.cs +++ b/src/Discord.Net.WebSocket/Entities/SocketEntity.cs @@ -5,7 +5,7 @@ namespace Discord.WebSocket public abstract class SocketEntity : IEntity where T : IEquatable { - public DiscordSocketClient Discord { get; } + internal DiscordSocketClient Discord { get; } public T Id { get; } internal SocketEntity(DiscordSocketClient discord, T id) From d60d1e4a0341d451a46d8de2c6b117bc98363fd6 Mon Sep 17 00:00:00 2001 From: RogueException Date: Thu, 6 Apr 2017 20:30:21 -0300 Subject: [PATCH 114/243] Fixed Webhook avatarUrls --- src/Discord.Net.Webhook/DiscordWebhookClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.Webhook/DiscordWebhookClient.cs b/src/Discord.Net.Webhook/DiscordWebhookClient.cs index 201ed8f09..014f57ce0 100644 --- a/src/Discord.Net.Webhook/DiscordWebhookClient.cs +++ b/src/Discord.Net.Webhook/DiscordWebhookClient.cs @@ -55,7 +55,7 @@ namespace Discord.Webhook if (username != null) args.Username = username; if (avatarUrl != null) - args.AvatarUrl = username; + args.AvatarUrl = avatarUrl; await ApiClient.CreateWebhookMessageAsync(_webhookId, args, options).ConfigureAwait(false); } From 17ba8fe4d07d4ce72dcabede00b2ff60286992ac Mon Sep 17 00:00:00 2001 From: RogueException Date: Thu, 6 Apr 2017 20:50:48 -0300 Subject: [PATCH 115/243] Better handle the primary alias. --- src/Discord.Net.Commands/Builders/CommandBuilder.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net.Commands/Builders/CommandBuilder.cs b/src/Discord.Net.Commands/Builders/CommandBuilder.cs index 27f991b16..465c9ff0e 100644 --- a/src/Discord.Net.Commands/Builders/CommandBuilder.cs +++ b/src/Discord.Net.Commands/Builders/CommandBuilder.cs @@ -17,6 +17,7 @@ namespace Discord.Commands.Builders public string Name { get; set; } public string Summary { get; set; } public string Remarks { get; set; } + public string PrimaryAlias { get; set; } public RunMode RunMode { get; set; } public int Priority { get; set; } @@ -41,6 +42,7 @@ namespace Discord.Commands.Builders Discord.Preconditions.NotNull(callback, nameof(callback)); Callback = callback; + PrimaryAlias = primaryAlias; _aliases.Add(primaryAlias); } @@ -111,7 +113,7 @@ namespace Discord.Commands.Builders { //Default name to first alias if (Name == null) - Name = _aliases[0]; + Name = PrimaryAlias; if (_parameters.Count > 0) { From bceb72dd92435311f0bcc391be0ad6da71d897ea Mon Sep 17 00:00:00 2001 From: RogueException Date: Thu, 6 Apr 2017 20:57:46 -0300 Subject: [PATCH 116/243] Typo --- src/Discord.Net.Commands/Builders/CommandBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.Commands/Builders/CommandBuilder.cs b/src/Discord.Net.Commands/Builders/CommandBuilder.cs index 465c9ff0e..c13ca10d4 100644 --- a/src/Discord.Net.Commands/Builders/CommandBuilder.cs +++ b/src/Discord.Net.Commands/Builders/CommandBuilder.cs @@ -111,7 +111,7 @@ namespace Discord.Commands.Builders internal CommandInfo Build(ModuleInfo info, CommandService service) { - //Default name to first alias + //Default name to primary alias if (Name == null) Name = PrimaryAlias; From a6469e9021af1b38f64dd777b5f63eae47fd245a Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 7 Apr 2017 05:48:33 -0300 Subject: [PATCH 117/243] Add support for void-returning commands --- .../Builders/ModuleClassBuilder.cs | 2 +- src/Discord.Net.Commands/Info/CommandInfo.cs | 25 +++++++++++++------ 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs index 82850b091..4e8ef2664 100644 --- a/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs +++ b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs @@ -252,7 +252,7 @@ namespace Discord.Commands private static bool IsValidCommandDefinition(MethodInfo methodInfo) { return methodInfo.IsDefined(typeof(CommandAttribute)) && - methodInfo.ReturnType == typeof(Task) && + (methodInfo.ReturnType == typeof(Task) || methodInfo.ReturnType == typeof(void)) && !methodInfo.IsStatic && !methodInfo.IsGenericMethod; } diff --git a/src/Discord.Net.Commands/Info/CommandInfo.cs b/src/Discord.Net.Commands/Info/CommandInfo.cs index d0bf25a4b..9abe6de32 100644 --- a/src/Discord.Net.Commands/Info/CommandInfo.cs +++ b/src/Discord.Net.Commands/Info/CommandInfo.cs @@ -1,13 +1,13 @@ +using Discord.Commands.Builders; using System; -using System.Linq; using System.Collections.Generic; using System.Collections.Immutable; using System.Collections.Concurrent; -using System.Threading.Tasks; -using System.Reflection; - -using Discord.Commands.Builders; using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Runtime.ExceptionServices; +using System.Threading.Tasks; namespace Discord.Commands { @@ -166,10 +166,19 @@ namespace Discord.Commands } catch (Exception ex) { - ex = new CommandException(this, context, ex); - await Module.Service._cmdLogger.ErrorAsync(ex).ConfigureAwait(false); + var originalEx = ex; + while (ex is TargetInvocationException) //Happens with void-returning commands + ex = ex.InnerException; + + var wrappedEx = new CommandException(this, context, ex); + await Module.Service._cmdLogger.ErrorAsync(wrappedEx).ConfigureAwait(false); if (Module.Service._throwOnError) - throw; + { + if (ex == originalEx) + throw; + else + ExceptionDispatchInfo.Capture(ex).Throw(); + } } await Module.Service._cmdLogger.VerboseAsync($"Executed {GetLogText(context)}").ConfigureAwait(false); } From 284af7b6e2941e133062589f23c28065d0756b2f Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 7 Apr 2017 05:49:14 -0300 Subject: [PATCH 118/243] Support large DeleteMessages collections --- .../Entities/Channels/ChannelHelper.cs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs index efcadac0d..8fb26377f 100644 --- a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs +++ b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs @@ -181,8 +181,22 @@ namespace Discord.Rest public static async Task DeleteMessagesAsync(IMessageChannel channel, BaseDiscordClient client, IEnumerable messages, RequestOptions options) { - var args = new DeleteMessagesParams(messages.Select(x => x.Id).ToArray()); - await client.ApiClient.DeleteMessagesAsync(channel.Id, args, options).ConfigureAwait(false); + var msgs = messages.Select(x => x.Id).ToArray(); + if (msgs.Length < 100) + { + var args = new DeleteMessagesParams(msgs); + await client.ApiClient.DeleteMessagesAsync(channel.Id, args, options).ConfigureAwait(false); + } + else + { + var batch = new ulong[100]; + for (int i = 0; i < (msgs.Length + 99) / 100; i++) + { + Array.Copy(msgs, i * 100, batch, 0, Math.Min(msgs.Length - (100 * i), 100)); + var args = new DeleteMessagesParams(batch); + await client.ApiClient.DeleteMessagesAsync(channel.Id, args, options).ConfigureAwait(false); + } + } } //Permission Overwrites From 90c22bb07f64bf8230d42eb04618fcfa7765d4dd Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 7 Apr 2017 05:59:33 -0300 Subject: [PATCH 119/243] Added webhook package and updated compilation deps --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 903fa76c1..237336f16 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ The individual components may also be installed from NuGet: - [Discord.Net.Rest](https://www.nuget.org/packages/Discord.Net.Rest/) - [Discord.Net.Rpc](https://www.nuget.org/packages/Discord.Net.Rpc/) - [Discord.Net.WebSocket](https://www.nuget.org/packages/Discord.Net.WebSocket/) +- [Discord.Net.Webhook](https://www.nuget.org/packages/Discord.Net.Webhook/) - [Discord.Net.Commands](https://www.nuget.org/packages/Discord.Net.Commands/) The following providers are available for platforms not supporting .NET Standard 1.3: @@ -29,13 +30,13 @@ Nightly builds are available through our MyGet feed (`https://www.myget.org/F/di In order to compile Discord.Net, you require the following: ### Using Visual Studio -- [Visual Studio 2017 RC](https://www.microsoft.com/net/core#windowsvs2017) -- [.NET Core SDK 1.0 RC3](https://github.com/dotnet/core/blob/master/release-notes/rc3-download.md) +- [Visual Studio 2017](https://www.microsoft.com/net/core#windowsvs2017) +- [.NET Core SDK](https://www.microsoft.com/net/download/core) -The .NET Core and Docker (Preview) workload is required during Visual Studio installation. +The .NET Core workload must be selected during Visual Studio installation. ### Using Command Line -- [.NET Core SDK 1.0 RC3](https://github.com/dotnet/core/blob/master/release-notes/rc3-download.md) +- [.NET Core SDK](https://www.microsoft.com/net/download/core) ## Known Issues From 3d657f83797d4c9ef8932491eacb2a90dd85bf14 Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 7 Apr 2017 06:03:39 -0300 Subject: [PATCH 120/243] Added NuGet shield --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 237336f16..c932f6b25 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # Discord.Net v1.0.0-rc +[![NuGet](https://img.shields.io/nuget/vpre/Discord.Net.svg?maxAge=2592000?style=plastic)](https://www.nuget.org/packages/Discord.Net) [![MyGet](https://img.shields.io/myget/discord-net/vpre/Discord.Net.svg)](https://www.myget.org/feed/Packages/discord-net) [![Build status](https://ci.appveyor.com/api/projects/status/5sb7n8a09w9clute/branch/dev?svg=true)](https://ci.appveyor.com/project/RogueException/discord-net/branch/dev) [![Discord](https://discordapp.com/api/guilds/81384788765712384/widget.png)](https://discord.gg/0SBTUU1wZTVjAMPx) From d2a4f1f09b68d5ffcb597e26f9448eec5405f4d2 Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 7 Apr 2017 18:35:51 -0300 Subject: [PATCH 121/243] Strip RTP header during read --- src/Discord.Net.WebSocket/Audio/Streams/RTPReadStream.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/Discord.Net.WebSocket/Audio/Streams/RTPReadStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/RTPReadStream.cs index 7fcef03e2..72d9fc63b 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/RTPReadStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/RTPReadStream.cs @@ -1,5 +1,4 @@ -using System; -using System.IO; +using System.IO; using System.Threading; using System.Threading.Tasks; @@ -33,9 +32,6 @@ namespace Discord.Audio.Streams if (buffer[offset + 0] != 0x80 || buffer[offset + 1] != 0x78) return; - var payload = new byte[count - 12]; - Buffer.BlockCopy(buffer, offset + 12, payload, 0, count - 12); - ushort seq = (ushort)((buffer[offset + 2] << 8) | (buffer[offset + 3] << 0)); @@ -45,7 +41,7 @@ namespace Discord.Audio.Streams (buffer[offset + 7] << 0)); _queue.WriteHeader(seq, timestamp); - await (_next ?? _queue as Stream).WriteAsync(buffer, offset, count, cancelToken).ConfigureAwait(false); + await (_next ?? _queue as Stream).WriteAsync(buffer, offset + 12, count - 12, cancelToken).ConfigureAwait(false); } public static bool TryReadSsrc(byte[] buffer, int offset, out uint ssrc) From 483d26093b767a075bc35ebdb2203890eb42f4a9 Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 7 Apr 2017 21:14:54 -0300 Subject: [PATCH 122/243] Bump up Opus PLP to 30 --- src/Discord.Net.WebSocket/Audio/Opus/OpusEncoder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.WebSocket/Audio/Opus/OpusEncoder.cs b/src/Discord.Net.WebSocket/Audio/Opus/OpusEncoder.cs index 1f0b35d77..c85e21834 100644 --- a/src/Discord.Net.WebSocket/Audio/Opus/OpusEncoder.cs +++ b/src/Discord.Net.WebSocket/Audio/Opus/OpusEncoder.cs @@ -56,7 +56,7 @@ namespace Discord.Audio if (result < 0) throw new Exception($"Opus Error: {(OpusError)result}"); - result = EncoderCtl(_ptr, OpusCtl.SetPacketLossPercent, 5); //%% + result = EncoderCtl(_ptr, OpusCtl.SetPacketLossPercent, 30); //%% if (result < 0) throw new Exception($"Opus Error: {(OpusError)result}"); From 65154e0d4a61a4e7e2b1a32580d6737dfb4b1900 Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 7 Apr 2017 21:28:03 -0300 Subject: [PATCH 123/243] Enable FEC decoding --- src/Discord.Net.WebSocket/Audio/Opus/OpusConverter.cs | 2 +- src/Discord.Net.WebSocket/Audio/Opus/OpusDecoder.cs | 4 ++-- src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Discord.Net.WebSocket/Audio/Opus/OpusConverter.cs b/src/Discord.Net.WebSocket/Audio/Opus/OpusConverter.cs index 732006990..95874cdf1 100644 --- a/src/Discord.Net.WebSocket/Audio/Opus/OpusConverter.cs +++ b/src/Discord.Net.WebSocket/Audio/Opus/OpusConverter.cs @@ -7,7 +7,7 @@ namespace Discord.Audio protected IntPtr _ptr; /// Gets the bit rate of this converter. - public const int BitsPerSample = 16; + public const int BitsPerSample = sizeof(short) * 8; /// Gets the bytes per sample. public const int SampleSize = (BitsPerSample / 8) * MaxChannels; /// Gets the maximum amount of channels this encoder supports. diff --git a/src/Discord.Net.WebSocket/Audio/Opus/OpusDecoder.cs b/src/Discord.Net.WebSocket/Audio/Opus/OpusDecoder.cs index c3e90789d..a8c05d781 100644 --- a/src/Discord.Net.WebSocket/Audio/Opus/OpusDecoder.cs +++ b/src/Discord.Net.WebSocket/Audio/Opus/OpusDecoder.cs @@ -32,11 +32,11 @@ namespace Discord.Audio int result = 0; fixed (byte* inPtr = input) fixed (byte* outPtr = output) - result = Decode(_ptr, inPtr + inputOffset, inputCount, outPtr + outputOffset, (output.Length - outputOffset) / SampleSize, 0); //TODO: Enable FEC + result = Decode(_ptr, inPtr + inputOffset, inputCount, outPtr + outputOffset, (output.Length - outputOffset) / SampleSize, 1); if (result < 0) throw new Exception($"Opus Error: {(OpusError)result}"); - return result * 4; + return result * SampleSize; } protected override void Dispose(bool disposing) diff --git a/src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs index 6c9d8b233..c46e16cd3 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs @@ -12,7 +12,7 @@ namespace Discord.Audio.Streams private readonly byte[] _buffer; private readonly OpusDecoder _decoder; - public OpusDecodeStream(AudioStream next, int channels = OpusConverter.MaxChannels, int bufferSize = 5760 * 4) + public OpusDecodeStream(AudioStream next, int channels = OpusConverter.MaxChannels, int bufferSize = 5760 * 2 * sizeof(short)) { _next = next; _buffer = new byte[bufferSize]; From ee4cde69a4486bb22bebc64cc9e37e0f5cc6a812 Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 7 Apr 2017 22:52:07 -0300 Subject: [PATCH 124/243] Added UDP keepalives and latency --- .../Audio/AudioClient.Events.cs | 6 + .../Audio/AudioClient.cs | 176 +++++++++++++----- .../DiscordVoiceApiClient.cs | 16 ++ 3 files changed, 149 insertions(+), 49 deletions(-) diff --git a/src/Discord.Net.WebSocket/Audio/AudioClient.Events.cs b/src/Discord.Net.WebSocket/Audio/AudioClient.Events.cs index 20ebe7e6d..b3e438a01 100644 --- a/src/Discord.Net.WebSocket/Audio/AudioClient.Events.cs +++ b/src/Discord.Net.WebSocket/Audio/AudioClient.Events.cs @@ -23,6 +23,12 @@ namespace Discord.Audio remove { _latencyUpdatedEvent.Remove(value); } } private readonly AsyncEvent> _latencyUpdatedEvent = new AsyncEvent>(); + public event Func UdpLatencyUpdated + { + add { _udpLatencyUpdatedEvent.Add(value); } + remove { _udpLatencyUpdatedEvent.Remove(value); } + } + private readonly AsyncEvent> _udpLatencyUpdatedEvent = new AsyncEvent>(); public event Func StreamCreated { add { _streamCreatedEvent.Add(value); } diff --git a/src/Discord.Net.WebSocket/Audio/AudioClient.cs b/src/Discord.Net.WebSocket/Audio/AudioClient.cs index 717393951..39814f9bf 100644 --- a/src/Discord.Net.WebSocket/Audio/AudioClient.cs +++ b/src/Discord.Net.WebSocket/Audio/AudioClient.cs @@ -11,6 +11,7 @@ using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; +using System.Collections.Generic; namespace Discord.Audio { @@ -34,10 +35,11 @@ namespace Discord.Audio private readonly ConnectionManager _connection; private readonly SemaphoreSlim _stateLock; private readonly ConcurrentQueue _heartbeatTimes; + private readonly ConcurrentQueue> _keepaliveTimes; private readonly ConcurrentDictionary _ssrcMap; private readonly ConcurrentDictionary _streams; - private Task _heartbeatTask; + private Task _heartbeatTask, _keepaliveTask; private long _lastMessageTime; private string _url, _sessionId, _token; private ulong _userId; @@ -46,6 +48,7 @@ namespace Discord.Audio public SocketGuild Guild { get; } public DiscordVoiceAPIClient ApiClient { get; private set; } public int Latency { get; private set; } + public int UdpLatency { get; private set; } public ulong ChannelId { get; internal set; } internal byte[] SecretKey { get; private set; } @@ -72,6 +75,7 @@ namespace Discord.Audio _connection.Connected += () => _connectedEvent.InvokeAsync(); _connection.Disconnected += (ex, recon) => _disconnectedEvent.InvokeAsync(ex); _heartbeatTimes = new ConcurrentQueue(); + _keepaliveTimes = new ConcurrentQueue>(); _ssrcMap = new ConcurrentDictionary(); _streams = new ConcurrentDictionary(); @@ -83,6 +87,7 @@ namespace Discord.Audio }; LatencyUpdated += async (old, val) => await _audioLogger.DebugAsync($"Latency = {val} ms").ConfigureAwait(false); + UdpLatencyUpdated += async (old, val) => await _audioLogger.DebugAsync($"UDP Latency = {val} ms").ConfigureAwait(false); } internal async Task StartAsync(string url, ulong userId, string sessionId, string token) @@ -119,6 +124,10 @@ namespace Discord.Audio if (heartbeatTask != null) await heartbeatTask.ConfigureAwait(false); _heartbeatTask = null; + var keepaliveTask = _keepaliveTask; + if (keepaliveTask != null) + await keepaliveTask.ConfigureAwait(false); + _keepaliveTask = null; long time; while (_heartbeatTimes.TryDequeue(out time)) { } @@ -242,6 +251,7 @@ namespace Discord.Audio SecretKey = data.SecretKey; await ApiClient.SendSetSpeaking(false).ConfigureAwait(false); + _keepaliveTask = RunKeepaliveAsync(5000, _connection.CancelToken); var _ = _connection.CompleteAsync(); } @@ -284,60 +294,94 @@ namespace Discord.Audio } private async Task ProcessPacketAsync(byte[] packet) { - if (_connection.State == ConnectionState.Connecting) + try { - if (packet.Length != 70) - { - await _audioLogger.DebugAsync($"Malformed Packet").ConfigureAwait(false); - return; - } - string ip; - int port; - try + if (_connection.State == ConnectionState.Connecting) { - ip = Encoding.UTF8.GetString(packet, 4, 70 - 6).TrimEnd('\0'); - port = (packet[69] << 8) | packet[68]; + if (packet.Length != 70) + { + await _audioLogger.DebugAsync($"Malformed Packet").ConfigureAwait(false); + return; + } + string ip; + int port; + try + { + ip = Encoding.UTF8.GetString(packet, 4, 70 - 6).TrimEnd('\0'); + port = (packet[69] << 8) | packet[68]; + } + catch (Exception ex) + { + await _audioLogger.DebugAsync($"Malformed Packet", ex).ConfigureAwait(false); + return; + } + + await _audioLogger.DebugAsync("Received Discovery").ConfigureAwait(false); + await ApiClient.SendSelectProtocol(ip, port).ConfigureAwait(false); } - catch (Exception ex) + else if (_connection.State == ConnectionState.Connected) { - await _audioLogger.DebugAsync($"Malformed Packet", ex).ConfigureAwait(false); - return; + if (packet.Length == 8) + { + await _audioLogger.DebugAsync("Received Keepalive").ConfigureAwait(false); + + ulong value = + ((ulong)packet[0] >> 0) | + ((ulong)packet[1] >> 8) | + ((ulong)packet[2] >> 16) | + ((ulong)packet[3] >> 24) | + ((ulong)packet[4] >> 32) | + ((ulong)packet[5] >> 40) | + ((ulong)packet[6] >> 48) | + ((ulong)packet[7] >> 56); + + while (_keepaliveTimes.TryDequeue(out var pair)) + { + if (pair.Key == value) + { + int latency = (int)(Environment.TickCount - pair.Value); + int before = UdpLatency; + UdpLatency = latency; + + await _udpLatencyUpdatedEvent.InvokeAsync(before, latency).ConfigureAwait(false); + break; + } + } + } + else + { + if (!RTPReadStream.TryReadSsrc(packet, 0, out var ssrc)) + { + await _audioLogger.DebugAsync($"Malformed Frame").ConfigureAwait(false); + return; + } + if (!_ssrcMap.TryGetValue(ssrc, out var userId)) + { + await _audioLogger.DebugAsync($"Unknown SSRC {ssrc}").ConfigureAwait(false); + return; + } + if (!_streams.TryGetValue(userId, out var pair)) + { + await _audioLogger.DebugAsync($"Unknown User {userId}").ConfigureAwait(false); + return; + } + try + { + await pair.Writer.WriteAsync(packet, 0, packet.Length).ConfigureAwait(false); + } + catch (Exception ex) + { + await _audioLogger.DebugAsync($"Malformed Frame", ex).ConfigureAwait(false); + return; + } + //await _audioLogger.DebugAsync($"Received {packet.Length} bytes from user {userId}").ConfigureAwait(false); + } } - - await _audioLogger.DebugAsync("Received Discovery").ConfigureAwait(false); - await ApiClient.SendSelectProtocol(ip, port).ConfigureAwait(false); } - else if (_connection.State == ConnectionState.Connected) + catch (Exception ex) { - uint ssrc; - ulong userId; - StreamPair pair; - - if (!RTPReadStream.TryReadSsrc(packet, 0, out ssrc)) - { - await _audioLogger.DebugAsync($"Malformed Frame").ConfigureAwait(false); - return; - } - if (!_ssrcMap.TryGetValue(ssrc, out userId)) - { - await _audioLogger.DebugAsync($"Unknown SSRC {ssrc}").ConfigureAwait(false); - return; - } - if (!_streams.TryGetValue(userId, out pair)) - { - await _audioLogger.DebugAsync($"Unknown User {userId}").ConfigureAwait(false); - return; - } - try - { - await pair.Writer.WriteAsync(packet, 0, packet.Length).ConfigureAwait(false); - } - catch (Exception ex) - { - await _audioLogger.DebugAsync($"Malformed Frame", ex).ConfigureAwait(false); - return; - } - //await _audioLogger.DebugAsync($"Received {packet.Length} bytes from user {userId}").ConfigureAwait(false); + await _audioLogger.WarningAsync($"Failed to process UDP packet", ex).ConfigureAwait(false); + return; } } @@ -366,7 +410,7 @@ namespace Discord.Audio } catch (Exception ex) { - await _audioLogger.WarningAsync("Heartbeat Errored", ex).ConfigureAwait(false); + await _audioLogger.WarningAsync("Failed to send heartbeat", ex).ConfigureAwait(false); } await Task.Delay(intervalMillis, cancelToken).ConfigureAwait(false); @@ -382,6 +426,40 @@ namespace Discord.Audio await _audioLogger.ErrorAsync("Heartbeat Errored", ex).ConfigureAwait(false); } } + private async Task RunKeepaliveAsync(int intervalMillis, CancellationToken cancelToken) + { + var packet = new byte[8]; + try + { + await _audioLogger.DebugAsync("Keepalive Started").ConfigureAwait(false); + while (!cancelToken.IsCancellationRequested) + { + var now = Environment.TickCount; + + try + { + ulong value = await ApiClient.SendKeepaliveAsync().ConfigureAwait(false); + if (_keepaliveTimes.Count < 12) //No reply for 60 Seconds + _keepaliveTimes.Enqueue(new KeyValuePair(value, now)); + } + catch (Exception ex) + { + await _audioLogger.WarningAsync("Failed to send keepalive", ex).ConfigureAwait(false); + } + + await Task.Delay(intervalMillis, cancelToken).ConfigureAwait(false); + } + await _audioLogger.DebugAsync("Keepalive Stopped").ConfigureAwait(false); + } + catch (OperationCanceledException) + { + await _audioLogger.DebugAsync("Keepalive Stopped").ConfigureAwait(false); + } + catch (Exception ex) + { + await _audioLogger.ErrorAsync("Keepalive Errored", ex).ConfigureAwait(false); + } + } internal void Dispose(bool disposing) { diff --git a/src/Discord.Net.WebSocket/DiscordVoiceApiClient.cs b/src/Discord.Net.WebSocket/DiscordVoiceApiClient.cs index f66050bc1..05671d71d 100644 --- a/src/Discord.Net.WebSocket/DiscordVoiceApiClient.cs +++ b/src/Discord.Net.WebSocket/DiscordVoiceApiClient.cs @@ -42,6 +42,7 @@ namespace Discord.Audio private CancellationTokenSource _connectCancelToken; private IUdpSocket _udp; private bool _isDisposed; + private ulong _nextKeepalive; public ulong GuildId { get; } internal IWebSocketClient WebSocketClient { get; } @@ -227,6 +228,21 @@ namespace Discord.Audio await SendAsync(packet, 0, 70).ConfigureAwait(false); await _sentDiscoveryEvent.InvokeAsync().ConfigureAwait(false); } + public async Task SendKeepaliveAsync() + { + var value = _nextKeepalive++; + var packet = new byte[8]; + packet[0] = (byte)(value >> 0); + packet[1] = (byte)(value >> 8); + packet[2] = (byte)(value >> 16); + packet[3] = (byte)(value >> 24); + packet[4] = (byte)(value >> 32); + packet[5] = (byte)(value >> 40); + packet[6] = (byte)(value >> 48); + packet[7] = (byte)(value >> 56); + await SendAsync(packet, 0, 8).ConfigureAwait(false); + return value; + } public void SetUdpEndpoint(string ip, int port) { From b62c9dc315719f4e9f31dda0039353065e1fc4ad Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 7 Apr 2017 22:53:27 -0300 Subject: [PATCH 125/243] Added UdpLatency to IAudioClient --- src/Discord.Net.Core/Audio/IAudioClient.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net.Core/Audio/IAudioClient.cs b/src/Discord.Net.Core/Audio/IAudioClient.cs index 3ee008320..a292c5aa8 100644 --- a/src/Discord.Net.Core/Audio/IAudioClient.cs +++ b/src/Discord.Net.Core/Audio/IAudioClient.cs @@ -8,14 +8,17 @@ namespace Discord.Audio event Func Connected; event Func Disconnected; event Func LatencyUpdated; + event Func UdpLatencyUpdated; event Func StreamCreated; event Func StreamDestroyed; event Func SpeakingUpdated; /// Gets the current connection state of this client. ConnectionState ConnectionState { get; } - /// Gets the estimated round-trip latency, in milliseconds, to the gateway server. + /// Gets the estimated round-trip latency, in milliseconds, to the voice websocket server. int Latency { get; } + /// Gets the estimated round-trip latency, in milliseconds, to the voice UDP server. + int UdpLatency { get; } Task StopAsync(); From 22a7b7dbba6a79d3f757540f780a87944f268bd0 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 8 Apr 2017 02:34:12 -0300 Subject: [PATCH 126/243] Support more incoming RTP packets types --- .../Audio/Streams/RTPReadStream.cs | 32 +++++++++++++++---- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/src/Discord.Net.WebSocket/Audio/Streams/RTPReadStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/RTPReadStream.cs index 72d9fc63b..292a9303a 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/RTPReadStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/RTPReadStream.cs @@ -29,8 +29,7 @@ namespace Discord.Audio.Streams { cancelToken.ThrowIfCancellationRequested(); - if (buffer[offset + 0] != 0x80 || buffer[offset + 1] != 0x78) - return; + int headerSize = GetHeaderSize(buffer, offset); ushort seq = (ushort)((buffer[offset + 2] << 8) | (buffer[offset + 3] << 0)); @@ -41,16 +40,21 @@ namespace Discord.Audio.Streams (buffer[offset + 7] << 0)); _queue.WriteHeader(seq, timestamp); - await (_next ?? _queue as Stream).WriteAsync(buffer, offset + 12, count - 12, cancelToken).ConfigureAwait(false); + await (_next ?? _queue as Stream).WriteAsync(buffer, offset + headerSize, count - headerSize, cancelToken).ConfigureAwait(false); } public static bool TryReadSsrc(byte[] buffer, int offset, out uint ssrc) { + ssrc = 0; if (buffer.Length - offset < 12) - { - ssrc = 0; return false; - } + + int version = (buffer[offset + 0] & 0b1100_0000) >> 6; + if (version != 2) + return false; + int type = (buffer[offset + 1] & 0b01111_1111); + if (type != 120) //Dynamic Discord type + return false; ssrc = (uint)((buffer[offset + 8] << 24) | (buffer[offset + 9] << 16) | @@ -58,5 +62,21 @@ namespace Discord.Audio.Streams (buffer[offset + 11] << 0)); return true; } + + public static int GetHeaderSize(byte[] buffer, int offset) + { + byte headerByte = buffer[offset]; + bool extension = (headerByte & 0b0001_0000) != 0; + int csics = (headerByte & 0b0000_1111) >> 4; + + if (!extension) + return 12 + csics * 4; + + int extensionOffset = offset + 12 + (csics * 4); + int extensionLength = + (buffer[extensionOffset + 2] << 8) | + (buffer[extensionOffset + 3]); + return extensionOffset + 4 + (extensionLength * 4); + } } } From 1d57d0cba6bd60d0f5a28630b349e75b36ccd89e Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 8 Apr 2017 02:50:19 -0300 Subject: [PATCH 127/243] Add support for invites without attached users --- src/Discord.Net.Rest/Entities/Invites/RestInviteMetadata.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.Rest/Entities/Invites/RestInviteMetadata.cs b/src/Discord.Net.Rest/Entities/Invites/RestInviteMetadata.cs index 9298b42e9..42aeb40aa 100644 --- a/src/Discord.Net.Rest/Entities/Invites/RestInviteMetadata.cs +++ b/src/Discord.Net.Rest/Entities/Invites/RestInviteMetadata.cs @@ -29,7 +29,7 @@ namespace Discord.Rest internal void Update(Model model) { base.Update(model); - Inviter = RestUser.Create(Discord, model.Inviter); + Inviter = model.Inviter != null ? RestUser.Create(Discord, model.Inviter) : null; IsRevoked = model.Revoked; IsTemporary = model.Temporary; MaxAge = model.MaxAge != 0 ? model.MaxAge : (int?)null; From 424216b7936ec79ec084dd6df71ef504d4b479d6 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 8 Apr 2017 15:44:00 -0300 Subject: [PATCH 128/243] Disable FEC decoding --- src/Discord.Net.WebSocket/Audio/Opus/OpusDecoder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.WebSocket/Audio/Opus/OpusDecoder.cs b/src/Discord.Net.WebSocket/Audio/Opus/OpusDecoder.cs index a8c05d781..605cd3467 100644 --- a/src/Discord.Net.WebSocket/Audio/Opus/OpusDecoder.cs +++ b/src/Discord.Net.WebSocket/Audio/Opus/OpusDecoder.cs @@ -32,7 +32,7 @@ namespace Discord.Audio int result = 0; fixed (byte* inPtr = input) fixed (byte* outPtr = output) - result = Decode(_ptr, inPtr + inputOffset, inputCount, outPtr + outputOffset, (output.Length - outputOffset) / SampleSize, 1); + result = Decode(_ptr, inPtr + inputOffset, inputCount, outPtr + outputOffset, (output.Length - outputOffset) / SampleSize, 0); //TODO: Enable FEC if (result < 0) throw new Exception($"Opus Error: {(OpusError)result}"); From e92cfd20ac6e08023fed80dba73f282cd5ef68e2 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 8 Apr 2017 17:12:10 -0300 Subject: [PATCH 129/243] Audio bugfixes and improvements. --- src/Discord.Net.Core/Audio/IAudioClient.cs | 34 +++-------- .../Audio/AudioClient.cs | 34 ++++------- .../Audio/Opus/OpusConverter.cs | 46 +++++++-------- .../Audio/Opus/OpusDecoder.cs | 28 ++++----- .../Audio/Opus/OpusEncoder.cs | 58 +++++-------------- .../Audio/Streams/BufferedWriteStream.cs | 8 +-- .../Audio/Streams/OpusDecodeStream.cs | 8 +-- .../Audio/Streams/OpusEncodeStream.cs | 36 +++++++----- .../Audio/Streams/RTPWriteStream.cs | 9 +-- 9 files changed, 98 insertions(+), 163 deletions(-) diff --git a/src/Discord.Net.Core/Audio/IAudioClient.cs b/src/Discord.Net.Core/Audio/IAudioClient.cs index a292c5aa8..c1c31af73 100644 --- a/src/Discord.Net.Core/Audio/IAudioClient.cs +++ b/src/Discord.Net.Core/Audio/IAudioClient.cs @@ -22,31 +22,13 @@ namespace Discord.Audio Task StopAsync(); - /// - /// Creates a new outgoing stream accepting Opus-encoded data. - /// - /// Samples per frame. Must be 120, 240, 480, 960, 1920 or 2880, representing 2.5, 5, 10, 20, 40 or 60 milliseconds respectively. - /// - AudioOutStream CreateOpusStream(int samplesPerFrame, int bufferMillis = 1000); - /// - /// Creates a new outgoing stream accepting Opus-encoded data. This is a direct stream with no internal timer. - /// - /// Samples per frame. Must be 120, 240, 480, 960, 1920 or 2880, representing 2.5, 5, 10, 20, 40 or 60 milliseconds respectively. - /// - AudioOutStream CreateDirectOpusStream(int samplesPerFrame); - /// - /// Creates a new outgoing stream accepting PCM (raw) data. - /// - /// Samples per frame. Must be 120, 240, 480, 960, 1920 or 2880, representing 2.5, 5, 10, 20, 40 or 60 milliseconds respectively. - /// - /// - AudioOutStream CreatePCMStream(AudioApplication application, int samplesPerFrame, int channels = 2, int? bitrate = null, int bufferMillis = 1000); - /// - /// Creates a new direct outgoing stream accepting PCM (raw) data. This is a direct stream with no internal timer. - /// - /// Samples per frame. Must be 120, 240, 480, 960, 1920 or 2880, representing 2.5, 5, 10, 20, 40 or 60 milliseconds respectively. - /// - /// - AudioOutStream CreateDirectPCMStream(AudioApplication application, int samplesPerFrame, int channels = 2, int? bitrate = null); + /// Creates a new outgoing stream accepting Opus-encoded data. + AudioOutStream CreateOpusStream(int bufferMillis = 1000); + /// Creates a new outgoing stream accepting Opus-encoded data. This is a direct stream with no internal timer. + AudioOutStream CreateDirectOpusStream(); + /// Creates a new outgoing stream accepting PCM (raw) data. + AudioOutStream CreatePCMStream(AudioApplication application, int? bitrate = null, int bufferMillis = 1000); + /// Creates a new direct outgoing stream accepting PCM (raw) data. This is a direct stream with no internal timer. + AudioOutStream CreateDirectPCMStream(AudioApplication application, int? bitrate = null); } } diff --git a/src/Discord.Net.WebSocket/Audio/AudioClient.cs b/src/Discord.Net.WebSocket/Audio/AudioClient.cs index 39814f9bf..c497b2632 100644 --- a/src/Discord.Net.WebSocket/Audio/AudioClient.cs +++ b/src/Discord.Net.WebSocket/Audio/AudioClient.cs @@ -139,43 +139,33 @@ namespace Discord.Audio await Discord.ApiClient.SendVoiceStateUpdateAsync(Guild.Id, null, false, false).ConfigureAwait(false); } - public AudioOutStream CreateOpusStream(int samplesPerFrame, int bufferMillis) + public AudioOutStream CreateOpusStream(int bufferMillis) { - CheckSamplesPerFrame(samplesPerFrame); var outputStream = new OutputStream(ApiClient); var sodiumEncrypter = new SodiumEncryptStream( outputStream, this); - var rtpWriter = new RTPWriteStream(sodiumEncrypter, samplesPerFrame, _ssrc); - return new BufferedWriteStream(rtpWriter, this, samplesPerFrame, bufferMillis, _connection.CancelToken, _audioLogger); + var rtpWriter = new RTPWriteStream(sodiumEncrypter, _ssrc); + return new BufferedWriteStream(rtpWriter, this, bufferMillis, _connection.CancelToken, _audioLogger); } - public AudioOutStream CreateDirectOpusStream(int samplesPerFrame) + public AudioOutStream CreateDirectOpusStream() { - CheckSamplesPerFrame(samplesPerFrame); var outputStream = new OutputStream(ApiClient); var sodiumEncrypter = new SodiumEncryptStream(outputStream, this); - return new RTPWriteStream(sodiumEncrypter, samplesPerFrame, _ssrc); + return new RTPWriteStream(sodiumEncrypter, _ssrc); } - public AudioOutStream CreatePCMStream(AudioApplication application, int samplesPerFrame, int channels, int? bitrate, int bufferMillis) + public AudioOutStream CreatePCMStream(AudioApplication application, int? bitrate, int bufferMillis) { - CheckSamplesPerFrame(samplesPerFrame); var outputStream = new OutputStream(ApiClient); var sodiumEncrypter = new SodiumEncryptStream(outputStream, this); - var rtpWriter = new RTPWriteStream(sodiumEncrypter, samplesPerFrame, _ssrc); - var bufferedStream = new BufferedWriteStream(rtpWriter, this, samplesPerFrame, bufferMillis, _connection.CancelToken, _audioLogger); - return new OpusEncodeStream(bufferedStream, channels, samplesPerFrame, bitrate ?? (96 * 1024), application); + var rtpWriter = new RTPWriteStream(sodiumEncrypter, _ssrc); + var bufferedStream = new BufferedWriteStream(rtpWriter, this, bufferMillis, _connection.CancelToken, _audioLogger); + return new OpusEncodeStream(bufferedStream, bitrate ?? (96 * 1024), application); } - public AudioOutStream CreateDirectPCMStream(AudioApplication application, int samplesPerFrame, int channels, int? bitrate) + public AudioOutStream CreateDirectPCMStream(AudioApplication application, int? bitrate) { - CheckSamplesPerFrame(samplesPerFrame); var outputStream = new OutputStream(ApiClient); var sodiumEncrypter = new SodiumEncryptStream(outputStream, this); - var rtpWriter = new RTPWriteStream(sodiumEncrypter, samplesPerFrame, _ssrc); - return new OpusEncodeStream(rtpWriter, channels, samplesPerFrame, bitrate ?? (96 * 1024), application); - } - private void CheckSamplesPerFrame(int samplesPerFrame) - { - if (samplesPerFrame != 120 && samplesPerFrame != 240 && samplesPerFrame != 480 && - samplesPerFrame != 960 && samplesPerFrame != 1920 && samplesPerFrame != 2880) - throw new ArgumentException("Value must be 120, 240, 480, 960, 1920 or 2880", nameof(samplesPerFrame)); + var rtpWriter = new RTPWriteStream(sodiumEncrypter, _ssrc); + return new OpusEncodeStream(rtpWriter, bitrate ?? (96 * 1024), application); } internal async Task CreateInputStreamAsync(ulong userId) diff --git a/src/Discord.Net.WebSocket/Audio/Opus/OpusConverter.cs b/src/Discord.Net.WebSocket/Audio/Opus/OpusConverter.cs index 95874cdf1..f802d65ad 100644 --- a/src/Discord.Net.WebSocket/Audio/Opus/OpusConverter.cs +++ b/src/Discord.Net.WebSocket/Audio/Opus/OpusConverter.cs @@ -6,37 +6,22 @@ namespace Discord.Audio { protected IntPtr _ptr; - /// Gets the bit rate of this converter. - public const int BitsPerSample = sizeof(short) * 8; - /// Gets the bytes per sample. - public const int SampleSize = (BitsPerSample / 8) * MaxChannels; - /// Gets the maximum amount of channels this encoder supports. - public const int MaxChannels = 2; + public const int SamplingRate = 48000; + public const int Channels = 2; + public const int FrameMillis = 20; - /// Gets the input sampling rate of this converter. - public int SamplingRate { get; } - /// Gets the number of samples per second for this stream. - public int Channels { get; } + public const int SampleBytes = sizeof(short) * Channels; - protected OpusConverter(int samplingRate, int channels) - { - if (samplingRate != 8000 && samplingRate != 12000 && - samplingRate != 16000 && samplingRate != 24000 && - samplingRate != 48000) - throw new ArgumentOutOfRangeException(nameof(samplingRate)); - if (channels != 1 && channels != 2) - throw new ArgumentOutOfRangeException(nameof(channels)); - - SamplingRate = samplingRate; - Channels = channels; - } + public const int FrameSamples = SamplingRate / 1000 * FrameMillis; + public const int FrameSamplesPerChannel = SamplingRate / 1000 * FrameMillis; + public const int FrameBytes = FrameSamples * SampleBytes; - private bool disposedValue = false; // To detect redundant calls + protected bool _isDisposed = false; protected virtual void Dispose(bool disposing) { - if (!disposedValue) - disposedValue = true; + if (!_isDisposed) + _isDisposed = true; } ~OpusConverter() { @@ -47,5 +32,16 @@ namespace Discord.Audio Dispose(true); GC.SuppressFinalize(this); } + + protected static void CheckError(int result) + { + if (result < 0) + throw new Exception($"Opus Error: {(OpusError)result}"); + } + protected static void CheckError(OpusError error) + { + if ((int)error < 0) + throw new Exception($"Opus Error: {error}"); + } } } diff --git a/src/Discord.Net.WebSocket/Audio/Opus/OpusDecoder.cs b/src/Discord.Net.WebSocket/Audio/Opus/OpusDecoder.cs index 605cd3467..c5c16dff6 100644 --- a/src/Discord.Net.WebSocket/Audio/Opus/OpusDecoder.cs +++ b/src/Discord.Net.WebSocket/Audio/Opus/OpusDecoder.cs @@ -14,37 +14,29 @@ namespace Discord.Audio [DllImport("opus", EntryPoint = "opus_decoder_ctl", CallingConvention = CallingConvention.Cdecl)] private static extern int DecoderCtl(IntPtr st, OpusCtl request, int value); - public OpusDecoder(int samplingRate, int channels) - : base(samplingRate, channels) + public OpusDecoder() { - OpusError error; - _ptr = CreateDecoder(samplingRate, channels, out error); - if (error != OpusError.OK) - throw new Exception($"Opus Error: {error}"); + _ptr = CreateDecoder(SamplingRate, Channels, out var error); + CheckError(error); } - /// Produces PCM samples from Opus-encoded audio. - /// PCM samples to decode. - /// Offset of the frame in input. - /// Buffer to store the decoded frame. public unsafe int DecodeFrame(byte[] input, int inputOffset, int inputCount, byte[] output, int outputOffset) { int result = 0; fixed (byte* inPtr = input) fixed (byte* outPtr = output) - result = Decode(_ptr, inPtr + inputOffset, inputCount, outPtr + outputOffset, (output.Length - outputOffset) / SampleSize, 0); //TODO: Enable FEC - - if (result < 0) - throw new Exception($"Opus Error: {(OpusError)result}"); - return result * SampleSize; + result = Decode(_ptr, inPtr + inputOffset, inputCount, outPtr + outputOffset, FrameBytes / SampleBytes, 1); + CheckError(result); + return FrameBytes; } protected override void Dispose(bool disposing) { - if (_ptr != IntPtr.Zero) + if (!_isDisposed) { - DestroyDecoder(_ptr); - _ptr = IntPtr.Zero; + if (_ptr != IntPtr.Zero) + DestroyDecoder(_ptr); + base.Dispose(disposing); } } } diff --git a/src/Discord.Net.WebSocket/Audio/Opus/OpusEncoder.cs b/src/Discord.Net.WebSocket/Audio/Opus/OpusEncoder.cs index c85e21834..a12854d69 100644 --- a/src/Discord.Net.WebSocket/Audio/Opus/OpusEncoder.cs +++ b/src/Discord.Net.WebSocket/Audio/Opus/OpusEncoder.cs @@ -12,14 +12,12 @@ namespace Discord.Audio [DllImport("opus", EntryPoint = "opus_encode", CallingConvention = CallingConvention.Cdecl)] private static extern int Encode(IntPtr st, byte* pcm, int frame_size, byte* data, int max_data_bytes); [DllImport("opus", EntryPoint = "opus_encoder_ctl", CallingConvention = CallingConvention.Cdecl)] - private static extern int EncoderCtl(IntPtr st, OpusCtl request, int value); + private static extern OpusError EncoderCtl(IntPtr st, OpusCtl request, int value); - /// Gets the coding mode of the encoder. public AudioApplication Application { get; } public int BitRate { get;} - public OpusEncoder(int samplingRate, int channels, int bitrate, AudioApplication application) - : base(samplingRate, channels) + public OpusEncoder(int bitrate, AudioApplication application) { if (bitrate < 1 || bitrate > DiscordVoiceAPIClient.MaxBitrate) throw new ArgumentOutOfRangeException(nameof(bitrate)); @@ -47,57 +45,31 @@ namespace Discord.Audio throw new ArgumentOutOfRangeException(nameof(application)); } - OpusError error; - _ptr = CreateEncoder(samplingRate, channels, (int)opusApplication, out error); - if (error != OpusError.OK) - throw new Exception($"Opus Error: {error}"); - - var result = EncoderCtl(_ptr, OpusCtl.SetSignal, (int)opusSignal); - if (result < 0) - throw new Exception($"Opus Error: {(OpusError)result}"); - - result = EncoderCtl(_ptr, OpusCtl.SetPacketLossPercent, 30); //%% - if (result < 0) - throw new Exception($"Opus Error: {(OpusError)result}"); - - result = EncoderCtl(_ptr, OpusCtl.SetInbandFEC, 1); //True - if (result < 0) - throw new Exception($"Opus Error: {(OpusError)result}"); - - result = EncoderCtl(_ptr, OpusCtl.SetBitrate, bitrate); - if (result < 0) - throw new Exception($"Opus Error: {(OpusError)result}"); - - /*if (application == AudioApplication.Music) - { - result = EncoderCtl(_ptr, OpusCtl.SetBandwidth, 1105); - if (result < 0) - throw new Exception($"Opus Error: {(OpusError)result}"); - }*/ + _ptr = CreateEncoder(SamplingRate, Channels, (int)opusApplication, out var error); + CheckError(error); + CheckError(EncoderCtl(_ptr, OpusCtl.SetSignal, (int)opusSignal)); + CheckError(EncoderCtl(_ptr, OpusCtl.SetPacketLossPercent, 30)); //% + CheckError(EncoderCtl(_ptr, OpusCtl.SetInbandFEC, 1)); //True + CheckError(EncoderCtl(_ptr, OpusCtl.SetBitrate, bitrate)); } - /// Produces Opus encoded audio from PCM samples. - /// PCM samples to encode. - /// Buffer to store the encoded frame. - /// Length of the frame contained in outputBuffer. - public unsafe int EncodeFrame(byte[] input, int inputOffset, int inputCount, byte[] output, int outputOffset) + public unsafe int EncodeFrame(byte[] input, int inputOffset, byte[] output, int outputOffset) { int result = 0; fixed (byte* inPtr = input) fixed (byte* outPtr = output) - result = Encode(_ptr, inPtr + inputOffset, inputCount / SampleSize, outPtr + outputOffset, output.Length - outputOffset); - - if (result < 0) - throw new Exception($"Opus Error: {(OpusError)result}"); + result = Encode(_ptr, inPtr + inputOffset, FrameSamplesPerChannel, outPtr + outputOffset, output.Length - outputOffset); + CheckError(result); return result; } protected override void Dispose(bool disposing) { - if (_ptr != IntPtr.Zero) + if (!_isDisposed) { - DestroyEncoder(_ptr); - _ptr = IntPtr.Zero; + if (_ptr != IntPtr.Zero) + DestroyEncoder(_ptr); + base.Dispose(disposing); } } } diff --git a/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs index d603f61a5..1764fa66a 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs @@ -38,14 +38,14 @@ namespace Discord.Audio.Streams private bool _isPreloaded; private int _silenceFrames; - public BufferedWriteStream(AudioStream next, IAudioClient client, int samplesPerFrame, int bufferMillis, CancellationToken cancelToken, int maxFrameSize = 1500) - : this(next, client as AudioClient, samplesPerFrame, bufferMillis, cancelToken, null, maxFrameSize) { } - internal BufferedWriteStream(AudioStream next, AudioClient client, int samplesPerFrame, int bufferMillis, CancellationToken cancelToken, Logger logger, int maxFrameSize = 1500) + public BufferedWriteStream(AudioStream next, IAudioClient client, int bufferMillis, CancellationToken cancelToken, int maxFrameSize = 1500) + : this(next, client as AudioClient, bufferMillis, cancelToken, null, maxFrameSize) { } + internal BufferedWriteStream(AudioStream next, AudioClient client, int bufferMillis, CancellationToken cancelToken, Logger logger, int maxFrameSize = 1500) { //maxFrameSize = 1275 was too limiting at 128kbps,2ch,60ms _next = next; _client = client; - _ticksPerFrame = samplesPerFrame / 48; + _ticksPerFrame = OpusEncoder.FrameSamples / 48; _logger = logger; _queueLength = (bufferMillis + (_ticksPerFrame - 1)) / _ticksPerFrame; //Round up diff --git a/src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs index c46e16cd3..96c809cca 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs @@ -9,14 +9,14 @@ namespace Discord.Audio.Streams public const int SampleRate = OpusEncodeStream.SampleRate; private readonly AudioStream _next; - private readonly byte[] _buffer; private readonly OpusDecoder _decoder; + private readonly byte[] _buffer; - public OpusDecodeStream(AudioStream next, int channels = OpusConverter.MaxChannels, int bufferSize = 5760 * 2 * sizeof(short)) + public OpusDecodeStream(AudioStream next) { _next = next; - _buffer = new byte[bufferSize]; - _decoder = new OpusDecoder(SampleRate, channels); + _buffer = new byte[OpusConverter.FrameBytes]; + _decoder = new OpusDecoder(); } public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) diff --git a/src/Discord.Net.WebSocket/Audio/Streams/OpusEncodeStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/OpusEncodeStream.cs index ac6284c91..2a3c03a47 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/OpusEncodeStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/OpusEncodeStream.cs @@ -12,18 +12,13 @@ namespace Discord.Audio.Streams private readonly AudioStream _next; private readonly OpusEncoder _encoder; private readonly byte[] _buffer; - - private int _frameSize; - private byte[] _partialFrameBuffer; private int _partialFramePos; - public OpusEncodeStream(AudioStream next, int channels, int samplesPerFrame, int bitrate, AudioApplication application, int bufferSize = 4000) + public OpusEncodeStream(AudioStream next, int bitrate, AudioApplication application) { _next = next; - _encoder = new OpusEncoder(SampleRate, channels, bitrate, application); - _frameSize = samplesPerFrame * channels * 2; - _buffer = new byte[bufferSize]; - _partialFrameBuffer = new byte[_frameSize]; + _encoder = new OpusEncoder(bitrate, application); + _buffer = new byte[OpusConverter.FrameBytes]; } public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) @@ -31,20 +26,31 @@ namespace Discord.Audio.Streams //Assume threadsafe while (count > 0) { - if (_partialFramePos + count >= _frameSize) + if (_partialFramePos == 0 && count >= OpusConverter.FrameBytes) + { + //We have enough data and no partial frames. Pass the buffer directly to the encoder + int encFrameSize = _encoder.EncodeFrame(buffer, offset, _buffer, 0); + await _next.WriteAsync(_buffer, 0, encFrameSize, cancellationToken).ConfigureAwait(false); + + offset += OpusConverter.FrameBytes; + count -= OpusConverter.FrameBytes; + } + else if (_partialFramePos + count >= OpusConverter.FrameBytes) { - int partialSize = _frameSize - _partialFramePos; - Buffer.BlockCopy(buffer, offset, _partialFrameBuffer, _partialFramePos, partialSize); + //We have enough data to complete a previous partial frame. + int partialSize = OpusConverter.FrameBytes - _partialFramePos; + Buffer.BlockCopy(buffer, offset, _buffer, _partialFramePos, partialSize); + int encFrameSize = _encoder.EncodeFrame(_buffer, 0, _buffer, 0); + await _next.WriteAsync(_buffer, 0, encFrameSize, cancellationToken).ConfigureAwait(false); + offset += partialSize; count -= partialSize; _partialFramePos = 0; - - int encFrameSize = _encoder.EncodeFrame(_partialFrameBuffer, 0, _frameSize, _buffer, 0); - await _next.WriteAsync(_buffer, 0, encFrameSize, cancellationToken).ConfigureAwait(false); } else { - Buffer.BlockCopy(buffer, offset, _partialFrameBuffer, _partialFramePos, count); + //Not enough data to build a complete frame, store this part for later + Buffer.BlockCopy(buffer, offset, _buffer, _partialFramePos, count); _partialFramePos += count; break; } diff --git a/src/Discord.Net.WebSocket/Audio/Streams/RTPWriteStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/RTPWriteStream.cs index b8d58c997..40d6f21f5 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/RTPWriteStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/RTPWriteStream.cs @@ -9,15 +9,12 @@ namespace Discord.Audio.Streams { private readonly AudioStream _next; private readonly byte[] _header; - private int _samplesPerFrame; - private uint _ssrc, _timestamp = 0; - protected readonly byte[] _buffer; + private uint _ssrc, _timestamp = 0; - public RTPWriteStream(AudioStream next, int samplesPerFrame, uint ssrc, int bufferSize = 4000) + public RTPWriteStream(AudioStream next, uint ssrc, int bufferSize = 4000) { _next = next; - _samplesPerFrame = samplesPerFrame; _ssrc = ssrc; _buffer = new byte[bufferSize]; _header = new byte[24]; @@ -38,7 +35,7 @@ namespace Discord.Audio.Streams if (_header[3]++ == byte.MaxValue) _header[2]++; - _timestamp += (uint)_samplesPerFrame; + _timestamp += (uint)OpusEncoder.FrameSamples; _header[4] = (byte)(_timestamp >> 24); _header[5] = (byte)(_timestamp >> 16); _header[6] = (byte)(_timestamp >> 8); From 0ce313c40861289e7383ac9959443598563618b3 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 8 Apr 2017 17:15:57 -0300 Subject: [PATCH 130/243] Added int constructor to Color --- src/Discord.Net.Core/Entities/Roles/Color.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net.Core/Entities/Roles/Color.cs b/src/Discord.Net.Core/Entities/Roles/Color.cs index ead46fd8a..4c707006c 100644 --- a/src/Discord.Net.Core/Entities/Roles/Color.cs +++ b/src/Discord.Net.Core/Entities/Roles/Color.cs @@ -28,7 +28,14 @@ namespace Discord RawValue = ((uint)r << 16) | ((uint)g << 8) | - b; + (uint)b; + } + public Color(int r, int g, int b) + { + RawValue = + ((uint)r << 16) | + ((uint)g << 8) | + (uint)b; } public Color(float r, float g, float b) { From 6a0c57cfe49fabb901317b45dcd863a439b03885 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 8 Apr 2017 17:24:02 -0300 Subject: [PATCH 131/243] Add range checks to new overload --- src/Discord.Net.Core/Entities/Roles/Color.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Roles/Color.cs b/src/Discord.Net.Core/Entities/Roles/Color.cs index 4c707006c..3250acb2d 100644 --- a/src/Discord.Net.Core/Entities/Roles/Color.cs +++ b/src/Discord.Net.Core/Entities/Roles/Color.cs @@ -32,6 +32,12 @@ namespace Discord } public Color(int r, int g, int b) { + if (r < 0 || r > 255) + throw new ArgumentOutOfRangeException(nameof(r), "Value must be within [0,255]"); + if (g < 0 || g > 255) + throw new ArgumentOutOfRangeException(nameof(g), "Value must be within [0,255]"); + if (b < 0 || b > 255) + throw new ArgumentOutOfRangeException(nameof(b), "Value must be within [0,255]"); RawValue = ((uint)r << 16) | ((uint)g << 8) | @@ -40,11 +46,11 @@ namespace Discord public Color(float r, float g, float b) { if (r < 0.0f || r > 1.0f) - throw new ArgumentOutOfRangeException(nameof(r), "A float value must be within [0,1]"); + throw new ArgumentOutOfRangeException(nameof(r), "Value must be within [0,1]"); if (g < 0.0f || g > 1.0f) - throw new ArgumentOutOfRangeException(nameof(g), "A float value must be within [0,1]"); + throw new ArgumentOutOfRangeException(nameof(g), "Value must be within [0,1]"); if (b < 0.0f || b > 1.0f) - throw new ArgumentOutOfRangeException(nameof(b), "A float value must be within [0,1]"); + throw new ArgumentOutOfRangeException(nameof(b), "Value must be within [0,1]"); RawValue = ((uint)(r * 255.0f) << 16) | ((uint)(g * 255.0f) << 8) | From 39b0a998c8b0e0c9fd04d7e6699604ed333c6140 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 8 Apr 2017 18:35:57 -0300 Subject: [PATCH 132/243] Fixed a few audio constants --- src/Discord.Net.WebSocket/Audio/Opus/OpusConverter.cs | 2 +- src/Discord.Net.WebSocket/Audio/Opus/OpusDecoder.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Discord.Net.WebSocket/Audio/Opus/OpusConverter.cs b/src/Discord.Net.WebSocket/Audio/Opus/OpusConverter.cs index f802d65ad..28581ea4e 100644 --- a/src/Discord.Net.WebSocket/Audio/Opus/OpusConverter.cs +++ b/src/Discord.Net.WebSocket/Audio/Opus/OpusConverter.cs @@ -12,8 +12,8 @@ namespace Discord.Audio public const int SampleBytes = sizeof(short) * Channels; - public const int FrameSamples = SamplingRate / 1000 * FrameMillis; public const int FrameSamplesPerChannel = SamplingRate / 1000 * FrameMillis; + public const int FrameSamples = FrameSamplesPerChannel * Channels; public const int FrameBytes = FrameSamples * SampleBytes; protected bool _isDisposed = false; diff --git a/src/Discord.Net.WebSocket/Audio/Opus/OpusDecoder.cs b/src/Discord.Net.WebSocket/Audio/Opus/OpusDecoder.cs index c5c16dff6..2c8d8036d 100644 --- a/src/Discord.Net.WebSocket/Audio/Opus/OpusDecoder.cs +++ b/src/Discord.Net.WebSocket/Audio/Opus/OpusDecoder.cs @@ -25,9 +25,9 @@ namespace Discord.Audio int result = 0; fixed (byte* inPtr = input) fixed (byte* outPtr = output) - result = Decode(_ptr, inPtr + inputOffset, inputCount, outPtr + outputOffset, FrameBytes / SampleBytes, 1); + result = Decode(_ptr, inPtr + inputOffset, inputCount, outPtr + outputOffset, FrameSamplesPerChannel, 1); CheckError(result); - return FrameBytes; + return result * SampleBytes; } protected override void Dispose(bool disposing) From aca8def0cb4028fbf1ab911bf1ca98f96d1da8ec Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 8 Apr 2017 18:38:58 -0300 Subject: [PATCH 133/243] ModuleBase should map to ICommandContext --- src/Discord.Net.Commands/ModuleBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.Commands/ModuleBase.cs b/src/Discord.Net.Commands/ModuleBase.cs index a38ffce06..ed0b49006 100644 --- a/src/Discord.Net.Commands/ModuleBase.cs +++ b/src/Discord.Net.Commands/ModuleBase.cs @@ -3,7 +3,7 @@ using System.Threading.Tasks; namespace Discord.Commands { - public abstract class ModuleBase : ModuleBase { } + public abstract class ModuleBase : ModuleBase { } public abstract class ModuleBase : IModuleBase where T : class, ICommandContext From 79fd14a95f7961ea90484846c68da7247fcd7bc0 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sun, 9 Apr 2017 01:05:52 -0300 Subject: [PATCH 134/243] Various audio fixes --- src/Discord.Net.Core/Audio/AudioStream.cs | 1 + src/Discord.Net.Core/Audio/RTPFrame.cs | 4 +- src/Discord.Net.Core/Net/Udp/IUdpSocket.cs | 2 + .../UDPClient.cs | 2 + .../Audio/AudioClient.cs | 4 +- .../Audio/Opus/OpusConverter.cs | 2 +- .../Audio/Opus/OpusDecoder.cs | 4 +- .../Audio/Streams/BufferedWriteStream.cs | 45 ++++++++++++------- .../Audio/Streams/InputStream.cs | 7 ++- .../Audio/Streams/OpusDecodeStream.cs | 34 ++++++++++++-- .../Audio/Streams/RTPReadStream.cs | 17 +++---- .../Audio/Streams/RTPWriteStream.cs | 31 +++++++++---- .../DiscordVoiceApiClient.cs | 2 + .../Net/DefaultUdpSocket.cs | 2 + 14 files changed, 111 insertions(+), 46 deletions(-) diff --git a/src/Discord.Net.Core/Audio/AudioStream.cs b/src/Discord.Net.Core/Audio/AudioStream.cs index 224409f8a..d39bcc48a 100644 --- a/src/Discord.Net.Core/Audio/AudioStream.cs +++ b/src/Discord.Net.Core/Audio/AudioStream.cs @@ -11,6 +11,7 @@ namespace Discord.Audio public override bool CanSeek => false; public override bool CanWrite => false; + public virtual void WriteHeader(ushort seq, uint timestamp, bool missed) { } public override void Write(byte[] buffer, int offset, int count) { WriteAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult(); diff --git a/src/Discord.Net.Core/Audio/RTPFrame.cs b/src/Discord.Net.Core/Audio/RTPFrame.cs index 5005870f4..6254b7173 100644 --- a/src/Discord.Net.Core/Audio/RTPFrame.cs +++ b/src/Discord.Net.Core/Audio/RTPFrame.cs @@ -5,12 +5,14 @@ namespace Discord.Audio public readonly ushort Sequence; public readonly uint Timestamp; public readonly byte[] Payload; + public readonly bool Missed; - public RTPFrame(ushort sequence, uint timestamp, byte[] payload) + public RTPFrame(ushort sequence, uint timestamp, byte[] payload, bool missed) { Sequence = sequence; Timestamp = timestamp; Payload = payload; + Missed = missed; } } } \ No newline at end of file diff --git a/src/Discord.Net.Core/Net/Udp/IUdpSocket.cs b/src/Discord.Net.Core/Net/Udp/IUdpSocket.cs index feb94b683..10ac652b3 100644 --- a/src/Discord.Net.Core/Net/Udp/IUdpSocket.cs +++ b/src/Discord.Net.Core/Net/Udp/IUdpSocket.cs @@ -8,6 +8,8 @@ namespace Discord.Net.Udp { event Func ReceivedDatagram; + ushort Port { get; } + void SetCancelToken(CancellationToken cancelToken); void SetDestination(string ip, int port); diff --git a/src/Discord.Net.Providers.UdpClient/UDPClient.cs b/src/Discord.Net.Providers.UdpClient/UDPClient.cs index 459feb335..dfd05cf38 100644 --- a/src/Discord.Net.Providers.UdpClient/UDPClient.cs +++ b/src/Discord.Net.Providers.UdpClient/UDPClient.cs @@ -18,6 +18,8 @@ namespace Discord.Net.Providers.UDPClient private CancellationToken _cancelToken, _parentToken; private Task _task; private bool _isDisposed; + + public ushort Port => (ushort)((_udp?.Client.LocalEndPoint as IPEndPoint)?.Port ?? 0); public UDPClient() { diff --git a/src/Discord.Net.WebSocket/Audio/AudioClient.cs b/src/Discord.Net.WebSocket/Audio/AudioClient.cs index c497b2632..ceaea01cc 100644 --- a/src/Discord.Net.WebSocket/Audio/AudioClient.cs +++ b/src/Discord.Net.WebSocket/Audio/AudioClient.cs @@ -107,6 +107,7 @@ namespace Discord.Audio { await _audioLogger.DebugAsync("Connecting ApiClient").ConfigureAwait(false); await ApiClient.ConnectAsync("wss://" + _url).ConfigureAwait(false); + await _audioLogger.DebugAsync("Listening on port " + ApiClient.UdpPort).ConfigureAwait(false); await _audioLogger.DebugAsync("Sending Identity").ConfigureAwait(false); await ApiClient.SendIdentityAsync(_userId, _sessionId, _token).ConfigureAwait(false); @@ -175,7 +176,8 @@ namespace Discord.Audio { var readerStream = new InputStream(); var opusDecoder = new OpusDecodeStream(readerStream); - var rtpReader = new RTPReadStream(readerStream, opusDecoder); + //var jitterBuffer = new JitterBuffer(opusDecoder, _audioLogger); + var rtpReader = new RTPReadStream(opusDecoder); var decryptStream = new SodiumDecryptStream(rtpReader, this); _streams.TryAdd(userId, new StreamPair(readerStream, decryptStream)); await _streamCreatedEvent.InvokeAsync(userId, readerStream); diff --git a/src/Discord.Net.WebSocket/Audio/Opus/OpusConverter.cs b/src/Discord.Net.WebSocket/Audio/Opus/OpusConverter.cs index 28581ea4e..4179ce9c9 100644 --- a/src/Discord.Net.WebSocket/Audio/Opus/OpusConverter.cs +++ b/src/Discord.Net.WebSocket/Audio/Opus/OpusConverter.cs @@ -14,7 +14,7 @@ namespace Discord.Audio public const int FrameSamplesPerChannel = SamplingRate / 1000 * FrameMillis; public const int FrameSamples = FrameSamplesPerChannel * Channels; - public const int FrameBytes = FrameSamples * SampleBytes; + public const int FrameBytes = FrameSamplesPerChannel * SampleBytes; protected bool _isDisposed = false; diff --git a/src/Discord.Net.WebSocket/Audio/Opus/OpusDecoder.cs b/src/Discord.Net.WebSocket/Audio/Opus/OpusDecoder.cs index 2c8d8036d..41c48e1ac 100644 --- a/src/Discord.Net.WebSocket/Audio/Opus/OpusDecoder.cs +++ b/src/Discord.Net.WebSocket/Audio/Opus/OpusDecoder.cs @@ -20,12 +20,12 @@ namespace Discord.Audio CheckError(error); } - public unsafe int DecodeFrame(byte[] input, int inputOffset, int inputCount, byte[] output, int outputOffset) + public unsafe int DecodeFrame(byte[] input, int inputOffset, int inputCount, byte[] output, int outputOffset, bool decodeFEC) { int result = 0; fixed (byte* inPtr = input) fixed (byte* outPtr = output) - result = Decode(_ptr, inPtr + inputOffset, inputCount, outPtr + outputOffset, FrameSamplesPerChannel, 1); + result = Decode(_ptr, inPtr + inputOffset, inputCount, outPtr + outputOffset, FrameSamplesPerChannel, decodeFEC ? 1 : 0); CheckError(result); return result * SampleBytes; } diff --git a/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs index 1764fa66a..e5065345f 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs @@ -35,7 +35,7 @@ namespace Discord.Audio.Streams private readonly SemaphoreSlim _queueLock; private readonly Logger _logger; private readonly int _ticksPerFrame, _queueLength; - private bool _isPreloaded; + private bool _isPreloaded, _isSpeaking; private int _silenceFrames; public BufferedWriteStream(AudioStream next, IAudioClient client, int bufferMillis, CancellationToken cancelToken, int maxFrameSize = 1500) @@ -45,7 +45,7 @@ namespace Discord.Audio.Streams //maxFrameSize = 1275 was too limiting at 128kbps,2ch,60ms _next = next; _client = client; - _ticksPerFrame = OpusEncoder.FrameSamples / 48; + _ticksPerFrame = OpusEncoder.FrameMillis; _logger = logger; _queueLength = (bufferMillis + (_ticksPerFrame - 1)) / _ticksPerFrame; //Round up @@ -60,6 +60,12 @@ namespace Discord.Audio.Streams _task = Run(); } + protected override void Dispose(bool disposing) + { + if (disposing) + _cancelTokenSource.Cancel(); + base.Dispose(disposing); + } private Task Run() { @@ -71,6 +77,8 @@ namespace Discord.Audio.Streams await Task.Delay(1).ConfigureAwait(false); long nextTick = Environment.TickCount; + ushort seq = 0; + uint timestamp = 0; while (!_cancelToken.IsCancellationRequested) { long tick = Environment.TickCount; @@ -80,14 +88,20 @@ namespace Discord.Audio.Streams Frame frame; if (_queuedFrames.TryDequeue(out frame)) { - await _client.ApiClient.SendSetSpeaking(true).ConfigureAwait(false); + if (!_isSpeaking) + { + await _client.ApiClient.SendSetSpeaking(true).ConfigureAwait(false); + _isSpeaking = true; + } + _next.WriteHeader(seq++, timestamp, false); await _next.WriteAsync(frame.Buffer, 0, frame.Bytes).ConfigureAwait(false); _bufferPool.Enqueue(frame.Buffer); _queueLock.Release(); nextTick += _ticksPerFrame; + timestamp += OpusEncoder.FrameSamplesPerChannel; _silenceFrames = 0; #if DEBUG - var _ = _logger.DebugAsync($"Sent {frame.Bytes} bytes ({_queuedFrames.Count} frames buffered)"); + var _ = _logger?.DebugAsync($"Sent {frame.Bytes} bytes ({_queuedFrames.Count} frames buffered)"); #endif } else @@ -95,13 +109,20 @@ namespace Discord.Audio.Streams while ((nextTick - tick) <= 0) { if (_silenceFrames++ < MaxSilenceFrames) + { + _next.WriteHeader(seq++, timestamp, false); await _next.WriteAsync(_silenceFrame, 0, _silenceFrame.Length).ConfigureAwait(false); - else + } + else if (_isSpeaking) + { await _client.ApiClient.SendSetSpeaking(false).ConfigureAwait(false); + _isSpeaking = false; + } nextTick += _ticksPerFrame; + timestamp += OpusEncoder.FrameSamplesPerChannel; } #if DEBUG - var _ = _logger.DebugAsync($"Buffer underrun"); + var _ = _logger?.DebugAsync($"Buffer underrun"); #endif } } @@ -125,19 +146,16 @@ namespace Discord.Audio.Streams if (!_bufferPool.TryDequeue(out buffer)) { #if DEBUG - var _ = _logger.DebugAsync($"Buffer overflow"); //Should never happen because of the queueLock + var _ = _logger?.DebugAsync($"Buffer overflow"); //Should never happen because of the queueLock #endif return; } Buffer.BlockCopy(data, offset, buffer, 0, count); _queuedFrames.Enqueue(new Frame(buffer, count)); -#if DEBUG - //var _ await _logger.DebugAsync($"Queued {count} bytes ({_queuedFrames.Count} frames buffered)"); -#endif if (!_isPreloaded && _queuedFrames.Count == _queueLength) { #if DEBUG - var _ = _logger.DebugAsync($"Preloaded"); + var _ = _logger?.DebugAsync($"Preloaded"); #endif _isPreloaded = true; } @@ -161,10 +179,5 @@ namespace Discord.Audio.Streams while (_queuedFrames.TryDequeue(out ignored)); return Task.Delay(0); } - protected override void Dispose(bool disposing) - { - if (disposing) - _cancelTokenSource.Cancel(); - } } } \ No newline at end of file diff --git a/src/Discord.Net.WebSocket/Audio/Streams/InputStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/InputStream.cs index e29302fa0..a46b6d3d2 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/InputStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/InputStream.cs @@ -14,6 +14,7 @@ namespace Discord.Audio.Streams private SemaphoreSlim _signal; private ushort _nextSeq; private uint _nextTimestamp; + private bool _nextMissed; private bool _hasHeader; private bool _isDisposed; @@ -60,13 +61,14 @@ namespace Discord.Audio.Streams return frame; } - public void WriteHeader(ushort seq, uint timestamp) + public override void WriteHeader(ushort seq, uint timestamp, bool missed) { if (_hasHeader) throw new InvalidOperationException("Header received with no payload"); _hasHeader = true; _nextSeq = seq; _nextTimestamp = timestamp; + _nextMissed = missed; } public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancelToken) { @@ -79,16 +81,17 @@ namespace Discord.Audio.Streams } if (!_hasHeader) throw new InvalidOperationException("Received payload without an RTP header"); + _hasHeader = false; byte[] payload = new byte[count]; Buffer.BlockCopy(buffer, offset, payload, 0, count); _frames.Enqueue(new RTPFrame( sequence: _nextSeq, timestamp: _nextTimestamp, + missed: _nextMissed, payload: payload )); _signal.Release(); - _hasHeader = false; return Task.Delay(0); } diff --git a/src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs index 96c809cca..43289c60e 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs @@ -1,4 +1,5 @@ -using System.Threading; +using System; +using System.Threading; using System.Threading.Tasks; namespace Discord.Audio.Streams @@ -11,6 +12,8 @@ namespace Discord.Audio.Streams private readonly AudioStream _next; private readonly OpusDecoder _decoder; private readonly byte[] _buffer; + private bool _nextMissed; + private bool _hasHeader; public OpusDecodeStream(AudioStream next) { @@ -19,10 +22,35 @@ namespace Discord.Audio.Streams _decoder = new OpusDecoder(); } + public override void WriteHeader(ushort seq, uint timestamp, bool missed) + { + if (_hasHeader) + throw new InvalidOperationException("Header received with no payload"); + _nextMissed = missed; + _hasHeader = true; + _next.WriteHeader(seq, timestamp, missed); + } public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { - count = _decoder.DecodeFrame(buffer, offset, count, _buffer, 0); - await _next.WriteAsync(_buffer, 0, count, cancellationToken).ConfigureAwait(false); + if (!_hasHeader) + throw new InvalidOperationException("Received payload without an RTP header"); + _hasHeader = false; + + if (!_nextMissed) + { + count = _decoder.DecodeFrame(buffer, offset, count, _buffer, 0, false); + await _next.WriteAsync(_buffer, 0, count, cancellationToken).ConfigureAwait(false); + } + else if (count > 0) + { + count = _decoder.DecodeFrame(buffer, offset, count, _buffer, 0, true); + await _next.WriteAsync(_buffer, 0, count, cancellationToken).ConfigureAwait(false); + } + else + { + count = _decoder.DecodeFrame(null, 0, 0, _buffer, 0, true); + await _next.WriteAsync(_buffer, 0, count, cancellationToken).ConfigureAwait(false); + } } public override async Task FlushAsync(CancellationToken cancelToken) diff --git a/src/Discord.Net.WebSocket/Audio/Streams/RTPReadStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/RTPReadStream.cs index 292a9303a..2cedea114 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/RTPReadStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/RTPReadStream.cs @@ -1,5 +1,4 @@ -using System.IO; -using System.Threading; +using System.Threading; using System.Threading.Tasks; namespace Discord.Audio.Streams @@ -7,7 +6,6 @@ namespace Discord.Audio.Streams /// Reads the payload from an RTP frame public class RTPReadStream : AudioOutStream { - private readonly InputStream _queue; private readonly AudioStream _next; private readonly byte[] _buffer, _nonce; @@ -15,11 +13,8 @@ namespace Discord.Audio.Streams public override bool CanSeek => false; public override bool CanWrite => true; - public RTPReadStream(InputStream queue, int bufferSize = 4000) - : this(queue, null, bufferSize) { } - public RTPReadStream(InputStream queue, AudioStream next, int bufferSize = 4000) + public RTPReadStream(AudioStream next, int bufferSize = 4000) { - _queue = queue; _next = next; _buffer = new byte[bufferSize]; _nonce = new byte[24]; @@ -36,11 +31,11 @@ namespace Discord.Audio.Streams uint timestamp = (uint)((buffer[offset + 4] << 24) | (buffer[offset + 5] << 16) | - (buffer[offset + 6] << 16) | + (buffer[offset + 6] << 8) | (buffer[offset + 7] << 0)); - _queue.WriteHeader(seq, timestamp); - await (_next ?? _queue as Stream).WriteAsync(buffer, offset + headerSize, count - headerSize, cancelToken).ConfigureAwait(false); + _next.WriteHeader(seq, timestamp, false); + await _next.WriteAsync(buffer, offset + headerSize, count - headerSize, cancelToken).ConfigureAwait(false); } public static bool TryReadSsrc(byte[] buffer, int offset, out uint ssrc) @@ -58,7 +53,7 @@ namespace Discord.Audio.Streams ssrc = (uint)((buffer[offset + 8] << 24) | (buffer[offset + 9] << 16) | - (buffer[offset + 10] << 16) | + (buffer[offset + 10] << 8) | (buffer[offset + 11] << 0)); return true; } diff --git a/src/Discord.Net.WebSocket/Audio/Streams/RTPWriteStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/RTPWriteStream.cs index 40d6f21f5..78f895381 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/RTPWriteStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/RTPWriteStream.cs @@ -10,7 +10,10 @@ namespace Discord.Audio.Streams private readonly AudioStream _next; private readonly byte[] _header; protected readonly byte[] _buffer; - private uint _ssrc, _timestamp = 0; + private uint _ssrc; + private ushort _nextSeq; + private uint _nextTimestamp; + private bool _hasHeader; public RTPWriteStream(AudioStream next, uint ssrc, int bufferSize = 4000) { @@ -26,20 +29,30 @@ namespace Discord.Audio.Streams _header[11] = (byte)(_ssrc >> 0); } + public override void WriteHeader(ushort seq, uint timestamp, bool missed) + { + if (_hasHeader) + throw new InvalidOperationException("Header received with no payload"); + _hasHeader = true; + _nextSeq = seq; + _nextTimestamp = timestamp; + } public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); + if (!_hasHeader) + throw new InvalidOperationException("Received payload without an RTP header"); + _hasHeader = false; + unchecked { - if (_header[3]++ == byte.MaxValue) - _header[2]++; - - _timestamp += (uint)OpusEncoder.FrameSamples; - _header[4] = (byte)(_timestamp >> 24); - _header[5] = (byte)(_timestamp >> 16); - _header[6] = (byte)(_timestamp >> 8); - _header[7] = (byte)(_timestamp >> 0); + _header[2] = (byte)(_nextSeq >> 8); + _header[3] = (byte)(_nextSeq >> 0); + _header[4] = (byte)(_nextTimestamp >> 24); + _header[5] = (byte)(_nextTimestamp >> 16); + _header[6] = (byte)(_nextTimestamp >> 8); + _header[7] = (byte)(_nextTimestamp >> 0); } Buffer.BlockCopy(_header, 0, _buffer, 0, 12); //Copy RTP header from to the buffer Buffer.BlockCopy(buffer, offset, _buffer, 12, count); diff --git a/src/Discord.Net.WebSocket/DiscordVoiceApiClient.cs b/src/Discord.Net.WebSocket/DiscordVoiceApiClient.cs index 05671d71d..25dc2cf7b 100644 --- a/src/Discord.Net.WebSocket/DiscordVoiceApiClient.cs +++ b/src/Discord.Net.WebSocket/DiscordVoiceApiClient.cs @@ -48,6 +48,8 @@ namespace Discord.Audio internal IWebSocketClient WebSocketClient { get; } public ConnectionState ConnectionState { get; private set; } + public ushort UdpPort => _udp.Port; + internal DiscordVoiceAPIClient(ulong guildId, WebSocketProvider webSocketProvider, UdpSocketProvider udpSocketProvider, JsonSerializer serializer = null) { GuildId = guildId; diff --git a/src/Discord.Net.WebSocket/Net/DefaultUdpSocket.cs b/src/Discord.Net.WebSocket/Net/DefaultUdpSocket.cs index 3366250cc..013ba62fc 100644 --- a/src/Discord.Net.WebSocket/Net/DefaultUdpSocket.cs +++ b/src/Discord.Net.WebSocket/Net/DefaultUdpSocket.cs @@ -18,6 +18,8 @@ namespace Discord.Net.Udp private CancellationToken _cancelToken, _parentToken; private Task _task; private bool _isDisposed; + + public ushort Port => (ushort)((_udp?.Client.LocalEndPoint as IPEndPoint)?.Port ?? 0); public DefaultUdpSocket() { From d2a7be91e57c227598b271ecedab9f31f4c22a89 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sun, 9 Apr 2017 01:06:42 -0300 Subject: [PATCH 135/243] Added experimental jitter buffer --- .../Audio/Streams/JitterBuffer.cs | 249 ++++++++++++++++++ 1 file changed, 249 insertions(+) create mode 100644 src/Discord.Net.WebSocket/Audio/Streams/JitterBuffer.cs diff --git a/src/Discord.Net.WebSocket/Audio/Streams/JitterBuffer.cs b/src/Discord.Net.WebSocket/Audio/Streams/JitterBuffer.cs new file mode 100644 index 000000000..2038e605a --- /dev/null +++ b/src/Discord.Net.WebSocket/Audio/Streams/JitterBuffer.cs @@ -0,0 +1,249 @@ +using Discord.Logging; +using System; +using System.Collections.Concurrent; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Audio.Streams +{ + /// Wraps another stream with a timed buffer and packet loss detection. + public class JitterBuffer : AudioOutStream + { + private struct Frame + { + public Frame(byte[] buffer, int bytes, ushort sequence, uint timestamp) + { + Buffer = buffer; + Bytes = bytes; + Sequence = sequence; + Timestamp = timestamp; + } + + public readonly byte[] Buffer; + public readonly int Bytes; + public readonly ushort Sequence; + public readonly uint Timestamp; + } + + private static readonly byte[] _silenceFrame = new byte[0]; + + private readonly AudioStream _next; + private readonly CancellationTokenSource _cancelTokenSource; + private readonly CancellationToken _cancelToken; + private readonly Task _task; + private readonly ConcurrentQueue _queuedFrames; + private readonly ConcurrentQueue _bufferPool; + private readonly SemaphoreSlim _queueLock; + private readonly Logger _logger; + private readonly int _ticksPerFrame, _queueLength; + private bool _isPreloaded, _hasHeader; + + private ushort _seq, _nextSeq; + private uint _timestamp, _nextTimestamp; + private bool _isFirst; + + public JitterBuffer(AudioStream next, int bufferMillis = 60, int maxFrameSize = 1500) + : this(next, null, bufferMillis, maxFrameSize) { } + internal JitterBuffer(AudioStream next, Logger logger, int bufferMillis = 60, int maxFrameSize = 1500) + { + //maxFrameSize = 1275 was too limiting at 128kbps,2ch,60ms + _next = next; + _ticksPerFrame = OpusEncoder.FrameMillis; + _logger = logger; + _queueLength = (bufferMillis + (_ticksPerFrame - 1)) / _ticksPerFrame; //Round up + + _cancelTokenSource = new CancellationTokenSource(); + _cancelToken = _cancelTokenSource.Token; + _queuedFrames = new ConcurrentQueue(); + _bufferPool = new ConcurrentQueue(); + for (int i = 0; i < _queueLength; i++) + _bufferPool.Enqueue(new byte[maxFrameSize]); + _queueLock = new SemaphoreSlim(_queueLength, _queueLength); + + _isFirst = true; + _task = Run(); + } + protected override void Dispose(bool disposing) + { + if (disposing) + _cancelTokenSource.Cancel(); + base.Dispose(disposing); + } + + private Task Run() + { + return Task.Run(async () => + { + try + { + long nextTick = Environment.TickCount; + int silenceFrames = 0; + while (!_cancelToken.IsCancellationRequested) + { + long tick = Environment.TickCount; + long dist = nextTick - tick; + if (dist > 0) + { + await Task.Delay((int)dist).ConfigureAwait(false); + continue; + } + nextTick += _ticksPerFrame; + if (!_isPreloaded) + { + await Task.Delay(_ticksPerFrame).ConfigureAwait(false); + continue; + } + + Frame frame; + if (_queuedFrames.TryPeek(out frame)) + { + silenceFrames = 0; + uint distance = (uint)(frame.Timestamp - _timestamp); + bool restartSeq = _isFirst; + if (!_isFirst) + { + if (distance > uint.MaxValue - (OpusEncoder.FrameSamplesPerChannel * 50 * 5)) //Negative distances wraps + { + _queuedFrames.TryDequeue(out frame); + _bufferPool.Enqueue(frame.Buffer); + _queueLock.Release(); +#if DEBUG + var _ = _logger?.DebugAsync($"Dropped frame {_timestamp} ({_queuedFrames.Count} frames buffered)"); +#endif + continue; //This is a missed packet less than five seconds old, ignore it + } + } + + if (distance == 0 || restartSeq) + { + //This is the frame we expected + _seq = frame.Sequence; + _timestamp = frame.Timestamp; + _isFirst = false; + silenceFrames = 0; + + _next.WriteHeader(_seq++, _timestamp, false); + await _next.WriteAsync(frame.Buffer, 0, frame.Bytes).ConfigureAwait(false); + _queuedFrames.TryDequeue(out frame); + _bufferPool.Enqueue(frame.Buffer); + _queueLock.Release(); +#if DEBUG + var _ = _logger?.DebugAsync($"Read frame {_timestamp} ({_queuedFrames.Count} frames buffered)"); +#endif + } + else if (distance == OpusEncoder.FrameSamplesPerChannel) + { + //Missed this frame, but the next queued one might have FEC info + _next.WriteHeader(_seq++, _timestamp, true); + await _next.WriteAsync(frame.Buffer, 0, frame.Bytes).ConfigureAwait(false); +#if DEBUG + var _ = _logger?.DebugAsync($"Recreated Frame {_timestamp} (Next is {frame.Timestamp}) ({_queuedFrames.Count} frames buffered)"); +#endif + } + else + { + //Missed this frame and we have no FEC data to work with + _next.WriteHeader(_seq++, _timestamp, true); + await _next.WriteAsync(null, 0, 0).ConfigureAwait(false); +#if DEBUG + var _ = _logger?.DebugAsync($"Missed Frame {_timestamp} (Next is {frame.Timestamp}) ({_queuedFrames.Count} frames buffered)"); +#endif + } + } + else if (!_isFirst) + { + //Missed this frame and we have no FEC data to work with + _next.WriteHeader(_seq++, _timestamp, true); + await _next.WriteAsync(null, 0, 0).ConfigureAwait(false); + if (silenceFrames < 5) + silenceFrames++; + else + { + _isFirst = true; + _isPreloaded = false; + } +#if DEBUG + var _ = _logger?.DebugAsync($"Missed Frame {_timestamp} ({_queuedFrames.Count} frames buffered)"); +#endif + } + _timestamp += OpusEncoder.FrameSamplesPerChannel; + } + } + catch (OperationCanceledException) { } + }); + } + + public override void WriteHeader(ushort seq, uint timestamp, bool missed) + { + if (_hasHeader) + throw new InvalidOperationException("Header received with no payload"); + _nextSeq = seq; + _nextTimestamp = timestamp; + _hasHeader = true; + } + public override async Task WriteAsync(byte[] data, int offset, int count, CancellationToken cancelToken) + { + if (cancelToken.CanBeCanceled) + cancelToken = CancellationTokenSource.CreateLinkedTokenSource(cancelToken, _cancelToken).Token; + else + cancelToken = _cancelToken; + + if (!_hasHeader) + throw new InvalidOperationException("Received payload without an RTP header"); + _hasHeader = false; + + uint distance = (uint)(_nextTimestamp - _timestamp); + if (!_isFirst && (distance == 0 || distance > OpusEncoder.FrameSamplesPerChannel * 50 * 5)) //Negative distances wraps + { +#if DEBUG + var _ = _logger?.DebugAsync($"Frame {_nextTimestamp} was {distance} samples off. Ignoring."); +#endif + return; //This is an old frame, ignore + } + + byte[] buffer; + if (!await _queueLock.WaitAsync(0).ConfigureAwait(false)) + { +#if DEBUG + var _ = _logger?.DebugAsync($"Buffer overflow"); +#endif + return; + } + _bufferPool.TryDequeue(out buffer); + + Buffer.BlockCopy(data, offset, buffer, 0, count); +#if DEBUG + { + var _ = _logger?.DebugAsync($"Queued Frame {_nextTimestamp}."); + } +#endif + _queuedFrames.Enqueue(new Frame(buffer, count, _nextSeq, _nextTimestamp)); + if (!_isPreloaded && _queuedFrames.Count >= _queueLength) + { +#if DEBUG + var _ = _logger?.DebugAsync($"Preloaded"); +#endif + _isPreloaded = true; + } + } + + public override async Task FlushAsync(CancellationToken cancelToken) + { + while (true) + { + cancelToken.ThrowIfCancellationRequested(); + if (_queuedFrames.Count == 0) + return; + await Task.Delay(250, cancelToken).ConfigureAwait(false); + } + } + public override Task ClearAsync(CancellationToken cancelToken) + { + Frame ignored; + do + cancelToken.ThrowIfCancellationRequested(); + while (_queuedFrames.TryDequeue(out ignored)); + return Task.Delay(0); + } + } +} \ No newline at end of file From 8d9e11c08afa794a9ee7ff0d0b870e76ef1cf56a Mon Sep 17 00:00:00 2001 From: RogueException Date: Mon, 10 Apr 2017 18:00:42 -0300 Subject: [PATCH 136/243] Exposed IAudioClient.SetSpeakingAsync --- src/Discord.Net.Core/Audio/IAudioClient.cs | 1 + src/Discord.Net.WebSocket/Audio/AudioClient.cs | 11 +++++++++++ .../Audio/Streams/BufferedWriteStream.cs | 15 ++++----------- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/Discord.Net.Core/Audio/IAudioClient.cs b/src/Discord.Net.Core/Audio/IAudioClient.cs index c1c31af73..7373a8e4d 100644 --- a/src/Discord.Net.Core/Audio/IAudioClient.cs +++ b/src/Discord.Net.Core/Audio/IAudioClient.cs @@ -21,6 +21,7 @@ namespace Discord.Audio int UdpLatency { get; } Task StopAsync(); + Task SetSpeakingAsync(bool value); /// Creates a new outgoing stream accepting Opus-encoded data. AudioOutStream CreateOpusStream(int bufferMillis = 1000); diff --git a/src/Discord.Net.WebSocket/Audio/AudioClient.cs b/src/Discord.Net.WebSocket/Audio/AudioClient.cs index ceaea01cc..405ff394e 100644 --- a/src/Discord.Net.WebSocket/Audio/AudioClient.cs +++ b/src/Discord.Net.WebSocket/Audio/AudioClient.cs @@ -44,6 +44,7 @@ namespace Discord.Audio private string _url, _sessionId, _token; private ulong _userId; private uint _ssrc; + private bool _isSpeaking; public SocketGuild Guild { get; } public DiscordVoiceAPIClient ApiClient { get; private set; } @@ -242,6 +243,7 @@ namespace Discord.Audio throw new InvalidOperationException($"Discord selected an unexpected mode: {data.Mode}"); SecretKey = data.SecretKey; + _isSpeaking = false; await ApiClient.SendSetSpeaking(false).ConfigureAwait(false); _keepaliveTask = RunKeepaliveAsync(5000, _connection.CancelToken); @@ -453,6 +455,15 @@ namespace Discord.Audio } } + public async Task SetSpeakingAsync(bool value) + { + if (_isSpeaking != value) + { + _isSpeaking = value; + await ApiClient.SendSetSpeaking(value).ConfigureAwait(false); + } + } + internal void Dispose(bool disposing) { if (disposing) diff --git a/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs index e5065345f..e73eb2cc2 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs @@ -35,7 +35,7 @@ namespace Discord.Audio.Streams private readonly SemaphoreSlim _queueLock; private readonly Logger _logger; private readonly int _ticksPerFrame, _queueLength; - private bool _isPreloaded, _isSpeaking; + private bool _isPreloaded; private int _silenceFrames; public BufferedWriteStream(AudioStream next, IAudioClient client, int bufferMillis, CancellationToken cancelToken, int maxFrameSize = 1500) @@ -88,11 +88,7 @@ namespace Discord.Audio.Streams Frame frame; if (_queuedFrames.TryDequeue(out frame)) { - if (!_isSpeaking) - { - await _client.ApiClient.SendSetSpeaking(true).ConfigureAwait(false); - _isSpeaking = true; - } + await _client.SetSpeakingAsync(true).ConfigureAwait(false); _next.WriteHeader(seq++, timestamp, false); await _next.WriteAsync(frame.Buffer, 0, frame.Bytes).ConfigureAwait(false); _bufferPool.Enqueue(frame.Buffer); @@ -113,11 +109,8 @@ namespace Discord.Audio.Streams _next.WriteHeader(seq++, timestamp, false); await _next.WriteAsync(_silenceFrame, 0, _silenceFrame.Length).ConfigureAwait(false); } - else if (_isSpeaking) - { - await _client.ApiClient.SendSetSpeaking(false).ConfigureAwait(false); - _isSpeaking = false; - } + else + await _client.SetSpeakingAsync(false).ConfigureAwait(false); nextTick += _ticksPerFrame; timestamp += OpusEncoder.FrameSamplesPerChannel; } From 660d4b0bf62152a8b0b1ee2e2bac74bfed9ef6b5 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Sat, 15 Apr 2017 18:03:19 -0400 Subject: [PATCH 137/243] Add an upper limit to prune length when banning a user (#611) Messages may only be pruned between 0 and 7 days, otherwise a 400 will be thrown. --- src/Discord.Net.Core/Entities/Guilds/IGuild.cs | 2 ++ src/Discord.Net.Rest/DiscordRestApiClient.cs | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs index 6c7b73370..8da731855 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs @@ -65,8 +65,10 @@ namespace Discord /// Gets a collection of all users banned on this guild. Task> GetBansAsync(RequestOptions options = null); /// Bans the provided user from this guild and optionally prunes their recent messages. + /// The number of days to remove messages from this user for - must be between [0, 7] Task AddBanAsync(IUser user, int pruneDays = 0, RequestOptions options = null); /// Bans the provided user id from this guild and optionally prunes their recent messages. + /// The number of days to remove messages from this user for - must be between [0, 7] Task AddBanAsync(ulong userId, int pruneDays = 0, RequestOptions options = null); /// Unbans the provided user if it is currently banned. Task RemoveBanAsync(IUser user, RequestOptions options = null); diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index c57d15645..a632e5d42 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -798,7 +798,8 @@ namespace Discord.API Preconditions.NotEqual(guildId, 0, nameof(guildId)); Preconditions.NotEqual(userId, 0, nameof(userId)); Preconditions.NotNull(args, nameof(args)); - Preconditions.AtLeast(args.DeleteMessageDays, 0, nameof(args.DeleteMessageDays)); + Preconditions.AtLeast(args.DeleteMessageDays, 0, nameof(args.DeleteMessageDays), "Prune length must be within [0, 7]"); + Preconditions.AtMost(args.DeleteMessageDays, 7, nameof(args.DeleteMessageDays), "Prune length must be within [0, 7]"); options = RequestOptions.CreateOrClone(options); var ids = new BucketIds(guildId: guildId); From 5dba95fe59ebdb2aa99bceff5926fb7a746c301d Mon Sep 17 00:00:00 2001 From: Confruggy Date: Sun, 16 Apr 2017 00:04:07 +0200 Subject: [PATCH 138/243] Update ChannelHelper.cs (#606) * Update ChannelHelper.cs * typo --- src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs index 8fb26377f..6f0aa67de 100644 --- a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs +++ b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs @@ -91,6 +91,8 @@ namespace Discord.Rest var guildId = (channel as IGuildChannel)?.GuildId; var guild = guildId != null ? await (client as IDiscordClient).GetGuildAsync(guildId.Value, CacheMode.CacheOnly).ConfigureAwait(false) : null; var model = await client.ApiClient.GetChannelMessageAsync(channel.Id, id, options).ConfigureAwait(false); + if (model == null) + return null; var author = GetAuthor(client, guild, model.Author.Value, model.WebhookId.ToNullable()); return RestMessage.Create(client, channel, author, model); } From c7ea29f1f0d6fa8d1a22f7e6db76d05a61130106 Mon Sep 17 00:00:00 2001 From: Benjamin Date: Tue, 18 Apr 2017 19:51:29 +0200 Subject: [PATCH 139/243] Added Jetbrains Rider installing guide --- .../images/install-rider-add.png | Bin 0 -> 24092 bytes .../images/install-rider-nuget-manager.png | Bin 0 -> 18004 bytes .../images/install-rider-search.png | Bin 0 -> 11674 bytes docs/guides/getting_started/installing.md | 13 ++++++++++++- 4 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 docs/guides/getting_started/images/install-rider-add.png create mode 100644 docs/guides/getting_started/images/install-rider-nuget-manager.png create mode 100644 docs/guides/getting_started/images/install-rider-search.png diff --git a/docs/guides/getting_started/images/install-rider-add.png b/docs/guides/getting_started/images/install-rider-add.png new file mode 100644 index 0000000000000000000000000000000000000000..3f0dc729ead56842c76abad0598e8eb43b7d58e3 GIT binary patch literal 24092 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

)J$-!HWKkXjNsUEGm5Q>8gwMNvNY=42 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 literal 0 HcmV?d00001 diff --git a/docs/guides/getting_started/images/install-rider-nuget-manager.png b/docs/guides/getting_started/images/install-rider-nuget-manager.png new file mode 100644 index 0000000000000000000000000000000000000000..884b32d6b590862d3cecaf37a2fb99be01ae5018 GIT binary patch literal 18004 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�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 B+8zJ^ literal 0 HcmV?d00001 diff --git a/docs/guides/getting_started/installing.md b/docs/guides/getting_started/installing.md index 02c7cdab7..82d242647 100644 --- a/docs/guides/getting_started/installing.md +++ b/docs/guides/getting_started/installing.md @@ -57,7 +57,18 @@ Also make sure to check 'Enable Prereleases' if installing a dev build! ![Step 5](images/install-vs-nuget.png) ## Using JetBrains Rider -**todo** + +1. Create a new solution for your bot +2. Open the NuGet window (Tools > NuGet > Manage NuGet packages for Solution) +![Step 2](images/install-rider-nuget-manager.png) +3. In the 'Packages' tab, search for 'Discord.Net' +![Step 3](images/install-rider-search.png) + +> [!TIP] +Make sure to check the 'Prerelease' box if installing a dev build! + +4. Install by adding the package to your project +![Step 4](images/install-rider-add.png) ## Using Visual Studio Code From ff67c0d9c327911b6113c9acd7ddc9abd39bf557 Mon Sep 17 00:00:00 2001 From: RogueException Date: Wed, 19 Apr 2017 14:49:04 -0300 Subject: [PATCH 140/243] Removed unused canceltoken --- src/Discord.Net.Rest/Net/DefaultRestClient.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/Discord.Net.Rest/Net/DefaultRestClient.cs b/src/Discord.Net.Rest/Net/DefaultRestClient.cs index 3b21307e1..ceda90035 100644 --- a/src/Discord.Net.Rest/Net/DefaultRestClient.cs +++ b/src/Discord.Net.Rest/Net/DefaultRestClient.cs @@ -19,8 +19,7 @@ namespace Discord.Net.Rest private readonly HttpClient _client; private readonly string _baseUrl; private readonly JsonSerializer _errorDeserializer; - private CancellationTokenSource _cancelTokenSource; - private CancellationToken _cancelToken, _parentToken; + private CancellationToken _cancelToken; private bool _isDisposed; public DefaultRestClient(string baseUrl) @@ -35,9 +34,7 @@ namespace Discord.Net.Rest }); SetHeader("accept-encoding", "gzip, deflate"); - _cancelTokenSource = new CancellationTokenSource(); _cancelToken = CancellationToken.None; - _parentToken = CancellationToken.None; _errorDeserializer = new JsonSerializer(); } private void Dispose(bool disposing) @@ -62,8 +59,7 @@ namespace Discord.Net.Rest } public void SetCancelToken(CancellationToken cancelToken) { - _parentToken = cancelToken; - _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _cancelTokenSource.Token).Token; + _cancelToken = cancelToken; } public async Task SendAsync(string method, string endpoint, CancellationToken cancelToken, bool headerOnly) From e762dddfbbd322313ad81a20823e381cede96614 Mon Sep 17 00:00:00 2001 From: RogueException Date: Wed, 19 Apr 2017 14:50:33 -0300 Subject: [PATCH 141/243] Added SnowflakeUtils --- src/Discord.Net.Core/Utils/DateTimeUtils.cs | 5 ----- src/Discord.Net.Core/Utils/Preconditions.cs | 2 +- src/Discord.Net.Core/Utils/SnowflakeUtils.cs | 12 ++++++++++++ .../Entities/Channels/RestChannel.cs | 2 +- .../Entities/Channels/RpcVirtualMessageChannel.cs | 2 +- src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs | 2 +- .../Entities/Guilds/RestUserGuild.cs | 2 +- .../Entities/Messages/RestMessage.cs | 2 +- src/Discord.Net.Rest/Entities/RestApplication.cs | 2 +- src/Discord.Net.Rest/Entities/Roles/RestRole.cs | 2 +- src/Discord.Net.Rest/Entities/Users/RestUser.cs | 2 +- src/Discord.Net.Rpc/Entities/Channels/RpcChannel.cs | 2 +- src/Discord.Net.Rpc/Entities/Messages/RpcMessage.cs | 2 +- src/Discord.Net.Rpc/Entities/Users/RpcUser.cs | 2 +- .../Entities/Channels/SocketChannel.cs | 2 +- .../Entities/Guilds/SocketGuild.cs | 2 +- .../Entities/Messages/SocketMessage.cs | 2 +- .../Entities/Roles/SocketRole.cs | 2 +- .../Entities/Users/SocketUser.cs | 2 +- 19 files changed, 29 insertions(+), 22 deletions(-) create mode 100644 src/Discord.Net.Core/Utils/SnowflakeUtils.cs diff --git a/src/Discord.Net.Core/Utils/DateTimeUtils.cs b/src/Discord.Net.Core/Utils/DateTimeUtils.cs index ab285b56d..109c8c791 100644 --- a/src/Discord.Net.Core/Utils/DateTimeUtils.cs +++ b/src/Discord.Net.Core/Utils/DateTimeUtils.cs @@ -11,11 +11,6 @@ namespace Discord private const long UnixEpochMilliseconds = 62_135_596_800_000; #endif - public static DateTimeOffset FromSnowflake(ulong value) - => FromUnixMilliseconds((long)((value >> 22) + 1420070400000UL)); - public static ulong ToSnowflake(DateTimeOffset value) - => ((ulong)ToUnixMilliseconds(value) - 1420070400000UL) << 22; - public static DateTimeOffset FromTicks(long ticks) => new DateTimeOffset(ticks, TimeSpan.Zero); public static DateTimeOffset? FromTicks(long? ticks) diff --git a/src/Discord.Net.Core/Utils/Preconditions.cs b/src/Discord.Net.Core/Utils/Preconditions.cs index faa35e653..705a15249 100644 --- a/src/Discord.Net.Core/Utils/Preconditions.cs +++ b/src/Discord.Net.Core/Utils/Preconditions.cs @@ -185,7 +185,7 @@ namespace Discord // Bulk Delete public static void YoungerThanTwoWeeks(ulong[] collection, string name) { - var minimum = DateTimeUtils.ToSnowflake(DateTimeOffset.UtcNow.Subtract(TimeSpan.FromDays(14))); + var minimum = SnowflakeUtils.ToSnowflake(DateTimeOffset.UtcNow.Subtract(TimeSpan.FromDays(14))); for (var i = 0; i < collection.Length; i++) { if (collection[i] <= minimum) diff --git a/src/Discord.Net.Core/Utils/SnowflakeUtils.cs b/src/Discord.Net.Core/Utils/SnowflakeUtils.cs new file mode 100644 index 000000000..c9d0d130b --- /dev/null +++ b/src/Discord.Net.Core/Utils/SnowflakeUtils.cs @@ -0,0 +1,12 @@ +using System; + +namespace Discord +{ + public static class SnowflakeUtils + { + public static DateTimeOffset FromSnowflake(ulong value) + => DateTimeUtils.FromUnixMilliseconds((long)((value >> 22) + 1420070400000UL)); + public static ulong ToSnowflake(DateTimeOffset value) + => ((ulong)DateTimeUtils.ToUnixMilliseconds(value) - 1420070400000UL) << 22; + } +} diff --git a/src/Discord.Net.Rest/Entities/Channels/RestChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestChannel.cs index 0481d37ed..bc521784d 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestChannel.cs @@ -8,7 +8,7 @@ namespace Discord.Rest { public abstract class RestChannel : RestEntity, IChannel, IUpdateable { - public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); + public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); internal RestChannel(BaseDiscordClient discord, ulong id) : base(discord, id) diff --git a/src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs index 664e9c9fc..2c69a3395 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs @@ -10,7 +10,7 @@ namespace Discord.Rest [DebuggerDisplay(@"{DebuggerDisplay,nq}")] internal class RestVirtualMessageChannel : RestEntity, IMessageChannel { - public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); + public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); public string Mention => MentionUtils.MentionChannel(Id); internal RestVirtualMessageChannel(BaseDiscordClient discord, ulong id) diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs index 15acef457..eff178db6 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -32,7 +32,7 @@ namespace Discord.Rest public string SplashId { get; private set; } internal bool Available { get; private set; } - public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); + public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); public ulong DefaultChannelId => Id; public string IconUrl => CDN.GetGuildIconUrl(Id, IconId); public string SplashUrl => CDN.GetGuildSplashUrl(Id, SplashId); diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestUserGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestUserGuild.cs index 12601b72e..6bc9cea7a 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestUserGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestUserGuild.cs @@ -14,7 +14,7 @@ namespace Discord.Rest public bool IsOwner { get; private set; } public GuildPermissions Permissions { get; private set; } - public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); + public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); public string IconUrl => CDN.GetGuildIconUrl(Id, _iconId); internal RestUserGuild(BaseDiscordClient discord, ulong id) diff --git a/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs b/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs index bdd4800c1..590886886 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs @@ -17,7 +17,7 @@ namespace Discord.Rest public string Content { get; private set; } - public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); + public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); public virtual bool IsTTS => false; public virtual bool IsPinned => false; public virtual DateTimeOffset? EditedTimestamp => null; diff --git a/src/Discord.Net.Rest/Entities/RestApplication.cs b/src/Discord.Net.Rest/Entities/RestApplication.cs index f81e4cd7b..827c33cf7 100644 --- a/src/Discord.Net.Rest/Entities/RestApplication.cs +++ b/src/Discord.Net.Rest/Entities/RestApplication.cs @@ -17,7 +17,7 @@ namespace Discord.Rest public IUser Owner { get; private set; } - public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); + public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); public string IconUrl => CDN.GetApplicationIconUrl(Id, _iconId); internal RestApplication(BaseDiscordClient discord, ulong id) diff --git a/src/Discord.Net.Rest/Entities/Roles/RestRole.cs b/src/Discord.Net.Rest/Entities/Roles/RestRole.cs index dfdbb150d..9807b8357 100644 --- a/src/Discord.Net.Rest/Entities/Roles/RestRole.cs +++ b/src/Discord.Net.Rest/Entities/Roles/RestRole.cs @@ -17,7 +17,7 @@ namespace Discord.Rest public GuildPermissions Permissions { get; private set; } public int Position { get; private set; } - public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); + public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); public bool IsEveryone => Id == Guild.Id; public string Mention => MentionUtils.MentionRole(Id); diff --git a/src/Discord.Net.Rest/Entities/Users/RestUser.cs b/src/Discord.Net.Rest/Entities/Users/RestUser.cs index e9b3b39ea..cded876c8 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestUser.cs @@ -13,7 +13,7 @@ namespace Discord.Rest public ushort DiscriminatorValue { get; private set; } public string AvatarId { get; private set; } - public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); + public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); public string Discriminator => DiscriminatorValue.ToString("D4"); public string Mention => MentionUtils.MentionUser(Id); public virtual Game? Game => null; diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcChannel.cs b/src/Discord.Net.Rpc/Entities/Channels/RpcChannel.cs index 934dae94b..cca559a31 100644 --- a/src/Discord.Net.Rpc/Entities/Channels/RpcChannel.cs +++ b/src/Discord.Net.Rpc/Entities/Channels/RpcChannel.cs @@ -8,7 +8,7 @@ namespace Discord.Rpc { public string Name { get; private set; } - public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); + public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); internal RpcChannel(DiscordRpcClient discord, ulong id) : base(discord, id) diff --git a/src/Discord.Net.Rpc/Entities/Messages/RpcMessage.cs b/src/Discord.Net.Rpc/Entities/Messages/RpcMessage.cs index c77c06288..a2f7b4558 100644 --- a/src/Discord.Net.Rpc/Entities/Messages/RpcMessage.cs +++ b/src/Discord.Net.Rpc/Entities/Messages/RpcMessage.cs @@ -18,7 +18,7 @@ namespace Discord.Rpc public string Content { get; private set; } public Color AuthorColor { get; private set; } - public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); + public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); public virtual bool IsTTS => false; public virtual bool IsPinned => false; public virtual bool IsBlocked => false; diff --git a/src/Discord.Net.Rpc/Entities/Users/RpcUser.cs b/src/Discord.Net.Rpc/Entities/Users/RpcUser.cs index cf21928bb..7ed11e57d 100644 --- a/src/Discord.Net.Rpc/Entities/Users/RpcUser.cs +++ b/src/Discord.Net.Rpc/Entities/Users/RpcUser.cs @@ -14,7 +14,7 @@ namespace Discord.Rpc public ushort DiscriminatorValue { get; private set; } public string AvatarId { get; private set; } - public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); + public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); public string Discriminator => DiscriminatorValue.ToString("D4"); public string Mention => MentionUtils.MentionUser(Id); public virtual bool IsWebhook => false; diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketChannel.cs index f982e66b5..319e17c50 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketChannel.cs @@ -10,7 +10,7 @@ namespace Discord.WebSocket [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public abstract class SocketChannel : SocketEntity, IChannel { - public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); + public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); public IReadOnlyCollection Users => GetUsersInternal(); internal SocketChannel(DiscordSocketClient discord, ulong id) diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index 1c2ee1847..889587921 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -53,7 +53,7 @@ namespace Discord.WebSocket public string IconId { get; private set; } public string SplashId { get; private set; } - public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); + public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); public SocketTextChannel DefaultChannel => GetTextChannel(Id); public string IconUrl => CDN.GetGuildIconUrl(Id, IconId); public string SplashUrl => CDN.GetGuildSplashUrl(Id, SplashId); diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs index 2d63665de..5442c888a 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs @@ -18,7 +18,7 @@ namespace Discord.WebSocket public string Content { get; private set; } - public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); + public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); public virtual bool IsTTS => false; public virtual bool IsPinned => false; public virtual DateTimeOffset? EditedTimestamp => null; diff --git a/src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs b/src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs index 61fd4310f..57d913317 100644 --- a/src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs +++ b/src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs @@ -19,7 +19,7 @@ namespace Discord.WebSocket public GuildPermissions Permissions { get; private set; } public int Position { get; private set; } - public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); + public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); public bool IsEveryone => Id == Guild.Id; public string Mention => MentionUtils.MentionRole(Id); diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs index da15ccbf9..1b599bf7e 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs @@ -15,7 +15,7 @@ namespace Discord.WebSocket internal abstract SocketGlobalUser GlobalUser { get; } internal abstract SocketPresence Presence { get; set; } - public DateTimeOffset CreatedAt => DateTimeUtils.FromSnowflake(Id); + public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); public string Discriminator => DiscriminatorValue.ToString("D4"); public string Mention => MentionUtils.MentionUser(Id); public Game? Game => Presence.Game; From 1c6eebf875d315b1d1b92dc555d49a61b8220428 Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 22 Apr 2017 08:04:33 -0300 Subject: [PATCH 142/243] Ensure command completes before cleaning up --- src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs index 4e8ef2664..fb93cc5ec 100644 --- a/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs +++ b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs @@ -160,14 +160,15 @@ namespace Discord.Commands var createInstance = ReflectionUtils.CreateBuilder(typeInfo, service); - builder.Callback = (ctx, args, map) => + builder.Callback = async (ctx, args, map) => { var instance = createInstance(map); instance.SetContext(ctx); try { instance.BeforeExecute(); - return method.Invoke(instance, args) as Task ?? Task.Delay(0); + var task = method.Invoke(instance, args) as Task ?? Task.Delay(0); + await task.ConfigureAwait(false); } finally { From 6000b15c4ddbb59ffe7818a11c48fd415da46cb8 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Sun, 23 Apr 2017 15:13:31 -0400 Subject: [PATCH 143/243] C#7 TODOs --- .../Builders/ModuleClassBuilder.cs | 35 +++++++++++-------- .../Permissions/ChannelPermissions.cs | 15 ++++---- src/Discord.Net.Core/Utils/Permissions.cs | 4 +-- src/Discord.Net.Rest/Net/DefaultRestClient.cs | 35 +++++++++---------- .../Entities/Channels/SocketChannelHelper.cs | 32 ++++++++--------- 5 files changed, 59 insertions(+), 62 deletions(-) diff --git a/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs index fb93cc5ec..25b6e034b 100644 --- a/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs +++ b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs @@ -81,23 +81,28 @@ namespace Discord.Commands foreach (var attribute in attributes) { - // TODO: C#7 type switch - if (attribute is NameAttribute) - builder.Name = (attribute as NameAttribute).Text; - else if (attribute is SummaryAttribute) - builder.Summary = (attribute as SummaryAttribute).Text; - else if (attribute is RemarksAttribute) - builder.Remarks = (attribute as RemarksAttribute).Text; - else if (attribute is AliasAttribute) - builder.AddAliases((attribute as AliasAttribute).Aliases); - else if (attribute is GroupAttribute) + switch (attribute) { - var groupAttr = attribute as GroupAttribute; - builder.Name = builder.Name ?? groupAttr.Prefix; - builder.AddAliases(groupAttr.Prefix); + case NameAttribute name: + builder.Name = name.Text; + break; + case SummaryAttribute summary: + builder.Summary = summary.Text; + break; + case RemarksAttribute remarks: + builder.Remarks = remarks.Text; + break; + case AliasAttribute alias: + builder.AddAliases(alias.Aliases); + break; + case GroupAttribute group: + builder.Name = builder.Name ?? group.Prefix; + builder.AddAliases(group.Prefix); + break; + case PreconditionAttribute precondition: + builder.AddPrecondition(precondition); + break; } - else if (attribute is PreconditionAttribute) - builder.AddPrecondition(attribute as PreconditionAttribute); } //Check for unspecified info diff --git a/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs b/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs index 054b80119..94596e0e6 100644 --- a/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs +++ b/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs @@ -20,13 +20,14 @@ namespace Discord ///

Gets a ChannelPermissions that grants all permissions for a given channelType. public static ChannelPermissions All(IChannel channel) { - //TODO: C#7 Candidate for typeswitch - if (channel is ITextChannel) return Text; - if (channel is IVoiceChannel) return Voice; - if (channel is IDMChannel) return DM; - if (channel is IGroupChannel) return Group; - - throw new ArgumentException("Unknown channel type", nameof(channel)); + switch (channel) + { + case ITextChannel _: return Text; + case IVoiceChannel _: return Voice; + case IDMChannel _: return DM; + case IGroupChannel _: return Group; + default: throw new ArgumentException("Unknown channel type", nameof(channel)); + } } /// Gets a packed value representing all the permissions in this ChannelPermissions. diff --git a/src/Discord.Net.Core/Utils/Permissions.cs b/src/Discord.Net.Core/Utils/Permissions.cs index b69b103e1..c2b7e83ea 100644 --- a/src/Discord.Net.Core/Utils/Permissions.cs +++ b/src/Discord.Net.Core/Utils/Permissions.cs @@ -150,9 +150,7 @@ namespace Discord if (perms != null) resolvedPermissions = (resolvedPermissions & ~perms.Value.DenyValue) | perms.Value.AllowValue; - //TODO: C#7 Typeswitch candidate - var textChannel = channel as ITextChannel; - if (textChannel != null) + if (channel is ITextChannel textChannel) { if (!GetValue(resolvedPermissions, ChannelPermission.ReadMessages)) { diff --git a/src/Discord.Net.Rest/Net/DefaultRestClient.cs b/src/Discord.Net.Rest/Net/DefaultRestClient.cs index ceda90035..493fc3aff 100644 --- a/src/Discord.Net.Rest/Net/DefaultRestClient.cs +++ b/src/Discord.Net.Rest/Net/DefaultRestClient.cs @@ -87,29 +87,26 @@ namespace Discord.Net.Rest { foreach (var p in multipartParams) { - //TODO: C#7 Typeswitch candidate - var stringValue = p.Value as string; - if (stringValue != null) { content.Add(new StringContent(stringValue), p.Key); continue; } - var byteArrayValue = p.Value as byte[]; - if (byteArrayValue != null) { content.Add(new ByteArrayContent(byteArrayValue), p.Key); continue; } - var streamValue = p.Value as Stream; - if (streamValue != null) { content.Add(new StreamContent(streamValue), p.Key); continue; } - if (p.Value is MultipartFile) + switch (p.Value) { - var fileValue = (MultipartFile)p.Value; - var stream = fileValue.Stream; - if (!stream.CanSeek) + case string stringValue: { content.Add(new StringContent(stringValue), p.Key); continue; } + case byte[] byteArrayValue: { content.Add(new ByteArrayContent(byteArrayValue), p.Key); continue; } + case Stream streamValue: { content.Add(new StreamContent(streamValue), p.Key); continue; } + case MultipartFile fileValue: { - var memoryStream = new MemoryStream(); - await stream.CopyToAsync(memoryStream).ConfigureAwait(false); - memoryStream.Position = 0; - stream = memoryStream; + var stream = fileValue.Stream; + if (!stream.CanSeek) + { + var memoryStream = new MemoryStream(); + await stream.CopyToAsync(memoryStream).ConfigureAwait(false); + memoryStream.Position = 0; + stream = memoryStream; + } + content.Add(new StreamContent(stream), p.Key, fileValue.Filename); + continue; } - content.Add(new StreamContent(stream), p.Key, fileValue.Filename); - continue; + default: throw new InvalidOperationException($"Unsupported param type \"{p.Value.GetType().Name}\""); } - - throw new InvalidOperationException($"Unsupported param type \"{p.Value.GetType().Name}\""); } } restRequest.Content = content; diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketChannelHelper.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketChannelHelper.cs index 1bc0fc9b5..ca53315aa 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketChannelHelper.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketChannelHelper.cs @@ -61,28 +61,24 @@ namespace Discord.WebSocket public static void AddMessage(ISocketMessageChannel channel, DiscordSocketClient discord, SocketMessage msg) { - //TODO: C#7 Candidate for pattern matching - if (channel is SocketDMChannel) - (channel as SocketDMChannel).AddMessage(msg); - else if (channel is SocketGroupChannel) - (channel as SocketGroupChannel).AddMessage(msg); - else if (channel is SocketTextChannel) - (channel as SocketTextChannel).AddMessage(msg); - else - throw new NotSupportedException("Unexpected ISocketMessageChannel type"); + switch (channel) + { + case SocketDMChannel dmChannel: dmChannel.AddMessage(msg); break; + case SocketGroupChannel groupChannel: groupChannel.AddMessage(msg); break; + case SocketTextChannel textChannel: textChannel.AddMessage(msg); break; + default: throw new NotSupportedException("Unexpected ISocketMessageChannel type"); + } } public static SocketMessage RemoveMessage(ISocketMessageChannel channel, DiscordSocketClient discord, ulong id) { - //TODO: C#7 Candidate for pattern matching - if (channel is SocketDMChannel) - return (channel as SocketDMChannel).RemoveMessage(id); - else if (channel is SocketGroupChannel) - return (channel as SocketGroupChannel).RemoveMessage(id); - else if (channel is SocketTextChannel) - return (channel as SocketTextChannel).RemoveMessage(id); - else - throw new NotSupportedException("Unexpected ISocketMessageChannel type"); + switch (channel) + { + case SocketDMChannel dmChannel: return dmChannel.RemoveMessage(id); + case SocketGroupChannel groupChannel: return groupChannel.RemoveMessage(id); + case SocketTextChannel textChannel: return textChannel.RemoveMessage(id); + default: throw new NotSupportedException("Unexpected ISocketMessageChannel type"); + } } } } From 431b7fbd9f227f9449be246d1e4ada1959681d34 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Sun, 23 Apr 2017 15:23:06 -0400 Subject: [PATCH 144/243] Visual Studio C#7 suggestions --- .../Entities/Messages/Emoji.cs | 6 +-- .../Utils/ConcurrentHashSet.cs | 11 ++--- src/Discord.Net.Core/Utils/MentionUtils.cs | 9 ++-- .../Entities/Channels/RestGroupChannel.cs | 3 +- .../Entities/Guilds/RestGuild.cs | 3 +- .../Entities/Invites/RestInvite.cs | 3 +- .../Entities/Messages/MessageHelper.cs | 3 +- .../Net/Converters/DiscordContractResolver.cs | 3 +- .../Net/Queue/RequestQueue.cs | 3 +- src/Discord.Net.Rest/Net/RateLimitInfo.cs | 5 +- src/Discord.Net.Rpc/DiscordRpcApiClient.cs | 3 +- .../Audio/AudioClient.cs | 9 ++-- .../Audio/Streams/BufferedWriteStream.cs | 11 ++--- .../Audio/Streams/InputStream.cs | 3 +- .../Audio/Streams/JitterBuffer.cs | 9 ++-- src/Discord.Net.WebSocket/ClientState.cs | 36 +++++--------- .../DiscordSocketClient.cs | 48 +++++++------------ .../Entities/Channels/SocketGroupChannel.cs | 15 ++---- .../Entities/Guilds/SocketGuild.cs | 44 +++++------------ .../Entities/Messages/MessageCache.cs | 15 ++---- .../Net/DefaultWebSocketClient.cs | 3 +- 21 files changed, 80 insertions(+), 165 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Messages/Emoji.cs b/src/Discord.Net.Core/Entities/Messages/Emoji.cs index f0a0489e2..a9c5a6bbd 100644 --- a/src/Discord.Net.Core/Entities/Messages/Emoji.cs +++ b/src/Discord.Net.Core/Entities/Messages/Emoji.cs @@ -20,8 +20,7 @@ namespace Discord public static Emoji Parse(string text) { - Emoji result; - if (TryParse(text, out result)) + if (TryParse(text, out Emoji result)) return result; throw new ArgumentException("Invalid emoji format", nameof(text)); } @@ -35,8 +34,7 @@ namespace Discord if (splitIndex == -1) return false; - ulong id; - if (!ulong.TryParse(text.Substring(splitIndex + 1, text.Length - splitIndex - 2), NumberStyles.None, CultureInfo.InvariantCulture, out id)) + if (!ulong.TryParse(text.Substring(splitIndex + 1, text.Length - splitIndex - 2), NumberStyles.None, CultureInfo.InvariantCulture, out ulong id)) return false; string name = text.Substring(2, splitIndex - 2); diff --git a/src/Discord.Net.Core/Utils/ConcurrentHashSet.cs b/src/Discord.Net.Core/Utils/ConcurrentHashSet.cs index 1ef105527..1fc11587e 100644 --- a/src/Discord.Net.Core/Utils/ConcurrentHashSet.cs +++ b/src/Discord.Net.Core/Utils/ConcurrentHashSet.cs @@ -239,10 +239,8 @@ namespace Discord { while (true) { - int bucketNo, lockNo; - Tables tables = _tables; - GetBucketAndLockNo(hashcode, out bucketNo, out lockNo, tables._buckets.Length, tables._locks.Length); + GetBucketAndLockNo(hashcode, out int bucketNo, out int lockNo, tables._buckets.Length, tables._locks.Length); bool resizeDesired = false; bool lockTaken = false; @@ -292,9 +290,7 @@ namespace Discord while (true) { Tables tables = _tables; - - int bucketNo, lockNo; - GetBucketAndLockNo(hashcode, out bucketNo, out lockNo, tables._buckets.Length, tables._locks.Length); + GetBucketAndLockNo(hashcode, out int bucketNo, out int lockNo, tables._buckets.Length, tables._locks.Length); lock (tables._locks[lockNo]) { @@ -426,8 +422,7 @@ namespace Discord while (current != null) { Node next = current._next; - int newBucketNo, newLockNo; - GetBucketAndLockNo(current._hashcode, out newBucketNo, out newLockNo, newBuckets.Length, newLocks.Length); + GetBucketAndLockNo(current._hashcode, out int newBucketNo, out int newLockNo, newBuckets.Length, newLocks.Length); newBuckets[newBucketNo] = new Node(current._value, current._hashcode, newBuckets[newBucketNo]); diff --git a/src/Discord.Net.Core/Utils/MentionUtils.cs b/src/Discord.Net.Core/Utils/MentionUtils.cs index 60e065b62..2af254fde 100644 --- a/src/Discord.Net.Core/Utils/MentionUtils.cs +++ b/src/Discord.Net.Core/Utils/MentionUtils.cs @@ -19,8 +19,7 @@ namespace Discord /// Parses a provided user mention string. public static ulong ParseUser(string text) { - ulong id; - if (TryParseUser(text, out id)) + if (TryParseUser(text, out ulong id)) return id; throw new ArgumentException("Invalid mention format", nameof(text)); } @@ -44,8 +43,7 @@ namespace Discord /// Parses a provided channel mention string. public static ulong ParseChannel(string text) { - ulong id; - if (TryParseChannel(text, out id)) + if (TryParseChannel(text, out ulong id)) return id; throw new ArgumentException("Invalid mention format", nameof(text)); } @@ -66,8 +64,7 @@ namespace Discord /// Parses a provided role mention string. public static ulong ParseRole(string text) { - ulong id; - if (TryParseRole(text, out id)) + if (TryParseRole(text, out ulong id)) return id; throw new ArgumentException("Invalid mention format", nameof(text)); } diff --git a/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs index e3ba4e94b..93e24e05b 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs @@ -60,8 +60,7 @@ namespace Discord.Rest public RestUser GetUser(ulong id) { - RestGroupUser user; - if (_users.TryGetValue(id, out user)) + if (_users.TryGetValue(id, out RestGroupUser user)) return user; return null; } diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs index eff178db6..5a47fce81 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -213,8 +213,7 @@ namespace Discord.Rest //Roles public RestRole GetRole(ulong id) { - RestRole value; - if (_roles.TryGetValue(id, out value)) + if (_roles.TryGetValue(id, out RestRole value)) return value; return null; } diff --git a/src/Discord.Net.Rest/Entities/Invites/RestInvite.cs b/src/Discord.Net.Rest/Entities/Invites/RestInvite.cs index 9e2249bff..900d1f0ac 100644 --- a/src/Discord.Net.Rest/Entities/Invites/RestInvite.cs +++ b/src/Discord.Net.Rest/Entities/Invites/RestInvite.cs @@ -58,8 +58,7 @@ namespace Discord.Rest { if (Guild != null) return Guild; - var guildChannel = Channel as IGuildChannel; - if (guildChannel != null) + if (Channel is IGuildChannel guildChannel) return guildChannel.Guild; //If it fails, it'll still return this exception throw new InvalidOperationException("Unable to return this entity's parent unless it was fetched through that object."); } diff --git a/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs b/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs index 367a33be6..285cf0e74 100644 --- a/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs +++ b/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs @@ -80,8 +80,7 @@ namespace Discord.Rest if (endIndex == -1) break; string content = text.Substring(index, endIndex - index + 1); - ulong id; - if (MentionUtils.TryParseUser(content, out id)) + if (MentionUtils.TryParseUser(content, out ulong id)) { IUser mentionedUser = null; foreach (var mention in userMentions) diff --git a/src/Discord.Net.Rest/Net/Converters/DiscordContractResolver.cs b/src/Discord.Net.Rest/Net/Converters/DiscordContractResolver.cs index 104b913da..b465fbed2 100644 --- a/src/Discord.Net.Rest/Net/Converters/DiscordContractResolver.cs +++ b/src/Discord.Net.Rest/Net/Converters/DiscordContractResolver.cs @@ -19,8 +19,7 @@ namespace Discord.Net.Converters if (property.Ignored) return property; - var propInfo = member as PropertyInfo; - if (propInfo != null) + if (member is PropertyInfo propInfo) { var converter = GetConverter(property, propInfo, propInfo.PropertyType, 0); if (converter != null) diff --git a/src/Discord.Net.Rest/Net/Queue/RequestQueue.cs b/src/Discord.Net.Rest/Net/Queue/RequestQueue.cs index fce7e3e1b..943b76359 100644 --- a/src/Discord.Net.Rest/Net/Queue/RequestQueue.cs +++ b/src/Discord.Net.Rest/Net/Queue/RequestQueue.cs @@ -114,9 +114,8 @@ namespace Discord.Net.Queue var now = DateTimeOffset.UtcNow; foreach (var bucket in _buckets.Select(x => x.Value)) { - RequestBucket ignored; if ((now - bucket.LastAttemptAt).TotalMinutes > 1.0) - _buckets.TryRemove(bucket.Id, out ignored); + _buckets.TryRemove(bucket.Id, out RequestBucket ignored); } await Task.Delay(60000, _cancelToken.Token); //Runs each minute } diff --git a/src/Discord.Net.Rest/Net/RateLimitInfo.cs b/src/Discord.Net.Rest/Net/RateLimitInfo.cs index 79fe47dd1..9421221ed 100644 --- a/src/Discord.Net.Rest/Net/RateLimitInfo.cs +++ b/src/Discord.Net.Rest/Net/RateLimitInfo.cs @@ -14,9 +14,8 @@ namespace Discord.Net internal RateLimitInfo(Dictionary headers) { - string temp; - IsGlobal = headers.TryGetValue("X-RateLimit-Global", out temp) && - bool.TryParse(temp, out var isGlobal) ? isGlobal : false; + IsGlobal = headers.TryGetValue("X-RateLimit-Global", out string temp) && + bool.TryParse(temp, out var isGlobal) ? isGlobal : false; Limit = headers.TryGetValue("X-RateLimit-Limit", out temp) && int.TryParse(temp, out var limit) ? limit : (int?)null; Remaining = headers.TryGetValue("X-RateLimit-Remaining", out temp) && diff --git a/src/Discord.Net.Rpc/DiscordRpcApiClient.cs b/src/Discord.Net.Rpc/DiscordRpcApiClient.cs index 8c83d24d6..50d467054 100644 --- a/src/Discord.Net.Rpc/DiscordRpcApiClient.cs +++ b/src/Discord.Net.Rpc/DiscordRpcApiClient.cs @@ -378,8 +378,7 @@ namespace Discord.API private bool ProcessMessage(API.Rpc.RpcFrame msg) { - RpcRequest requestTracker; - if (_requests.TryGetValue(msg.Nonce.Value.Value, out requestTracker)) + if (_requests.TryGetValue(msg.Nonce.Value.Value, out RpcRequest requestTracker)) { if (msg.Event.GetValueOrDefault("") == "ERROR") { diff --git a/src/Discord.Net.WebSocket/Audio/AudioClient.cs b/src/Discord.Net.WebSocket/Audio/AudioClient.cs index 405ff394e..19639a418 100644 --- a/src/Discord.Net.WebSocket/Audio/AudioClient.cs +++ b/src/Discord.Net.WebSocket/Audio/AudioClient.cs @@ -131,8 +131,7 @@ namespace Discord.Audio await keepaliveTask.ConfigureAwait(false); _keepaliveTask = null; - long time; - while (_heartbeatTimes.TryDequeue(out time)) { } + while (_heartbeatTimes.TryDequeue(out long time)) { } _lastMessageTime = 0; await ClearInputStreamsAsync().ConfigureAwait(false); @@ -186,8 +185,7 @@ namespace Discord.Audio } internal AudioInStream GetInputStream(ulong id) { - StreamPair streamPair; - if (_streams.TryGetValue(id, out streamPair)) + if (_streams.TryGetValue(id, out StreamPair streamPair)) return streamPair.Reader; return null; } @@ -254,8 +252,7 @@ namespace Discord.Audio { await _audioLogger.DebugAsync("Received HeartbeatAck").ConfigureAwait(false); - long time; - if (_heartbeatTimes.TryDequeue(out time)) + if (_heartbeatTimes.TryDequeue(out long time)) { int latency = (int)(Environment.TickCount - time); int before = Latency; diff --git a/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs index e73eb2cc2..29586389c 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs @@ -85,8 +85,7 @@ namespace Discord.Audio.Streams long dist = nextTick - tick; if (dist <= 0) { - Frame frame; - if (_queuedFrames.TryDequeue(out frame)) + if (_queuedFrames.TryDequeue(out Frame frame)) { await _client.SetSpeakingAsync(true).ConfigureAwait(false); _next.WriteHeader(seq++, timestamp, false); @@ -100,7 +99,7 @@ namespace Discord.Audio.Streams var _ = _logger?.DebugAsync($"Sent {frame.Bytes} bytes ({_queuedFrames.Count} frames buffered)"); #endif } - else + else { while ((nextTick - tick) <= 0) { @@ -135,8 +134,7 @@ namespace Discord.Audio.Streams cancelToken = _cancelToken; await _queueLock.WaitAsync(-1, cancelToken).ConfigureAwait(false); - byte[] buffer; - if (!_bufferPool.TryDequeue(out buffer)) + if (!_bufferPool.TryDequeue(out byte[] buffer)) { #if DEBUG var _ = _logger?.DebugAsync($"Buffer overflow"); //Should never happen because of the queueLock @@ -166,10 +164,9 @@ namespace Discord.Audio.Streams } public override Task ClearAsync(CancellationToken cancelToken) { - Frame ignored; do cancelToken.ThrowIfCancellationRequested(); - while (_queuedFrames.TryDequeue(out ignored)); + while (_queuedFrames.TryDequeue(out Frame ignored)); return Task.Delay(0); } } diff --git a/src/Discord.Net.WebSocket/Audio/Streams/InputStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/InputStream.cs index a46b6d3d2..b9d6157ea 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/InputStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/InputStream.cs @@ -55,9 +55,8 @@ namespace Discord.Audio.Streams { cancelToken.ThrowIfCancellationRequested(); - RTPFrame frame; await _signal.WaitAsync(cancelToken).ConfigureAwait(false); - _frames.TryDequeue(out frame); + _frames.TryDequeue(out RTPFrame frame); return frame; } diff --git a/src/Discord.Net.WebSocket/Audio/Streams/JitterBuffer.cs b/src/Discord.Net.WebSocket/Audio/Streams/JitterBuffer.cs index 2038e605a..a5ecdea6f 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/JitterBuffer.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/JitterBuffer.cs @@ -94,8 +94,7 @@ namespace Discord.Audio.Streams continue; } - Frame frame; - if (_queuedFrames.TryPeek(out frame)) + if (_queuedFrames.TryPeek(out Frame frame)) { silenceFrames = 0; uint distance = (uint)(frame.Timestamp - _timestamp); @@ -201,7 +200,6 @@ namespace Discord.Audio.Streams return; //This is an old frame, ignore } - byte[] buffer; if (!await _queueLock.WaitAsync(0).ConfigureAwait(false)) { #if DEBUG @@ -209,7 +207,7 @@ namespace Discord.Audio.Streams #endif return; } - _bufferPool.TryDequeue(out buffer); + _bufferPool.TryDequeue(out byte[] buffer); Buffer.BlockCopy(data, offset, buffer, 0, count); #if DEBUG @@ -239,10 +237,9 @@ namespace Discord.Audio.Streams } public override Task ClearAsync(CancellationToken cancelToken) { - Frame ignored; do cancelToken.ThrowIfCancellationRequested(); - while (_queuedFrames.TryDequeue(out ignored)); + while (_queuedFrames.TryDequeue(out Frame ignored)); return Task.Delay(0); } } diff --git a/src/Discord.Net.WebSocket/ClientState.cs b/src/Discord.Net.WebSocket/ClientState.cs index a452113e2..8bc745211 100644 --- a/src/Discord.Net.WebSocket/ClientState.cs +++ b/src/Discord.Net.WebSocket/ClientState.cs @@ -41,15 +41,13 @@ namespace Discord.WebSocket internal SocketChannel GetChannel(ulong id) { - SocketChannel channel; - if (_channels.TryGetValue(id, out channel)) + if (_channels.TryGetValue(id, out SocketChannel channel)) return channel; return null; } internal SocketDMChannel GetDMChannel(ulong userId) { - SocketDMChannel channel; - if (_dmChannels.TryGetValue(userId, out channel)) + if (_dmChannels.TryGetValue(userId, out SocketDMChannel channel)) return channel; return null; } @@ -57,31 +55,25 @@ namespace Discord.WebSocket { _channels[channel.Id] = channel; - var dmChannel = channel as SocketDMChannel; - if (dmChannel != null) + if (channel is SocketDMChannel dmChannel) _dmChannels[dmChannel.Recipient.Id] = dmChannel; else { - var groupChannel = channel as SocketGroupChannel; - if (groupChannel != null) + if (channel is SocketGroupChannel groupChannel) _groupChannels.TryAdd(groupChannel.Id); } } internal SocketChannel RemoveChannel(ulong id) { - SocketChannel channel; - if (_channels.TryRemove(id, out channel)) + if (_channels.TryRemove(id, out SocketChannel channel)) { - var dmChannel = channel as SocketDMChannel; - if (dmChannel != null) + if (channel is SocketDMChannel dmChannel) { - SocketDMChannel ignored; - _dmChannels.TryRemove(dmChannel.Recipient.Id, out ignored); + _dmChannels.TryRemove(dmChannel.Recipient.Id, out SocketDMChannel ignored); } else { - var groupChannel = channel as SocketGroupChannel; - if (groupChannel != null) + if (channel is SocketGroupChannel groupChannel) _groupChannels.TryRemove(id); } return channel; @@ -91,8 +83,7 @@ namespace Discord.WebSocket internal SocketGuild GetGuild(ulong id) { - SocketGuild guild; - if (_guilds.TryGetValue(id, out guild)) + if (_guilds.TryGetValue(id, out SocketGuild guild)) return guild; return null; } @@ -102,16 +93,14 @@ namespace Discord.WebSocket } internal SocketGuild RemoveGuild(ulong id) { - SocketGuild guild; - if (_guilds.TryRemove(id, out guild)) + if (_guilds.TryRemove(id, out SocketGuild guild)) return guild; return null; } internal SocketGlobalUser GetUser(ulong id) { - SocketGlobalUser user; - if (_users.TryGetValue(id, out user)) + if (_users.TryGetValue(id, out SocketGlobalUser user)) return user; return null; } @@ -121,8 +110,7 @@ namespace Discord.WebSocket } internal SocketGlobalUser RemoveUser(ulong id) { - SocketGlobalUser user; - if (_users.TryRemove(id, out user)) + if (_users.TryRemove(id, out SocketGlobalUser user)) return user; return null; } diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index 96eccacfe..4d79ff964 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -197,7 +197,6 @@ namespace Discord.WebSocket } private async Task OnDisconnectingAsync(Exception ex) { - ulong guildId; await _gatewayLogger.DebugAsync("Disconnecting ApiClient").ConfigureAwait(false); await ApiClient.DisconnectAsync().ConfigureAwait(false); @@ -209,8 +208,7 @@ namespace Discord.WebSocket await heartbeatTask.ConfigureAwait(false); _heartbeatTask = null; - long time; - while (_heartbeatTimes.TryDequeue(out time)) { } + while (_heartbeatTimes.TryDequeue(out long time)) { } _lastMessageTime = 0; await _gatewayLogger.DebugAsync("Waiting for guild downloader").ConfigureAwait(false); @@ -221,7 +219,7 @@ namespace Discord.WebSocket //Clear large guild queue await _gatewayLogger.DebugAsync("Clearing large guild queue").ConfigureAwait(false); - while (_largeGuilds.TryDequeue(out guildId)) { } + while (_largeGuilds.TryDequeue(out ulong guildId)) { } //Raise virtual GUILD_UNAVAILABLEs await _gatewayLogger.DebugAsync("Raising virtual GuildUnavailables").ConfigureAwait(false); @@ -298,8 +296,7 @@ namespace Discord.WebSocket /// public RestVoiceRegion GetVoiceRegion(string id) { - RestVoiceRegion region; - if (_voiceRegions.TryGetValue(id, out region)) + if (_voiceRegions.TryGetValue(id, out RestVoiceRegion region)) return region; return null; } @@ -417,8 +414,7 @@ namespace Discord.WebSocket { await _gatewayLogger.DebugAsync("Received HeartbeatAck").ConfigureAwait(false); - long time; - if (_heartbeatTimes.TryDequeue(out time)) + if (_heartbeatTimes.TryDequeue(out long time)) { int latency = (int)(Environment.TickCount - time); int before = Latency; @@ -903,8 +899,7 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_RECIPIENT_ADD)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var channel = State.GetChannel(data.ChannelId) as SocketGroupChannel; - if (channel != null) + if (State.GetChannel(data.ChannelId) is SocketGroupChannel channel) { var user = channel.GetOrAddUser(data.User); await TimedInvokeAsync(_recipientAddedEvent, nameof(RecipientAdded), user).ConfigureAwait(false); @@ -921,8 +916,7 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_RECIPIENT_REMOVE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var channel = State.GetChannel(data.ChannelId) as SocketGroupChannel; - if (channel != null) + if (State.GetChannel(data.ChannelId) is SocketGroupChannel channel) { var user = channel.RemoveUser(data.User.Id); if (user != null) @@ -1094,12 +1088,11 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_CREATE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var channel = State.GetChannel(data.ChannelId) as ISocketMessageChannel; - if (channel != null) + if (State.GetChannel(data.ChannelId) is ISocketMessageChannel channel) { var guild = (channel as SocketGuildChannel)?.Guild; if (guild != null && !guild.IsSynced) - { + { await UnsyncedGuildAsync(type, guild.Id).ConfigureAwait(false); return; } @@ -1142,8 +1135,7 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_UPDATE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var channel = State.GetChannel(data.ChannelId) as ISocketMessageChannel; - if (channel != null) + if (State.GetChannel(data.ChannelId) is ISocketMessageChannel channel) { var guild = (channel as SocketGuildChannel)?.Guild; if (guild != null && !guild.IsSynced) @@ -1174,9 +1166,9 @@ namespace Discord.WebSocket after = SocketMessage.Create(this, State, author, channel, data); } - var cacheableBefore = new Cacheable(before, data.Id, isCached , async () => await channel.GetMessageAsync(data.Id)); + var cacheableBefore = new Cacheable(before, data.Id, isCached, async () => await channel.GetMessageAsync(data.Id)); - await TimedInvokeAsync(_messageUpdatedEvent, nameof(MessageUpdated), cacheableBefore, after, channel).ConfigureAwait(false); + await TimedInvokeAsync(_messageUpdatedEvent, nameof(MessageUpdated), cacheableBefore, after, channel).ConfigureAwait(false); } else { @@ -1190,8 +1182,7 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_DELETE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var channel = State.GetChannel(data.ChannelId) as ISocketMessageChannel; - if (channel != null) + if (State.GetChannel(data.ChannelId) is ISocketMessageChannel channel) { var guild = (channel as SocketGuildChannel)?.Guild; if (!(guild?.IsSynced ?? true)) @@ -1218,8 +1209,7 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_REACTION_ADD)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var channel = State.GetChannel(data.ChannelId) as ISocketMessageChannel; - if (channel != null) + if (State.GetChannel(data.ChannelId) is ISocketMessageChannel channel) { SocketUserMessage cachedMsg = channel.GetCachedMessage(data.MessageId) as SocketUserMessage; bool isCached = cachedMsg != null; @@ -1243,8 +1233,7 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_REACTION_REMOVE)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var channel = State.GetChannel(data.ChannelId) as ISocketMessageChannel; - if (channel != null) + if (State.GetChannel(data.ChannelId) is ISocketMessageChannel channel) { SocketUserMessage cachedMsg = channel.GetCachedMessage(data.MessageId) as SocketUserMessage; bool isCached = cachedMsg != null; @@ -1268,8 +1257,7 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_REACTION_REMOVE_ALL)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var channel = State.GetChannel(data.ChannelId) as ISocketMessageChannel; - if (channel != null) + if (State.GetChannel(data.ChannelId) is ISocketMessageChannel channel) { SocketUserMessage cachedMsg = channel.GetCachedMessage(data.MessageId) as SocketUserMessage; bool isCached = cachedMsg != null; @@ -1291,8 +1279,7 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_DELETE_BULK)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var channel = State.GetChannel(data.ChannelId) as ISocketMessageChannel; - if (channel != null) + if (State.GetChannel(data.ChannelId) is ISocketMessageChannel channel) { var guild = (channel as SocketGuildChannel)?.Guild; if (!(guild?.IsSynced ?? true)) @@ -1376,8 +1363,7 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Received Dispatch (TYPING_START)").ConfigureAwait(false); var data = (payload as JToken).ToObject(_serializer); - var channel = State.GetChannel(data.ChannelId) as ISocketMessageChannel; - if (channel != null) + if (State.GetChannel(data.ChannelId) is ISocketMessageChannel channel) { var guild = (channel as SocketGuildChannel)?.Guild; if (!(guild?.IsSynced ?? true)) diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs index ff7048848..bb4ed7344 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs @@ -120,15 +120,13 @@ namespace Discord.WebSocket //Users public new SocketGroupUser GetUser(ulong id) { - SocketGroupUser user; - if (_users.TryGetValue(id, out user)) + if (_users.TryGetValue(id, out SocketGroupUser user)) return user; return null; } internal SocketGroupUser GetOrAddUser(UserModel model) { - SocketGroupUser user; - if (_users.TryGetValue(model.Id, out user)) + if (_users.TryGetValue(model.Id, out SocketGroupUser user)) return user as SocketGroupUser; else { @@ -139,8 +137,7 @@ namespace Discord.WebSocket } internal SocketGroupUser RemoveUser(ulong id) { - SocketGroupUser user; - if (_users.TryRemove(id, out user)) + if (_users.TryRemove(id, out SocketGroupUser user)) { user.GlobalUser.RemoveRef(Discord); return user as SocketGroupUser; @@ -158,15 +155,13 @@ namespace Discord.WebSocket } internal SocketVoiceState? GetVoiceState(ulong id) { - SocketVoiceState voiceState; - if (_voiceStates.TryGetValue(id, out voiceState)) + if (_voiceStates.TryGetValue(id, out SocketVoiceState voiceState)) return voiceState; return null; } internal SocketVoiceState? RemoveVoiceState(ulong id) { - SocketVoiceState voiceState; - if (_voiceStates.TryRemove(id, out voiceState)) + if (_voiceStates.TryRemove(id, out SocketVoiceState voiceState)) return voiceState; return null; } diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index 889587921..6535ecf9a 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -82,16 +82,7 @@ namespace Discord.WebSocket => Channels.Select(x => x as SocketTextChannel).Where(x => x != null).ToImmutableArray(); public IReadOnlyCollection VoiceChannels => Channels.Select(x => x as SocketVoiceChannel).Where(x => x != null).ToImmutableArray(); - public SocketGuildUser CurrentUser - { - get - { - SocketGuildUser member; - if (_members.TryGetValue(Discord.CurrentUser.Id, out member)) - return member; - return null; - } - } + public SocketGuildUser CurrentUser => _members.TryGetValue(Discord.CurrentUser.Id, out SocketGuildUser member) ? member : null; public SocketRole EveryoneRole => GetRole(Id); public IReadOnlyCollection Channels { @@ -162,8 +153,7 @@ namespace Discord.WebSocket for (int i = 0; i < model.Presences.Length; i++) { - SocketGuildUser member; - if (members.TryGetValue(model.Presences[i].User.Id, out member)) + if (members.TryGetValue(model.Presences[i].User.Id, out SocketGuildUser member)) member.Update(state, model.Presences[i], true); else Debug.Assert(false); @@ -248,8 +238,7 @@ namespace Discord.WebSocket for (int i = 0; i < model.Presences.Length; i++) { - SocketGuildUser member; - if (members.TryGetValue(model.Presences[i].User.Id, out member)) + if (members.TryGetValue(model.Presences[i].User.Id, out SocketGuildUser member)) member.Update(state, model.Presences[i], true); else Debug.Assert(false); @@ -343,8 +332,7 @@ namespace Discord.WebSocket //Roles public SocketRole GetRole(ulong id) { - SocketRole value; - if (_roles.TryGetValue(id, out value)) + if (_roles.TryGetValue(id, out SocketRole value)) return value; return null; } @@ -359,8 +347,7 @@ namespace Discord.WebSocket } internal SocketRole RemoveRole(ulong id) { - SocketRole role; - if (_roles.TryRemove(id, out role)) + if (_roles.TryRemove(id, out SocketRole role)) return role; return null; } @@ -368,8 +355,7 @@ namespace Discord.WebSocket //Users public SocketGuildUser GetUser(ulong id) { - SocketGuildUser member; - if (_members.TryGetValue(id, out member)) + if (_members.TryGetValue(id, out SocketGuildUser member)) return member; return null; } @@ -378,8 +364,7 @@ namespace Discord.WebSocket internal SocketGuildUser AddOrUpdateUser(UserModel model) { - SocketGuildUser member; - if (_members.TryGetValue(model.Id, out member)) + if (_members.TryGetValue(model.Id, out SocketGuildUser member)) member.GlobalUser?.Update(Discord.State, model); else { @@ -391,8 +376,7 @@ namespace Discord.WebSocket } internal SocketGuildUser AddOrUpdateUser(MemberModel model) { - SocketGuildUser member; - if (_members.TryGetValue(model.User.Id, out member)) + if (_members.TryGetValue(model.User.Id, out SocketGuildUser member)) member.Update(Discord.State, model); else { @@ -404,8 +388,7 @@ namespace Discord.WebSocket } internal SocketGuildUser AddOrUpdateUser(PresenceModel model) { - SocketGuildUser member; - if (_members.TryGetValue(model.User.Id, out member)) + if (_members.TryGetValue(model.User.Id, out SocketGuildUser member)) member.Update(Discord.State, model, false); else { @@ -417,8 +400,7 @@ namespace Discord.WebSocket } internal SocketGuildUser RemoveUser(ulong id) { - SocketGuildUser member; - if (_members.TryRemove(id, out member)) + if (_members.TryRemove(id, out SocketGuildUser member)) { DownloadedMemberCount--; member.GlobalUser.RemoveRef(Discord); @@ -466,15 +448,13 @@ namespace Discord.WebSocket } internal SocketVoiceState? GetVoiceState(ulong id) { - SocketVoiceState voiceState; - if (_voiceStates.TryGetValue(id, out voiceState)) + if (_voiceStates.TryGetValue(id, out SocketVoiceState voiceState)) return voiceState; return null; } internal async Task RemoveVoiceStateAsync(ulong id) { - SocketVoiceState voiceState; - if (_voiceStates.TryRemove(id, out voiceState)) + if (_voiceStates.TryRemove(id, out SocketVoiceState voiceState)) { if (_audioClient != null) await _audioClient.RemoveInputStreamAsync(id).ConfigureAwait(false); //User changed channels, end their stream diff --git a/src/Discord.Net.WebSocket/Entities/Messages/MessageCache.cs b/src/Discord.Net.WebSocket/Entities/Messages/MessageCache.cs index 7b8d9c2cd..c2cad4d86 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/MessageCache.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/MessageCache.cs @@ -27,24 +27,20 @@ namespace Discord.WebSocket { _orderedMessages.Enqueue(message.Id); - ulong msgId; - SocketMessage msg; - while (_orderedMessages.Count > _size && _orderedMessages.TryDequeue(out msgId)) - _messages.TryRemove(msgId, out msg); + while (_orderedMessages.Count > _size && _orderedMessages.TryDequeue(out ulong msgId)) + _messages.TryRemove(msgId, out SocketMessage msg); } } public SocketMessage Remove(ulong id) { - SocketMessage msg; - _messages.TryRemove(id, out msg); + _messages.TryRemove(id, out SocketMessage msg); return msg; } public SocketMessage Get(ulong id) { - SocketMessage result; - if (_messages.TryGetValue(id, out result)) + if (_messages.TryGetValue(id, out SocketMessage result)) return result; return null; } @@ -67,8 +63,7 @@ namespace Discord.WebSocket return cachedMessageIds .Select(x => { - SocketMessage msg; - if (_messages.TryGetValue(x, out msg)) + if (_messages.TryGetValue(x, out SocketMessage msg)) return msg; return null; }) diff --git a/src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs b/src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs index 6f667ae41..92175a917 100644 --- a/src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs +++ b/src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs @@ -206,8 +206,7 @@ namespace Discord.Net.WebSockets //Use the internal buffer if we can get it resultCount = (int)stream.Length; - ArraySegment streamBuffer; - if (stream.TryGetBuffer(out streamBuffer)) + if (stream.TryGetBuffer(out ArraySegment streamBuffer)) result = streamBuffer.Array; else result = stream.ToArray(); From f759f942f8cd413fe4996fd957d82c01b7b854bf Mon Sep 17 00:00:00 2001 From: Christopher F Date: Mon, 24 Apr 2017 20:34:18 -0400 Subject: [PATCH 145/243] Throw a preemptive exception when sending presence data before connect This prevents a later, less detailed nullref, when attempting to set the CurrentUser's presence data. This also removes a redundant CurrentUser assignment in the SetGameAsync method, since this will be set later on in the SendStatusAsync method. --- src/Discord.Net.WebSocket/DiscordSocketClient.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index 4d79ff964..e0bf08e71 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -355,11 +355,12 @@ namespace Discord.WebSocket Game = new Game(name, streamUrl, streamType); else Game = null; - CurrentUser.Presence = new SocketPresence(Status, Game); await SendStatusAsync().ConfigureAwait(false); } private async Task SendStatusAsync() { + if (ConnectionState != ConnectionState.Connected) + throw new InvalidOperationException("Presence data cannot be sent while the client is disconnected."); var game = Game; var status = Status; var statusSince = _statusSince; From be6abe1161bcb9c2a7d25f8b3636ca600661fdb9 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Mon, 24 Apr 2017 21:53:41 -0400 Subject: [PATCH 146/243] Throw when the client isn't logged in instead of connected The previous commit prevents any connections, since the initial presence update is sent while the client is still in the 'connecting' state, rather than the 'connected' state. This resolves the original issue by preventing a nullref, and the more recent issue by only throwing a detailed exception when the CurrentUser is null (the client isn't logged in). --- src/Discord.Net.WebSocket/DiscordSocketClient.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index e0bf08e71..a8543f108 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -359,8 +359,8 @@ namespace Discord.WebSocket } private async Task SendStatusAsync() { - if (ConnectionState != ConnectionState.Connected) - throw new InvalidOperationException("Presence data cannot be sent while the client is disconnected."); + if (CurrentUser == null) + throw new InvalidOperationException("Presence data cannot be sent before the client has logged in."); var game = Game; var status = Status; var statusSince = _statusSince; From 41d97884116380c47fb47727c712ce97700aac7a Mon Sep 17 00:00:00 2001 From: RogueException Date: Tue, 25 Apr 2017 09:25:19 -0300 Subject: [PATCH 147/243] Isolated Analyzers and Relay projects --- Discord.Net.sln | 15 --------------- .../Discord.Net.Analyzers/AssemblyInfo.cs | 0 .../ConfigureAwaitAnalyzer.cs | 0 .../Discord.Net.Analyzers.csproj | 0 .../ApplicationBuilderExtensions.cs | 0 .../Discord.Net.Relay/AssemblyInfo.cs | 0 .../Discord.Net.Relay/Discord.Net.Relay.csproj | 0 .../Discord.Net.Relay/RelayConnection.cs | 0 .../Discord.Net.Relay/RelayServer.cs | 0 9 files changed, 15 deletions(-) rename {src => experiment}/Discord.Net.Analyzers/AssemblyInfo.cs (100%) rename {src => experiment}/Discord.Net.Analyzers/ConfigureAwaitAnalyzer.cs (100%) rename {src => experiment}/Discord.Net.Analyzers/Discord.Net.Analyzers.csproj (100%) rename {src => experiment}/Discord.Net.Relay/ApplicationBuilderExtensions.cs (100%) rename {src => experiment}/Discord.Net.Relay/AssemblyInfo.cs (100%) rename {src => experiment}/Discord.Net.Relay/Discord.Net.Relay.csproj (100%) rename {src => experiment}/Discord.Net.Relay/RelayConnection.cs (100%) rename {src => experiment}/Discord.Net.Relay/RelayServer.cs (100%) diff --git a/Discord.Net.sln b/Discord.Net.sln index 1c8347293..4a7b07577 100644 --- a/Discord.Net.sln +++ b/Discord.Net.sln @@ -24,8 +24,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Extensions", "Extensions", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Tests", "test\Discord.Net.Tests\Discord.Net.Tests.csproj", "{C38E5BC1-11CB-4101-8A38-5B40A1BC6433}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Relay", "src\Discord.Net.Relay\Discord.Net.Relay.csproj", "{2705FCB3-68C9-4CEB-89CC-01F8EC80512B}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Webhook", "src\Discord.Net.Webhook\Discord.Net.Webhook.csproj", "{9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}" EndProject Global @@ -134,18 +132,6 @@ Global {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Release|x64.Build.0 = Release|Any CPU {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Release|x86.ActiveCfg = Release|Any CPU {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Release|x86.Build.0 = Release|Any CPU - {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Debug|x64.ActiveCfg = Debug|Any CPU - {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Debug|x64.Build.0 = Debug|Any CPU - {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Debug|x86.ActiveCfg = Debug|Any CPU - {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Debug|x86.Build.0 = Debug|Any CPU - {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Release|Any CPU.Build.0 = Release|Any CPU - {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Release|x64.ActiveCfg = Release|Any CPU - {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Release|x64.Build.0 = Release|Any CPU - {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Release|x86.ActiveCfg = Release|Any CPU - {2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Release|x86.Build.0 = Release|Any CPU {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Debug|Any CPU.Build.0 = Debug|Any CPU {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -169,7 +155,6 @@ Global {688FD1D8-7F01-4539-B2E9-F473C5D699C7} = {288C363D-A636-4EAE-9AC1-4698B641B26E} {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7} = {B0657AAE-DCC5-4FBF-8E5D-1FB578CF3012} {ABC9F4B9-2452-4725-B522-754E0A02E282} = {B0657AAE-DCC5-4FBF-8E5D-1FB578CF3012} - {2705FCB3-68C9-4CEB-89CC-01F8EC80512B} = {CC3D4B1C-9DE0-448B-8AE7-F3F1F3EC5C3A} {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30} = {CC3D4B1C-9DE0-448B-8AE7-F3F1F3EC5C3A} EndGlobalSection EndGlobal diff --git a/src/Discord.Net.Analyzers/AssemblyInfo.cs b/experiment/Discord.Net.Analyzers/AssemblyInfo.cs similarity index 100% rename from src/Discord.Net.Analyzers/AssemblyInfo.cs rename to experiment/Discord.Net.Analyzers/AssemblyInfo.cs diff --git a/src/Discord.Net.Analyzers/ConfigureAwaitAnalyzer.cs b/experiment/Discord.Net.Analyzers/ConfigureAwaitAnalyzer.cs similarity index 100% rename from src/Discord.Net.Analyzers/ConfigureAwaitAnalyzer.cs rename to experiment/Discord.Net.Analyzers/ConfigureAwaitAnalyzer.cs diff --git a/src/Discord.Net.Analyzers/Discord.Net.Analyzers.csproj b/experiment/Discord.Net.Analyzers/Discord.Net.Analyzers.csproj similarity index 100% rename from src/Discord.Net.Analyzers/Discord.Net.Analyzers.csproj rename to experiment/Discord.Net.Analyzers/Discord.Net.Analyzers.csproj diff --git a/src/Discord.Net.Relay/ApplicationBuilderExtensions.cs b/experiment/Discord.Net.Relay/ApplicationBuilderExtensions.cs similarity index 100% rename from src/Discord.Net.Relay/ApplicationBuilderExtensions.cs rename to experiment/Discord.Net.Relay/ApplicationBuilderExtensions.cs diff --git a/src/Discord.Net.Relay/AssemblyInfo.cs b/experiment/Discord.Net.Relay/AssemblyInfo.cs similarity index 100% rename from src/Discord.Net.Relay/AssemblyInfo.cs rename to experiment/Discord.Net.Relay/AssemblyInfo.cs diff --git a/src/Discord.Net.Relay/Discord.Net.Relay.csproj b/experiment/Discord.Net.Relay/Discord.Net.Relay.csproj similarity index 100% rename from src/Discord.Net.Relay/Discord.Net.Relay.csproj rename to experiment/Discord.Net.Relay/Discord.Net.Relay.csproj diff --git a/src/Discord.Net.Relay/RelayConnection.cs b/experiment/Discord.Net.Relay/RelayConnection.cs similarity index 100% rename from src/Discord.Net.Relay/RelayConnection.cs rename to experiment/Discord.Net.Relay/RelayConnection.cs diff --git a/src/Discord.Net.Relay/RelayServer.cs b/experiment/Discord.Net.Relay/RelayServer.cs similarity index 100% rename from src/Discord.Net.Relay/RelayServer.cs rename to experiment/Discord.Net.Relay/RelayServer.cs From 582b8f9637a8f0578a45cce8d87849c98a9e8dd8 Mon Sep 17 00:00:00 2001 From: RogueException Date: Wed, 26 Apr 2017 09:45:19 -0300 Subject: [PATCH 148/243] Added ChannelName/GuildName to IInvite --- src/Discord.Net.Core/Entities/Invites/IInvite.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Discord.Net.Core/Entities/Invites/IInvite.cs b/src/Discord.Net.Core/Entities/Invites/IInvite.cs index a023749c2..73555e453 100644 --- a/src/Discord.Net.Core/Entities/Invites/IInvite.cs +++ b/src/Discord.Net.Core/Entities/Invites/IInvite.cs @@ -13,10 +13,15 @@ namespace Discord IChannel Channel { get; } /// Gets the id of the channel this invite is linked to. ulong ChannelId { get; } + /// Gets the name of the channel this invite is linked to. + string ChannelName { get; } + /// Gets the guild this invite is linked to. IGuild Guild { get; } /// Gets the id of the guild this invite is linked to. ulong GuildId { get; } + /// Gets the name of the guild this invite is linked to. + string GuildName { get; } /// Accepts this invite and joins the target guild. This will fail on bot accounts. Task AcceptAsync(RequestOptions options = null); From f8b9acc4a1adcc34021ee435431f7367d834c864 Mon Sep 17 00:00:00 2001 From: RogueException Date: Wed, 26 Apr 2017 10:03:23 -0300 Subject: [PATCH 149/243] Use implicit package references (#626) --- Discord.Net.targets | 1 - src/Discord.Net.Commands/Discord.Net.Commands.csproj | 2 +- src/Discord.Net.Core/Discord.Net.Core.csproj | 8 -------- src/Discord.Net.DebugTools/Discord.Net.DebugTools.csproj | 5 ----- src/Discord.Net.Rest/Discord.Net.Rest.csproj | 3 --- src/Discord.Net.Rpc/Discord.Net.Rpc.csproj | 3 --- src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj | 5 ----- 7 files changed, 1 insertion(+), 26 deletions(-) diff --git a/Discord.Net.targets b/Discord.Net.targets index a4558b069..9dee9910e 100644 --- a/Discord.Net.targets +++ b/Discord.Net.targets @@ -8,7 +8,6 @@ http://opensource.org/licenses/MIT git git://github.com/RogueException/Discord.Net - true
dev diff --git a/src/Discord.Net.Commands/Discord.Net.Commands.csproj b/src/Discord.Net.Commands/Discord.Net.Commands.csproj index 05853109a..40f130d7b 100644 --- a/src/Discord.Net.Commands/Discord.Net.Commands.csproj +++ b/src/Discord.Net.Commands/Discord.Net.Commands.csproj @@ -4,7 +4,7 @@ Discord.Net.Commands Discord.Commands A Discord.Net extension adding support for bot commands. - netstandard1.1;netstandard1.3 + netstandard1.1 diff --git a/src/Discord.Net.Core/Discord.Net.Core.csproj b/src/Discord.Net.Core/Discord.Net.Core.csproj index b41203a74..d0e9e999d 100644 --- a/src/Discord.Net.Core/Discord.Net.Core.csproj +++ b/src/Discord.Net.Core/Discord.Net.Core.csproj @@ -8,15 +8,7 @@ - - - - - - - -
\ No newline at end of file diff --git a/src/Discord.Net.DebugTools/Discord.Net.DebugTools.csproj b/src/Discord.Net.DebugTools/Discord.Net.DebugTools.csproj index 4d529c070..05230433d 100644 --- a/src/Discord.Net.DebugTools/Discord.Net.DebugTools.csproj +++ b/src/Discord.Net.DebugTools/Discord.Net.DebugTools.csproj @@ -9,9 +9,4 @@ - - - - - \ No newline at end of file diff --git a/src/Discord.Net.Rest/Discord.Net.Rest.csproj b/src/Discord.Net.Rest/Discord.Net.Rest.csproj index 42583abf1..79fe14c09 100644 --- a/src/Discord.Net.Rest/Discord.Net.Rest.csproj +++ b/src/Discord.Net.Rest/Discord.Net.Rest.csproj @@ -9,7 +9,4 @@ - - - \ No newline at end of file diff --git a/src/Discord.Net.Rpc/Discord.Net.Rpc.csproj b/src/Discord.Net.Rpc/Discord.Net.Rpc.csproj index 22f932638..f3adeff6a 100644 --- a/src/Discord.Net.Rpc/Discord.Net.Rpc.csproj +++ b/src/Discord.Net.Rpc/Discord.Net.Rpc.csproj @@ -18,9 +18,6 @@
- - - diff --git a/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj b/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj index 4e252795b..37fbc1edd 100644 --- a/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj +++ b/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj @@ -11,12 +11,7 @@ - - - - - \ No newline at end of file From a306d83283d06530d2c8468737c7026d0912c273 Mon Sep 17 00:00:00 2001 From: RogueException Date: Wed, 26 Apr 2017 10:25:08 -0300 Subject: [PATCH 150/243] Added net45 TFM --- Discord.Net.targets | 6 ++++++ src/Discord.Net.Core/Discord.Net.Core.csproj | 2 +- .../Entities/Channels/IMessageChannel.cs | 2 +- src/Discord.Net.Core/Entities/Image.cs | 2 +- src/Discord.Net.Core/Logging/LogManager.cs | 12 ++++++------ src/Discord.Net.Core/Logging/Logger.cs | 12 ++++++------ src/Discord.Net.Core/Utils/DateTimeUtils.cs | 10 +++++----- .../Discord.Net.DebugTools.csproj | 2 +- .../UnstableWebSocketClient.cs | 7 +++++-- src/Discord.Net.Rest/Discord.Net.Rest.csproj | 2 +- .../Entities/Channels/ChannelHelper.cs | 2 +- .../Entities/Channels/IRestMessageChannel.cs | 2 +- .../Entities/Channels/RestDMChannel.cs | 4 ++-- .../Entities/Channels/RestGroupChannel.cs | 4 ++-- .../Entities/Channels/RestTextChannel.cs | 4 ++-- .../Entities/Channels/RpcVirtualMessageChannel.cs | 4 ++-- src/Discord.Net.Rpc/Discord.Net.Rpc.csproj | 2 +- src/Discord.Net.Rpc/DiscordRpcConfig.cs | 2 +- .../Entities/Channels/RpcDMChannel.cs | 4 ++-- .../Entities/Channels/RpcGroupChannel.cs | 4 ++-- .../Entities/Channels/RpcTextChannel.cs | 4 ++-- .../Discord.Net.WebSocket.csproj | 2 +- .../Entities/Channels/ISocketMessageChannel.cs | 2 +- .../Entities/Channels/SocketDMChannel.cs | 4 ++-- .../Entities/Channels/SocketGroupChannel.cs | 4 ++-- .../Entities/Channels/SocketTextChannel.cs | 4 ++-- src/Discord.Net.WebSocket/Net/DefaultUdpSocket.cs | 6 +++++- .../Net/DefaultUdpSocketProvider.cs | 2 +- .../Net/DefaultWebSocketClient.cs | 8 ++++++-- .../Net/DefaultWebSocketClientProvider.cs | 2 +- src/Discord.Net.Webhook/Discord.Net.Webhook.csproj | 2 +- src/Discord.Net.Webhook/DiscordWebhookClient.cs | 2 +- src/Discord.Net/Discord.Net.nuspec | 8 ++++++++ 33 files changed, 82 insertions(+), 57 deletions(-) diff --git a/Discord.Net.targets b/Discord.Net.targets index 9dee9910e..669db428d 100644 --- a/Discord.Net.targets +++ b/Discord.Net.targets @@ -17,6 +17,12 @@ $(BuildNumber) $(VersionSuffix)-$(BuildNumber) + + $(DefineConstants);FILESYSTEM;DEFAULTUDPCLIENT;DEFAULTWEBSOCKET + + + $(DefineConstants);FORMATSTR;UNIXTIME;MSTRYBUFFER;UDPDISPOSE + $(NoWarn);CS1573;CS1591 true diff --git a/src/Discord.Net.Core/Discord.Net.Core.csproj b/src/Discord.Net.Core/Discord.Net.Core.csproj index d0e9e999d..4f20dad3b 100644 --- a/src/Discord.Net.Core/Discord.Net.Core.csproj +++ b/src/Discord.Net.Core/Discord.Net.Core.csproj @@ -4,7 +4,7 @@ Discord.Net.Core Discord The core components for the Discord.Net library. - netstandard1.1;netstandard1.3 + net45;netstandard1.1;netstandard1.3 diff --git a/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs b/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs index 9c9c63929..7fce1e855 100644 --- a/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs @@ -9,7 +9,7 @@ namespace Discord { /// Sends a message to this message channel. Task SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null); -#if NETSTANDARD1_3 +#if FILESYSTEM /// Sends a file to this text channel, with an optional caption. Task SendFileAsync(string filePath, string text = null, bool isTTS = false, RequestOptions options = null); #endif diff --git a/src/Discord.Net.Core/Entities/Image.cs b/src/Discord.Net.Core/Entities/Image.cs index 59fe8bbdb..c2c997365 100644 --- a/src/Discord.Net.Core/Entities/Image.cs +++ b/src/Discord.Net.Core/Entities/Image.cs @@ -15,7 +15,7 @@ namespace Discord { Stream = stream; } -#if NETSTANDARD1_3 +#if FILESYSTEM /// /// Create the image from a file path. /// diff --git a/src/Discord.Net.Core/Logging/LogManager.cs b/src/Discord.Net.Core/Logging/LogManager.cs index 6f87d1229..995a5d96a 100644 --- a/src/Discord.Net.Core/Logging/LogManager.cs +++ b/src/Discord.Net.Core/Logging/LogManager.cs @@ -35,7 +35,7 @@ namespace Discord.Logging } catch { } } -#if NETSTANDARD1_3 +#if FORMATSTR public async Task LogAsync(LogSeverity severity, string source, FormattableString message, Exception ex = null) { try @@ -51,7 +51,7 @@ namespace Discord.Logging => LogAsync(LogSeverity.Error, source, ex); public Task ErrorAsync(string source, string message, Exception ex = null) => LogAsync(LogSeverity.Error, source, message, ex); -#if NETSTANDARD1_3 +#if FORMATSTR public Task ErrorAsync(string source, FormattableString message, Exception ex = null) => LogAsync(LogSeverity.Error, source, message, ex); #endif @@ -60,7 +60,7 @@ namespace Discord.Logging => LogAsync(LogSeverity.Warning, source, ex); public Task WarningAsync(string source, string message, Exception ex = null) => LogAsync(LogSeverity.Warning, source, message, ex); -#if NETSTANDARD1_3 +#if FORMATSTR public Task WarningAsync(string source, FormattableString message, Exception ex = null) => LogAsync(LogSeverity.Warning, source, message, ex); #endif @@ -69,7 +69,7 @@ namespace Discord.Logging => LogAsync(LogSeverity.Info, source, ex); public Task InfoAsync(string source, string message, Exception ex = null) => LogAsync(LogSeverity.Info, source, message, ex); -#if NETSTANDARD1_3 +#if FORMATSTR public Task InfoAsync(string source, FormattableString message, Exception ex = null) => LogAsync(LogSeverity.Info, source, message, ex); #endif @@ -78,7 +78,7 @@ namespace Discord.Logging => LogAsync(LogSeverity.Verbose, source, ex); public Task VerboseAsync(string source, string message, Exception ex = null) => LogAsync(LogSeverity.Verbose, source, message, ex); -#if NETSTANDARD1_3 +#if FORMATSTR public Task VerboseAsync(string source, FormattableString message, Exception ex = null) => LogAsync(LogSeverity.Verbose, source, message, ex); #endif @@ -87,7 +87,7 @@ namespace Discord.Logging => LogAsync(LogSeverity.Debug, source, ex); public Task DebugAsync(string source, string message, Exception ex = null) => LogAsync(LogSeverity.Debug, source, message, ex); -#if NETSTANDARD1_3 +#if FORMATSTR public Task DebugAsync(string source, FormattableString message, Exception ex = null) => LogAsync(LogSeverity.Debug, source, message, ex); #endif diff --git a/src/Discord.Net.Core/Logging/Logger.cs b/src/Discord.Net.Core/Logging/Logger.cs index cff69a84c..a8d88b2b4 100644 --- a/src/Discord.Net.Core/Logging/Logger.cs +++ b/src/Discord.Net.Core/Logging/Logger.cs @@ -20,7 +20,7 @@ namespace Discord.Logging => _manager.LogAsync(severity, Name, exception); public Task LogAsync(LogSeverity severity, string message, Exception exception = null) => _manager.LogAsync(severity, Name, message, exception); -#if NETSTANDARD1_3 +#if FORMATSTR public Task LogAsync(LogSeverity severity, FormattableString message, Exception exception = null) => _manager.LogAsync(severity, Name, message, exception); #endif @@ -29,7 +29,7 @@ namespace Discord.Logging => _manager.ErrorAsync(Name, exception); public Task ErrorAsync(string message, Exception exception = null) => _manager.ErrorAsync(Name, message, exception); -#if NETSTANDARD1_3 +#if FORMATSTR public Task ErrorAsync(FormattableString message, Exception exception = null) => _manager.ErrorAsync(Name, message, exception); #endif @@ -38,7 +38,7 @@ namespace Discord.Logging => _manager.WarningAsync(Name, exception); public Task WarningAsync(string message, Exception exception = null) => _manager.WarningAsync(Name, message, exception); -#if NETSTANDARD1_3 +#if FORMATSTR public Task WarningAsync(FormattableString message, Exception exception = null) => _manager.WarningAsync(Name, message, exception); #endif @@ -47,7 +47,7 @@ namespace Discord.Logging => _manager.InfoAsync(Name, exception); public Task InfoAsync(string message, Exception exception = null) => _manager.InfoAsync(Name, message, exception); -#if NETSTANDARD1_3 +#if FORMATSTR public Task InfoAsync(FormattableString message, Exception exception = null) => _manager.InfoAsync(Name, message, exception); #endif @@ -56,7 +56,7 @@ namespace Discord.Logging => _manager.VerboseAsync(Name, exception); public Task VerboseAsync(string message, Exception exception = null) => _manager.VerboseAsync(Name, message, exception); -#if NETSTANDARD1_3 +#if FORMATSTR public Task VerboseAsync(FormattableString message, Exception exception = null) => _manager.VerboseAsync(Name, message, exception); #endif @@ -65,7 +65,7 @@ namespace Discord.Logging => _manager.DebugAsync(Name, exception); public Task DebugAsync(string message, Exception exception = null) => _manager.DebugAsync(Name, message, exception); -#if NETSTANDARD1_3 +#if FORMATSTR public Task DebugAsync(FormattableString message, Exception exception = null) => _manager.DebugAsync(Name, message, exception); #endif diff --git a/src/Discord.Net.Core/Utils/DateTimeUtils.cs b/src/Discord.Net.Core/Utils/DateTimeUtils.cs index 109c8c791..af2126853 100644 --- a/src/Discord.Net.Core/Utils/DateTimeUtils.cs +++ b/src/Discord.Net.Core/Utils/DateTimeUtils.cs @@ -5,7 +5,7 @@ namespace Discord //Source: https://github.com/dotnet/coreclr/blob/master/src/mscorlib/src/System/DateTimeOffset.cs internal static class DateTimeUtils { -#if !NETSTANDARD1_3 +#if !UNIXTIME private const long UnixEpochTicks = 621_355_968_000_000_000; private const long UnixEpochSeconds = 62_135_596_800; private const long UnixEpochMilliseconds = 62_135_596_800_000; @@ -18,7 +18,7 @@ namespace Discord public static DateTimeOffset FromUnixSeconds(long seconds) { -#if NETSTANDARD1_3 +#if UNIXTIME return DateTimeOffset.FromUnixTimeSeconds(seconds); #else long ticks = seconds * TimeSpan.TicksPerSecond + UnixEpochTicks; @@ -27,7 +27,7 @@ namespace Discord } public static DateTimeOffset FromUnixMilliseconds(long milliseconds) { -#if NETSTANDARD1_3 +#if UNIXTIME return DateTimeOffset.FromUnixTimeMilliseconds(milliseconds); #else long ticks = milliseconds * TimeSpan.TicksPerMillisecond + UnixEpochTicks; @@ -37,7 +37,7 @@ namespace Discord public static long ToUnixSeconds(DateTimeOffset dto) { -#if NETSTANDARD1_3 +#if UNIXTIME return dto.ToUnixTimeSeconds(); #else long seconds = dto.UtcDateTime.Ticks / TimeSpan.TicksPerSecond; @@ -46,7 +46,7 @@ namespace Discord } public static long ToUnixMilliseconds(DateTimeOffset dto) { -#if NETSTANDARD1_3 +#if UNIXTIME return dto.ToUnixTimeMilliseconds(); #else long milliseconds = dto.UtcDateTime.Ticks / TimeSpan.TicksPerMillisecond; diff --git a/src/Discord.Net.DebugTools/Discord.Net.DebugTools.csproj b/src/Discord.Net.DebugTools/Discord.Net.DebugTools.csproj index 05230433d..1a0d08c4d 100644 --- a/src/Discord.Net.DebugTools/Discord.Net.DebugTools.csproj +++ b/src/Discord.Net.DebugTools/Discord.Net.DebugTools.csproj @@ -4,7 +4,7 @@ Discord.Net.DebugTools Discord A Discord.Net extension adding some helper classes for diagnosing issues. - netstandard1.6 + net45;netstandard1.3 diff --git a/src/Discord.Net.DebugTools/UnstableWebSocketClient.cs b/src/Discord.Net.DebugTools/UnstableWebSocketClient.cs index a0f28ba0a..4f45503c9 100644 --- a/src/Discord.Net.DebugTools/UnstableWebSocketClient.cs +++ b/src/Discord.Net.DebugTools/UnstableWebSocketClient.cs @@ -213,11 +213,14 @@ namespace Discord.Net.Providers.UnstableWebSocket //Use the internal buffer if we can get it resultCount = (int)stream.Length; - ArraySegment streamBuffer; - if (stream.TryGetBuffer(out streamBuffer)) +#if MSTRYBUFFER + if (stream.TryGetBuffer(out var streamBuffer)) result = streamBuffer.Array; else result = stream.ToArray(); +#else + result = stream.GetBuffer(); +#endif } } else diff --git a/src/Discord.Net.Rest/Discord.Net.Rest.csproj b/src/Discord.Net.Rest/Discord.Net.Rest.csproj index 79fe14c09..c8da70c6b 100644 --- a/src/Discord.Net.Rest/Discord.Net.Rest.csproj +++ b/src/Discord.Net.Rest/Discord.Net.Rest.csproj @@ -4,7 +4,7 @@ Discord.Net.Rest Discord.Rest A core Discord.Net library containing the REST client and models. - netstandard1.1;netstandard1.3 + net45;netstandard1.1;netstandard1.3 diff --git a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs index 6f0aa67de..e6d017a89 100644 --- a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs +++ b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs @@ -163,7 +163,7 @@ namespace Discord.Rest return RestUserMessage.Create(client, channel, client.CurrentUser, model); } -#if NETSTANDARD1_3 +#if FILESYSTEM public static async Task SendFileAsync(IMessageChannel channel, BaseDiscordClient client, string filePath, string text, bool isTTS, RequestOptions options) { diff --git a/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs b/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs index 2c006834c..3a104dd9c 100644 --- a/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs @@ -8,7 +8,7 @@ namespace Discord.Rest { /// Sends a message to this message channel. new Task SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null); -#if NETSTANDARD1_3 +#if FILESYSTEM /// Sends a file to this text channel, with an optional caption. new Task SendFileAsync(string filePath, string text = null, bool isTTS = false, RequestOptions options = null); #endif diff --git a/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs index 75b331499..0a4bc9522 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs @@ -65,7 +65,7 @@ namespace Discord.Rest public Task SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); -#if NETSTANDARD1_3 +#if FILESYSTEM public Task SendFileAsync(string filePath, string text, bool isTTS = false, RequestOptions options = null) => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, options); #endif @@ -124,7 +124,7 @@ namespace Discord.Rest async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) => await GetPinnedMessagesAsync(options).ConfigureAwait(false); -#if NETSTANDARD1_3 +#if FILESYSTEM async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options) => await SendFileAsync(filePath, text, isTTS, options).ConfigureAwait(false); #endif diff --git a/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs index 93e24e05b..b324422a5 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs @@ -78,7 +78,7 @@ namespace Discord.Rest public Task SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); -#if NETSTANDARD1_3 +#if FILESYSTEM public Task SendFileAsync(string filePath, string text, bool isTTS = false, RequestOptions options = null) => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, options); #endif @@ -134,7 +134,7 @@ namespace Discord.Rest async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) => await GetPinnedMessagesAsync(options).ConfigureAwait(false); -#if NETSTANDARD1_3 +#if FILESYSTEM async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options) => await SendFileAsync(filePath, text, isTTS, options).ConfigureAwait(false); #endif diff --git a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs index 2687312a7..2fe9feb91 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs @@ -56,7 +56,7 @@ namespace Discord.Rest public Task SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); -#if NETSTANDARD1_3 +#if FILESYSTEM public Task SendFileAsync(string filePath, string text, bool isTTS = false, RequestOptions options = null) => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, options); #endif @@ -105,7 +105,7 @@ namespace Discord.Rest async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) => await GetPinnedMessagesAsync(options).ConfigureAwait(false); -#if NETSTANDARD1_3 +#if FILESYSTEM async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options) => await SendFileAsync(filePath, text, isTTS, options).ConfigureAwait(false); #endif diff --git a/src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs index 2c69a3395..7e515978d 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs @@ -35,7 +35,7 @@ namespace Discord.Rest public Task SendMessageAsync(string text, bool isTTS, Embed embed = null, RequestOptions options = null) => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); -#if NETSTANDARD1_3 +#if FILESYSTEM public Task SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options = null) => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, options); #endif @@ -84,7 +84,7 @@ namespace Discord.Rest async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) => await GetPinnedMessagesAsync(options); -#if NETSTANDARD1_3 +#if FILESYSTEM async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options) => await SendFileAsync(filePath, text, isTTS, options); #endif diff --git a/src/Discord.Net.Rpc/Discord.Net.Rpc.csproj b/src/Discord.Net.Rpc/Discord.Net.Rpc.csproj index f3adeff6a..3561e6da5 100644 --- a/src/Discord.Net.Rpc/Discord.Net.Rpc.csproj +++ b/src/Discord.Net.Rpc/Discord.Net.Rpc.csproj @@ -4,7 +4,7 @@ Discord.Net.Rpc Discord.Rpc A core Discord.Net library containing the RPC client and models. - netstandard1.1;netstandard1.3 + net45;netstandard1.1;netstandard1.3 diff --git a/src/Discord.Net.Rpc/DiscordRpcConfig.cs b/src/Discord.Net.Rpc/DiscordRpcConfig.cs index 1866e838b..fd6b74ff1 100644 --- a/src/Discord.Net.Rpc/DiscordRpcConfig.cs +++ b/src/Discord.Net.Rpc/DiscordRpcConfig.cs @@ -19,7 +19,7 @@ namespace Discord.Rpc public DiscordRpcConfig() { -#if NETSTANDARD1_3 +#if FILESYSTEM WebSocketProvider = () => new DefaultWebSocketClient(); #else WebSocketProvider = () => diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcDMChannel.cs b/src/Discord.Net.Rpc/Entities/Channels/RpcDMChannel.cs index 1fb6d5867..b2c3daaa2 100644 --- a/src/Discord.Net.Rpc/Entities/Channels/RpcDMChannel.cs +++ b/src/Discord.Net.Rpc/Entities/Channels/RpcDMChannel.cs @@ -46,7 +46,7 @@ namespace Discord.Rpc public Task SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); -#if NETSTANDARD1_3 +#if FILESYSTEM public Task SendFileAsync(string filePath, string text, bool isTTS = false, RequestOptions options = null) => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, options); #endif @@ -102,7 +102,7 @@ namespace Discord.Rpc async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) => await GetPinnedMessagesAsync(options).ConfigureAwait(false); -#if NETSTANDARD1_3 +#if FILESYSTEM async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options) => await SendFileAsync(filePath, text, isTTS, options).ConfigureAwait(false); #endif diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs b/src/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs index c365ad4ff..b5effacc6 100644 --- a/src/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs +++ b/src/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs @@ -49,7 +49,7 @@ namespace Discord.Rpc public Task SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); -#if NETSTANDARD1_3 +#if FILESYSTEM public Task SendFileAsync(string filePath, string text, bool isTTS = false, RequestOptions options = null) => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, options); #endif @@ -102,7 +102,7 @@ namespace Discord.Rpc async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) => await GetPinnedMessagesAsync(options).ConfigureAwait(false); -#if NETSTANDARD1_3 +#if FILESYSTEM async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options) => await SendFileAsync(filePath, text, isTTS, options).ConfigureAwait(false); #endif diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs b/src/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs index 9a88072b9..bdfafa561 100644 --- a/src/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs +++ b/src/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs @@ -50,7 +50,7 @@ namespace Discord.Rpc public Task SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); -#if NETSTANDARD1_3 +#if FILESYSTEM public Task SendFileAsync(string filePath, string text, bool isTTS = false, RequestOptions options = null) => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, options); #endif @@ -102,7 +102,7 @@ namespace Discord.Rpc async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) => await GetPinnedMessagesAsync(options).ConfigureAwait(false); -#if NETSTANDARD1_3 +#if FILESYSTEM async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options) => await SendFileAsync(filePath, text, isTTS, options).ConfigureAwait(false); #endif diff --git a/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj b/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj index 37fbc1edd..2b0dcf1fe 100644 --- a/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj +++ b/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj @@ -4,7 +4,7 @@ Discord.Net.WebSocket Discord.WebSocket A core Discord.Net library containing the WebSocket client and models. - netstandard1.1;netstandard1.3 + net45;netstandard1.1;netstandard1.3 true diff --git a/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs index 43246f5ca..e2119e7a2 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs @@ -12,7 +12,7 @@ namespace Discord.WebSocket /// Sends a message to this message channel. new Task SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null); -#if NETSTANDARD1_3 +#if FILESYSTEM /// Sends a file to this text channel, with an optional caption. new Task SendFileAsync(string filePath, string text = null, bool isTTS = false, RequestOptions options = null); #endif diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs index c976b64f8..4d79d1ab4 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs @@ -68,7 +68,7 @@ namespace Discord.WebSocket public Task SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); -#if NETSTANDARD1_3 +#if FILESYSTEM public Task SendFileAsync(string filePath, string text, bool isTTS = false, RequestOptions options = null) => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, options); #endif @@ -132,7 +132,7 @@ namespace Discord.WebSocket => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessage.Id, dir, limit, mode, options); async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) => await GetPinnedMessagesAsync(options).ConfigureAwait(false); -#if NETSTANDARD1_3 +#if FILESYSTEM async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options) => await SendFileAsync(filePath, text, isTTS, options).ConfigureAwait(false); #endif diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs index bb4ed7344..bdf9dbc2b 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs @@ -97,7 +97,7 @@ namespace Discord.WebSocket public Task SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); -#if NETSTANDARD1_3 +#if FILESYSTEM public Task SendFileAsync(string filePath, string text, bool isTTS = false, RequestOptions options = null) => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, options); #endif @@ -196,7 +196,7 @@ namespace Discord.WebSocket => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessage.Id, dir, limit, mode, options); async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) => await GetPinnedMessagesAsync(options).ConfigureAwait(false); -#if NETSTANDARD1_3 +#if FILESYSTEM async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options) => await SendFileAsync(filePath, text, isTTS, options).ConfigureAwait(false); #endif diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs index 98aefcf9b..e75b6a4f1 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs @@ -73,7 +73,7 @@ namespace Discord.WebSocket public Task SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); -#if NETSTANDARD1_3 +#if FILESYSTEM public Task SendFileAsync(string filePath, string text, bool isTTS = false, RequestOptions options = null) => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, options); #endif @@ -132,7 +132,7 @@ namespace Discord.WebSocket => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessage.Id, dir, limit, mode, options); async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) => await GetPinnedMessagesAsync(options).ConfigureAwait(false); -#if NETSTANDARD1_3 +#if FILESYSTEM async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options) => await SendFileAsync(filePath, text, isTTS, options).ConfigureAwait(false); #endif diff --git a/src/Discord.Net.WebSocket/Net/DefaultUdpSocket.cs b/src/Discord.Net.WebSocket/Net/DefaultUdpSocket.cs index 013ba62fc..6a6194397 100644 --- a/src/Discord.Net.WebSocket/Net/DefaultUdpSocket.cs +++ b/src/Discord.Net.WebSocket/Net/DefaultUdpSocket.cs @@ -1,4 +1,4 @@ -#if NETSTANDARD1_3 +#if DEFAULTUDPCLIENT using System; using System.Net; using System.Net.Sockets; @@ -85,7 +85,11 @@ namespace Discord.Net.Udp if (_udp != null) { +#if UDPDISPOSE try { _udp.Dispose(); } +#else + try { _udp.Close(); } +#endif catch { } _udp = null; } diff --git a/src/Discord.Net.WebSocket/Net/DefaultUdpSocketProvider.cs b/src/Discord.Net.WebSocket/Net/DefaultUdpSocketProvider.cs index cba4fecb0..82b6ec4c0 100644 --- a/src/Discord.Net.WebSocket/Net/DefaultUdpSocketProvider.cs +++ b/src/Discord.Net.WebSocket/Net/DefaultUdpSocketProvider.cs @@ -4,7 +4,7 @@ namespace Discord.Net.Udp { public static class DefaultUdpSocketProvider { -#if NETSTANDARD1_3 +#if DEFAULTUDPCLIENT public static readonly UdpSocketProvider Instance = () => { try diff --git a/src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs b/src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs index 92175a917..282ae210a 100644 --- a/src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs +++ b/src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs @@ -1,4 +1,4 @@ -#if NETSTANDARD1_3 +#if DEFAULTWEBSOCKET using System; using System.Collections.Generic; using System.ComponentModel; @@ -206,10 +206,14 @@ namespace Discord.Net.WebSockets //Use the internal buffer if we can get it resultCount = (int)stream.Length; - if (stream.TryGetBuffer(out ArraySegment streamBuffer)) +#if MSTRYBUFFER + if (stream.TryGetBuffer(out var streamBuffer)) result = streamBuffer.Array; else result = stream.ToArray(); +#else + result = stream.GetBuffer(); +#endif } } else diff --git a/src/Discord.Net.WebSocket/Net/DefaultWebSocketClientProvider.cs b/src/Discord.Net.WebSocket/Net/DefaultWebSocketClientProvider.cs index d93ded57d..04b3f8388 100644 --- a/src/Discord.Net.WebSocket/Net/DefaultWebSocketClientProvider.cs +++ b/src/Discord.Net.WebSocket/Net/DefaultWebSocketClientProvider.cs @@ -4,7 +4,7 @@ namespace Discord.Net.WebSockets { public static class DefaultWebSocketProvider { -#if NETSTANDARD1_3 +#if DEFAULTWEBSOCKET public static readonly WebSocketProvider Instance = () => { try diff --git a/src/Discord.Net.Webhook/Discord.Net.Webhook.csproj b/src/Discord.Net.Webhook/Discord.Net.Webhook.csproj index d5072c18a..7246624e5 100644 --- a/src/Discord.Net.Webhook/Discord.Net.Webhook.csproj +++ b/src/Discord.Net.Webhook/Discord.Net.Webhook.csproj @@ -4,7 +4,7 @@ Discord.Net.Webhook Discord.Webhook A core Discord.Net library containing the Webhook client and models. - netstandard1.1;netstandard1.3 + netstandard1.1 diff --git a/src/Discord.Net.Webhook/DiscordWebhookClient.cs b/src/Discord.Net.Webhook/DiscordWebhookClient.cs index 014f57ce0..9695099ee 100644 --- a/src/Discord.Net.Webhook/DiscordWebhookClient.cs +++ b/src/Discord.Net.Webhook/DiscordWebhookClient.cs @@ -59,7 +59,7 @@ namespace Discord.Webhook await ApiClient.CreateWebhookMessageAsync(_webhookId, args, options).ConfigureAwait(false); } -#if NETSTANDARD1_3 +#if FILESYSTEM public async Task SendFileAsync(string filePath, string text, bool isTTS = false, string username = null, string avatarUrl = null, RequestOptions options = null) { diff --git a/src/Discord.Net/Discord.Net.nuspec b/src/Discord.Net/Discord.Net.nuspec index 2ce2c6c0f..a410294b1 100644 --- a/src/Discord.Net/Discord.Net.nuspec +++ b/src/Discord.Net/Discord.Net.nuspec @@ -12,6 +12,14 @@ http://opensource.org/licenses/MIT false + + + + + + + + From 7b99c6003d09783269430fb5a0f7265850b766f8 Mon Sep 17 00:00:00 2001 From: RogueException Date: Wed, 26 Apr 2017 10:35:56 -0300 Subject: [PATCH 151/243] Updated test dependencies --- test/Discord.Net.Tests/Discord.Net.Tests.csproj | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/test/Discord.Net.Tests/Discord.Net.Tests.csproj b/test/Discord.Net.Tests/Discord.Net.Tests.csproj index 75f6ec26d..926699324 100644 --- a/test/Discord.Net.Tests/Discord.Net.Tests.csproj +++ b/test/Discord.Net.Tests/Discord.Net.Tests.csproj @@ -17,11 +17,10 @@ - + - - - - + + + From 649bf27557e5b2d6e5cf21e09977345ad60569b3 Mon Sep 17 00:00:00 2001 From: RogueException Date: Wed, 26 Apr 2017 12:06:46 -0300 Subject: [PATCH 152/243] Fixed nullref in UDPClient.SetCancelToken --- src/Discord.Net.Providers.UdpClient/UDPClient.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Discord.Net.Providers.UdpClient/UDPClient.cs b/src/Discord.Net.Providers.UdpClient/UDPClient.cs index dfd05cf38..b4798d7f1 100644 --- a/src/Discord.Net.Providers.UdpClient/UDPClient.cs +++ b/src/Discord.Net.Providers.UdpClient/UDPClient.cs @@ -24,6 +24,7 @@ namespace Discord.Net.Providers.UDPClient public UDPClient() { _lock = new SemaphoreSlim(1, 1); + _cancelTokenSource = new CancellationTokenSource(); } private void Dispose(bool disposing) { From 9954536fcc30cfee917d037e1bf90ed5c1927122 Mon Sep 17 00:00:00 2001 From: RogueException Date: Wed, 26 Apr 2017 12:19:50 -0300 Subject: [PATCH 153/243] Removed UDP Provider --- Discord.Net.sln | 15 -- .../Discord.Net.Providers.UdpClient.csproj | 12 -- .../UDPClient.cs | 131 ------------------ .../UDPClientProvider.cs | 9 -- 4 files changed, 167 deletions(-) delete mode 100644 src/Discord.Net.Providers.UdpClient/Discord.Net.Providers.UdpClient.csproj delete mode 100644 src/Discord.Net.Providers.UdpClient/UDPClient.cs delete mode 100644 src/Discord.Net.Providers.UdpClient/UDPClientProvider.cs diff --git a/Discord.Net.sln b/Discord.Net.sln index 4a7b07577..a63606787 100644 --- a/Discord.Net.sln +++ b/Discord.Net.sln @@ -18,8 +18,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Providers", "Providers", "{ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Providers.WS4Net", "src\Discord.Net.Providers.WS4Net\Discord.Net.Providers.WS4Net.csproj", "{6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Providers.UdpClient", "src\Discord.Net.Providers.UdpClient\Discord.Net.Providers.UdpClient.csproj", "{ABC9F4B9-2452-4725-B522-754E0A02E282}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Extensions", "Extensions", "{CC3D4B1C-9DE0-448B-8AE7-F3F1F3EC5C3A}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Tests", "test\Discord.Net.Tests\Discord.Net.Tests.csproj", "{C38E5BC1-11CB-4101-8A38-5B40A1BC6433}" @@ -108,18 +106,6 @@ Global {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Release|x64.Build.0 = Release|Any CPU {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Release|x86.ActiveCfg = Release|Any CPU {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Release|x86.Build.0 = Release|Any CPU - {ABC9F4B9-2452-4725-B522-754E0A02E282}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {ABC9F4B9-2452-4725-B522-754E0A02E282}.Debug|Any CPU.Build.0 = Debug|Any CPU - {ABC9F4B9-2452-4725-B522-754E0A02E282}.Debug|x64.ActiveCfg = Debug|Any CPU - {ABC9F4B9-2452-4725-B522-754E0A02E282}.Debug|x64.Build.0 = Debug|Any CPU - {ABC9F4B9-2452-4725-B522-754E0A02E282}.Debug|x86.ActiveCfg = Debug|Any CPU - {ABC9F4B9-2452-4725-B522-754E0A02E282}.Debug|x86.Build.0 = Debug|Any CPU - {ABC9F4B9-2452-4725-B522-754E0A02E282}.Release|Any CPU.ActiveCfg = Release|Any CPU - {ABC9F4B9-2452-4725-B522-754E0A02E282}.Release|Any CPU.Build.0 = Release|Any CPU - {ABC9F4B9-2452-4725-B522-754E0A02E282}.Release|x64.ActiveCfg = Release|Any CPU - {ABC9F4B9-2452-4725-B522-754E0A02E282}.Release|x64.Build.0 = Release|Any CPU - {ABC9F4B9-2452-4725-B522-754E0A02E282}.Release|x86.ActiveCfg = Release|Any CPU - {ABC9F4B9-2452-4725-B522-754E0A02E282}.Release|x86.Build.0 = Release|Any CPU {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Debug|Any CPU.Build.0 = Debug|Any CPU {C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -154,7 +140,6 @@ Global {078DD7E6-943D-4D09-AFC2-D2BA58B76C9C} = {CC3D4B1C-9DE0-448B-8AE7-F3F1F3EC5C3A} {688FD1D8-7F01-4539-B2E9-F473C5D699C7} = {288C363D-A636-4EAE-9AC1-4698B641B26E} {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7} = {B0657AAE-DCC5-4FBF-8E5D-1FB578CF3012} - {ABC9F4B9-2452-4725-B522-754E0A02E282} = {B0657AAE-DCC5-4FBF-8E5D-1FB578CF3012} {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30} = {CC3D4B1C-9DE0-448B-8AE7-F3F1F3EC5C3A} EndGlobalSection EndGlobal diff --git a/src/Discord.Net.Providers.UdpClient/Discord.Net.Providers.UdpClient.csproj b/src/Discord.Net.Providers.UdpClient/Discord.Net.Providers.UdpClient.csproj deleted file mode 100644 index 3a0a6612a..000000000 --- a/src/Discord.Net.Providers.UdpClient/Discord.Net.Providers.UdpClient.csproj +++ /dev/null @@ -1,12 +0,0 @@ - - - - Discord.Net.Providers.UDPClient - Discord.Providers.UDPClient - An optional UDP client provider for Discord.Net using System.Net.UdpClient - net45 - - - - - \ No newline at end of file diff --git a/src/Discord.Net.Providers.UdpClient/UDPClient.cs b/src/Discord.Net.Providers.UdpClient/UDPClient.cs deleted file mode 100644 index b4798d7f1..000000000 --- a/src/Discord.Net.Providers.UdpClient/UDPClient.cs +++ /dev/null @@ -1,131 +0,0 @@ -using Discord.Net.Udp; -using System; -using System.Net; -using System.Threading; -using System.Threading.Tasks; -using UdpSocket = System.Net.Sockets.UdpClient; - -namespace Discord.Net.Providers.UDPClient -{ - internal class UDPClient : IUdpSocket, IDisposable - { - public event Func ReceivedDatagram; - - private readonly SemaphoreSlim _lock; - private UdpSocket _udp; - private IPEndPoint _destination; - private CancellationTokenSource _cancelTokenSource; - private CancellationToken _cancelToken, _parentToken; - private Task _task; - private bool _isDisposed; - - public ushort Port => (ushort)((_udp?.Client.LocalEndPoint as IPEndPoint)?.Port ?? 0); - - public UDPClient() - { - _lock = new SemaphoreSlim(1, 1); - _cancelTokenSource = new CancellationTokenSource(); - } - private void Dispose(bool disposing) - { - if (!_isDisposed) - { - if (disposing) - StopInternalAsync(true).GetAwaiter().GetResult(); - _isDisposed = true; - } - } - public void Dispose() - { - Dispose(true); - } - - - public async Task StartAsync() - { - await _lock.WaitAsync().ConfigureAwait(false); - try - { - await StartInternalAsync(_cancelToken).ConfigureAwait(false); - } - finally - { - _lock.Release(); - } - } - public async Task StartInternalAsync(CancellationToken cancelToken) - { - await StopInternalAsync().ConfigureAwait(false); - - _cancelTokenSource = new CancellationTokenSource(); - _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _cancelTokenSource.Token).Token; - - _udp = new UdpSocket(); - - _task = RunAsync(_cancelToken); - } - public async Task StopAsync() - { - await _lock.WaitAsync().ConfigureAwait(false); - try - { - await StopInternalAsync().ConfigureAwait(false); - } - finally - { - _lock.Release(); - } - } - public async Task StopInternalAsync(bool isDisposing = false) - { - try { _cancelTokenSource.Cancel(false); } catch { } - - if (!isDisposing) - await (_task ?? Task.Delay(0)).ConfigureAwait(false); - - if (_udp != null) - { - try { _udp.Close(); } - catch { } - _udp = null; - } - } - - public void SetDestination(string host, int port) - { - var entry = Dns.GetHostEntryAsync(host).GetAwaiter().GetResult(); - _destination = new IPEndPoint(entry.AddressList[0], port); - } - public void SetCancelToken(CancellationToken cancelToken) - { - _parentToken = cancelToken; - _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _cancelTokenSource.Token).Token; - } - - public async Task SendAsync(byte[] data, int index, int count) - { - if (index != 0) //Should never happen? - { - var newData = new byte[count]; - Buffer.BlockCopy(data, index, newData, 0, count); - data = newData; - } - await _udp.SendAsync(data, count, _destination).ConfigureAwait(false); - } - - private async Task RunAsync(CancellationToken cancelToken) - { - var closeTask = Task.Delay(-1, cancelToken); - while (!cancelToken.IsCancellationRequested) - { - var receiveTask = _udp.ReceiveAsync(); - var task = await Task.WhenAny(closeTask, receiveTask).ConfigureAwait(false); - if (task == closeTask) - break; - - var result = receiveTask.Result; - await ReceivedDatagram(result.Buffer, 0, result.Buffer.Length).ConfigureAwait(false); - } - } - } -} \ No newline at end of file diff --git a/src/Discord.Net.Providers.UdpClient/UDPClientProvider.cs b/src/Discord.Net.Providers.UdpClient/UDPClientProvider.cs deleted file mode 100644 index 6bdf9eb63..000000000 --- a/src/Discord.Net.Providers.UdpClient/UDPClientProvider.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Discord.Net.Udp; - -namespace Discord.Net.Providers.UDPClient -{ - public static class UDPClientProvider - { - public static readonly UdpSocketProvider Instance = () => new UDPClient(); - } -} From 294ffa37291b87035ea5fb1e54d97788031d46eb Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Wed, 26 Apr 2017 17:41:32 +0200 Subject: [PATCH 154/243] Remove Discord.Net.Providers.UdpClient from pack.ps (#627) This should fix the appveyor build. --- pack.ps1 | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pack.ps1 b/pack.ps1 index 5ce51834c..39b9c21ce 100644 --- a/pack.ps1 +++ b/pack.ps1 @@ -12,8 +12,6 @@ dotnet pack "src\Discord.Net.Webhook\Discord.Net.Webhook.csproj" -c "Release" -o if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) } dotnet pack "src\Discord.Net.Providers.WS4Net\Discord.Net.Providers.WS4Net.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) } -dotnet pack "src\Discord.Net.Providers.UdpClient\Discord.Net.Providers.UdpClient.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" -if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) } if ($Env:APPVEYOR_REPO_TAG -eq "true") { nuget pack src\Discord.Net\Discord.Net.nuspec -OutputDirectory "artifacts" -properties suffix="" @@ -21,4 +19,4 @@ if ($Env:APPVEYOR_REPO_TAG -eq "true") { else { nuget pack src\Discord.Net\Discord.Net.nuspec -OutputDirectory "artifacts" -properties suffix="-$Env:BUILD" } -if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) } \ No newline at end of file +if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) } From c6ccddb4cede07d2476d0573bfd7e28dcd1a386f Mon Sep 17 00:00:00 2001 From: RogueException Date: Wed, 26 Apr 2017 13:06:13 -0300 Subject: [PATCH 155/243] Added UnstableRestClientProvider --- .../UnstableRestClient.cs | 154 ++++++++++++++++++ .../UnstableRestClientProvider.cs | 9 + src/Discord.Net.Rest/Net/DefaultRestClient.cs | 15 +- 3 files changed, 169 insertions(+), 9 deletions(-) create mode 100644 src/Discord.Net.DebugTools/UnstableRestClient.cs create mode 100644 src/Discord.Net.DebugTools/UnstableRestClientProvider.cs diff --git a/src/Discord.Net.DebugTools/UnstableRestClient.cs b/src/Discord.Net.DebugTools/UnstableRestClient.cs new file mode 100644 index 000000000..42a77ff96 --- /dev/null +++ b/src/Discord.Net.DebugTools/UnstableRestClient.cs @@ -0,0 +1,154 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Net.Rest +{ + internal sealed class UnstableRestClient : IRestClient, IDisposable + { + private const double FailureRate = 0.10; //10% + + private const int HR_SECURECHANNELFAILED = -2146233079; + + private readonly HttpClient _client; + private readonly string _baseUrl; + private readonly JsonSerializer _errorDeserializer; + private readonly Random _rand; + private CancellationToken _cancelToken; + private bool _isDisposed; + + public DefaultRestClient(string baseUrl) + { + _baseUrl = baseUrl; + + _client = new HttpClient(new HttpClientHandler + { + AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate, + UseCookies = false, + UseProxy = false + }); + SetHeader("accept-encoding", "gzip, deflate"); + + _cancelToken = CancellationToken.None; + _errorDeserializer = new JsonSerializer(); + _rand = new Random(); + } + private void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + _client.Dispose(); + _isDisposed = true; + } + } + public void Dispose() + { + Dispose(true); + } + + public void SetHeader(string key, string value) + { + _client.DefaultRequestHeaders.Remove(key); + if (value != null) + _client.DefaultRequestHeaders.Add(key, value); + } + public void SetCancelToken(CancellationToken cancelToken) + { + _cancelToken = cancelToken; + } + + public async Task SendAsync(string method, string endpoint, CancellationToken cancelToken, bool headerOnly) + { + string uri = Path.Combine(_baseUrl, endpoint); + using (var restRequest = new HttpRequestMessage(GetMethod(method), uri)) + return await SendInternalAsync(restRequest, cancelToken, headerOnly).ConfigureAwait(false); + } + public async Task SendAsync(string method, string endpoint, string json, CancellationToken cancelToken, bool headerOnly) + { + string uri = Path.Combine(_baseUrl, endpoint); + using (var restRequest = new HttpRequestMessage(GetMethod(method), uri)) + { + restRequest.Content = new StringContent(json, Encoding.UTF8, "application/json"); + return await SendInternalAsync(restRequest, cancelToken, headerOnly).ConfigureAwait(false); + } + } + public async Task SendAsync(string method, string endpoint, IReadOnlyDictionary multipartParams, CancellationToken cancelToken, bool headerOnly) + { + string uri = Path.Combine(_baseUrl, endpoint); + using (var restRequest = new HttpRequestMessage(GetMethod(method), uri)) + { + var content = new MultipartFormDataContent("Upload----" + DateTime.Now.ToString(CultureInfo.InvariantCulture)); + if (multipartParams != null) + { + foreach (var p in multipartParams) + { + switch (p.Value) + { + case string stringValue: { content.Add(new StringContent(stringValue), p.Key); continue; } + case byte[] byteArrayValue: { content.Add(new ByteArrayContent(byteArrayValue), p.Key); continue; } + case Stream streamValue: { content.Add(new StreamContent(streamValue), p.Key); continue; } + case MultipartFile fileValue: + { + var stream = fileValue.Stream; + if (!stream.CanSeek) + { + var memoryStream = new MemoryStream(); + await stream.CopyToAsync(memoryStream).ConfigureAwait(false); + memoryStream.Position = 0; + stream = memoryStream; + } + content.Add(new StreamContent(stream), p.Key, fileValue.Filename); + continue; + } + default: throw new InvalidOperationException($"Unsupported param type \"{p.Value.GetType().Name}\""); + } + } + } + restRequest.Content = content; + return await SendInternalAsync(restRequest, cancelToken, headerOnly).ConfigureAwait(false); + } + } + + private async Task SendInternalAsync(HttpRequestMessage request, CancellationToken cancelToken, bool headerOnly) + { + if (!UnstableCheck()) + return; + + cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_cancelToken, cancelToken).Token; + HttpResponseMessage response = await _client.SendAsync(request, cancelToken).ConfigureAwait(false); + + var headers = response.Headers.ToDictionary(x => x.Key, x => x.Value.FirstOrDefault(), StringComparer.OrdinalIgnoreCase); + var stream = !headerOnly ? await response.Content.ReadAsStreamAsync().ConfigureAwait(false) : null; + + return new RestResponse(response.StatusCode, headers, stream); + } + + private static readonly HttpMethod _patch = new HttpMethod("PATCH"); + private HttpMethod GetMethod(string method) + { + switch (method) + { + case "DELETE": return HttpMethod.Delete; + case "GET": return HttpMethod.Get; + case "PATCH": return _patch; + case "POST": return HttpMethod.Post; + case "PUT": return HttpMethod.Put; + default: throw new ArgumentOutOfRangeException(nameof(method), $"Unknown HttpMethod: {method}"); + } + } + + private bool UnstableCheck() + { + return _rand.NextDouble() > FailureRate; + } + } +} diff --git a/src/Discord.Net.DebugTools/UnstableRestClientProvider.cs b/src/Discord.Net.DebugTools/UnstableRestClientProvider.cs new file mode 100644 index 000000000..80ed91c5b --- /dev/null +++ b/src/Discord.Net.DebugTools/UnstableRestClientProvider.cs @@ -0,0 +1,9 @@ +using Discord.Net.Rest; + +namespace Discord.Net.Providers.UnstableUdpSocket +{ + public static class UnstableRestClientProvider + { + public static readonly RestCientProvider Instance = () => new UnstableRestClientProvider(); + } +} diff --git a/src/Discord.Net.Rest/Net/DefaultRestClient.cs b/src/Discord.Net.Rest/Net/DefaultRestClient.cs index 493fc3aff..20fbe2278 100644 --- a/src/Discord.Net.Rest/Net/DefaultRestClient.cs +++ b/src/Discord.Net.Rest/Net/DefaultRestClient.cs @@ -116,16 +116,13 @@ namespace Discord.Net.Rest private async Task SendInternalAsync(HttpRequestMessage request, CancellationToken cancelToken, bool headerOnly) { - while (true) - { - cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_cancelToken, cancelToken).Token; - HttpResponseMessage response = await _client.SendAsync(request, cancelToken).ConfigureAwait(false); - - var headers = response.Headers.ToDictionary(x => x.Key, x => x.Value.FirstOrDefault(), StringComparer.OrdinalIgnoreCase); - var stream = !headerOnly ? await response.Content.ReadAsStreamAsync().ConfigureAwait(false) : null; + cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_cancelToken, cancelToken).Token; + HttpResponseMessage response = await _client.SendAsync(request, cancelToken).ConfigureAwait(false); + + var headers = response.Headers.ToDictionary(x => x.Key, x => x.Value.FirstOrDefault(), StringComparer.OrdinalIgnoreCase); + var stream = !headerOnly ? await response.Content.ReadAsStreamAsync().ConfigureAwait(false) : null; - return new RestResponse(response.StatusCode, headers, stream); - } + return new RestResponse(response.StatusCode, headers, stream); } private static readonly HttpMethod _patch = new HttpMethod("PATCH"); From bd85bbf30a4a1577f93e19a0bd38e9601e0e1914 Mon Sep 17 00:00:00 2001 From: RogueException Date: Wed, 26 Apr 2017 13:06:34 -0300 Subject: [PATCH 156/243] Moved UserAgent to DiscordConfig --- src/Discord.Net.Core/DiscordConfig.cs | 1 + src/Discord.Net.Rest/DiscordRestConfig.cs | 5 ----- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Discord.Net.Core/DiscordConfig.cs b/src/Discord.Net.Core/DiscordConfig.cs index d24a0ea3a..fd2fe92e8 100644 --- a/src/Discord.Net.Core/DiscordConfig.cs +++ b/src/Discord.Net.Core/DiscordConfig.cs @@ -10,6 +10,7 @@ namespace Discord typeof(DiscordConfig).GetTypeInfo().Assembly.GetName().Version.ToString(3) ?? "Unknown"; + public static string UserAgent { get; } = $"DiscordBot (https://github.com/RogueException/Discord.Net, v{Version})"; public static readonly string APIUrl = $"https://discordapp.com/api/v{APIVersion}/"; public const string CDNUrl = "https://cdn.discordapp.com/"; public const string InviteUrl = "https://discord.gg/"; diff --git a/src/Discord.Net.Rest/DiscordRestConfig.cs b/src/Discord.Net.Rest/DiscordRestConfig.cs index c3cd70683..4a7aae287 100644 --- a/src/Discord.Net.Rest/DiscordRestConfig.cs +++ b/src/Discord.Net.Rest/DiscordRestConfig.cs @@ -4,11 +4,6 @@ namespace Discord.Rest { public class DiscordRestConfig : DiscordConfig { - public static string UserAgent { get; } = $"DiscordBot (https://github.com/RogueException/Discord.Net, v{Version})"; - - internal const int MessageQueueInterval = 100; - internal const int WebSocketQueueInterval = 100; - /// Gets or sets the provider used to generate new REST connections. public RestClientProvider RestClientProvider { get; set; } = DefaultRestClientProvider.Instance; } From ba1a9aaa185c8087d86cef4c910f3ce74b1db1fe Mon Sep 17 00:00:00 2001 From: RogueException Date: Wed, 26 Apr 2017 13:09:08 -0300 Subject: [PATCH 157/243] UnstableRestClient should timeout instead of ignore requests --- src/Discord.Net.DebugTools/UnstableRestClient.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net.DebugTools/UnstableRestClient.cs b/src/Discord.Net.DebugTools/UnstableRestClient.cs index 42a77ff96..847b8d64e 100644 --- a/src/Discord.Net.DebugTools/UnstableRestClient.cs +++ b/src/Discord.Net.DebugTools/UnstableRestClient.cs @@ -121,8 +121,8 @@ namespace Discord.Net.Rest private async Task SendInternalAsync(HttpRequestMessage request, CancellationToken cancelToken, bool headerOnly) { if (!UnstableCheck()) - return; - + throw new TimeoutException(); + cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_cancelToken, cancelToken).Token; HttpResponseMessage response = await _client.SendAsync(request, cancelToken).ConfigureAwait(false); From 4ca273dd4a6cd6c36f279e9c9f5efba28e0222f8 Mon Sep 17 00:00:00 2001 From: Confruggy Date: Fri, 28 Apr 2017 16:49:50 +0200 Subject: [PATCH 158/243] Fixes RoleTypeReader (#631) --- src/Discord.Net.Commands/Readers/RoleTypeReader.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.Commands/Readers/RoleTypeReader.cs b/src/Discord.Net.Commands/Readers/RoleTypeReader.cs index 48544eeda..a90432782 100644 --- a/src/Discord.Net.Commands/Readers/RoleTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/RoleTypeReader.cs @@ -31,7 +31,7 @@ namespace Discord.Commands AddResult(results, role as T, role.Name == input ? 0.80f : 0.70f); if (results.Count > 0) - return Task.FromResult(TypeReaderResult.FromSuccess(results.Values)); + return Task.FromResult(TypeReaderResult.FromSuccess(results.Values.ToReadOnlyCollection())); } return Task.FromResult(TypeReaderResult.FromError(CommandError.ObjectNotFound, "Role not found.")); } From 3365bbb04306c139f8507e66922381a0e7130d3e Mon Sep 17 00:00:00 2001 From: Confruggy Date: Fri, 28 Apr 2017 16:49:59 +0200 Subject: [PATCH 159/243] Fixes ChannelTypeReader (#630) --- src/Discord.Net.Commands/Readers/ChannelTypeReader.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.Commands/Readers/ChannelTypeReader.cs b/src/Discord.Net.Commands/Readers/ChannelTypeReader.cs index 08821c62f..d2e34b436 100644 --- a/src/Discord.Net.Commands/Readers/ChannelTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/ChannelTypeReader.cs @@ -30,7 +30,7 @@ namespace Discord.Commands AddResult(results, channel as T, channel.Name == input ? 0.80f : 0.70f); if (results.Count > 0) - return TypeReaderResult.FromSuccess(results.Values); + return TypeReaderResult.FromSuccess(results.Values.ToReadOnlyCollection()); } return TypeReaderResult.FromError(CommandError.ObjectNotFound, "Channel not found."); From cb4f6e37f63c4383ae54f6674ec09bee854879ac Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Sat, 29 Apr 2017 09:42:47 +0200 Subject: [PATCH 160/243] Overloaded AddModuleAsync with Type (#581) * Overloaded AddModuleAsync with Type * Overloaded RemoveModuleAsync with Type * Use expression-bodied method for consistency --- src/Discord.Net.Commands/CommandService.cs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index c0c20f80f..945db33a8 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -83,20 +83,21 @@ namespace Discord.Commands _moduleLock.Release(); } } - public async Task AddModuleAsync() + public Task AddModuleAsync() => AddModuleAsync(typeof(T)); + public async Task AddModuleAsync(Type type) { await _moduleLock.WaitAsync().ConfigureAwait(false); try { - var typeInfo = typeof(T).GetTypeInfo(); + var typeInfo = type.GetTypeInfo(); - if (_typedModuleDefs.ContainsKey(typeof(T))) + if (_typedModuleDefs.ContainsKey(type)) throw new ArgumentException($"This module has already been added."); var module = ModuleClassBuilder.Build(this, typeInfo).FirstOrDefault(); if (module.Value == default(ModuleInfo)) - throw new InvalidOperationException($"Could not build the module {typeof(T).FullName}, did you pass an invalid type?"); + throw new InvalidOperationException($"Could not build the module {type.FullName}, did you pass an invalid type?"); _typedModuleDefs[module.Key] = module.Value; @@ -153,13 +154,14 @@ namespace Discord.Commands _moduleLock.Release(); } } - public async Task RemoveModuleAsync() + public Task RemoveModuleAsync() => RemoveModuleAsync(typeof(T)); + public async Task RemoveModuleAsync(Type type) { await _moduleLock.WaitAsync().ConfigureAwait(false); try { ModuleInfo module; - if (!_typedModuleDefs.TryRemove(typeof(T), out module)) + if (!_typedModuleDefs.TryRemove(type, out module)) return false; return RemoveModuleInternal(module); From 90ac9027cf41238dd782770bfb773b14a4283f07 Mon Sep 17 00:00:00 2001 From: Sindre Langhus Date: Mon, 1 May 2017 02:29:12 +0200 Subject: [PATCH 161/243] Replace Where+FirstOrDefault with FirstOrDefault in SocketClient (#636) * Replace Where.FirstOrDefault with FirstOrDefault * Replace Where+FirstOrDefault in ClientHelper --- src/Discord.Net.Rest/ClientHelper.cs | 2 +- src/Discord.Net.WebSocket/DiscordSocketClient.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net.Rest/ClientHelper.cs b/src/Discord.Net.Rest/ClientHelper.cs index a1fedb113..8bc800a7d 100644 --- a/src/Discord.Net.Rest/ClientHelper.cs +++ b/src/Discord.Net.Rest/ClientHelper.cs @@ -150,7 +150,7 @@ namespace Discord.Rest string id, RequestOptions options) { var models = await client.ApiClient.GetVoiceRegionsAsync(options).ConfigureAwait(false); - return models.Select(x => RestVoiceRegion.Create(client, x)).Where(x => x.Id == id).FirstOrDefault(); + return models.Select(x => RestVoiceRegion.Create(client, x)).FirstOrDefault(x => x.Id == id); } } } diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index a8543f108..5e19e14e6 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -267,7 +267,7 @@ namespace Discord.WebSocket /// public SocketUser GetUser(string username, string discriminator) { - return State.Users.Where(x => x.Discriminator == discriminator && x.Username == username).FirstOrDefault(); + return State.Users.FirstOrDefault(x => x.Discriminator == discriminator && x.Username == username); } internal SocketGlobalUser GetOrCreateUser(ClientState state, Discord.API.User model) { From 5ad2d7361d31458012b320ef85b27149d86daf42 Mon Sep 17 00:00:00 2001 From: RogueException Date: Tue, 2 May 2017 01:03:04 -0300 Subject: [PATCH 162/243] Added appveyor.yml, README edits (#639) --- Discord.Net.targets | 4 ++-- README.md | 5 ++--- appveyor.yml | 55 +++++++++++++++++++++++++++++++++++++++++++++ build.ps1 | 4 ---- pack.ps1 | 22 ------------------ test.ps1 | 2 -- 6 files changed, 59 insertions(+), 33 deletions(-) create mode 100644 appveyor.yml delete mode 100644 build.ps1 delete mode 100644 pack.ps1 delete mode 100644 test.ps1 diff --git a/Discord.Net.targets b/Discord.Net.targets index 669db428d..3bfa199c1 100644 --- a/Discord.Net.targets +++ b/Discord.Net.targets @@ -10,12 +10,12 @@ git://github.com/RogueException/Discord.Net - dev $(VersionSuffix)-dev + dev - $(BuildNumber) $(VersionSuffix)-$(BuildNumber) + build-$(BuildNumber) $(DefineConstants);FILESYSTEM;DEFAULTUDPCLIENT;DEFAULTWEBSOCKET diff --git a/README.md b/README.md index c932f6b25..c5ed907ee 100644 --- a/README.md +++ b/README.md @@ -14,14 +14,13 @@ Our stable builds available from NuGet through the Discord.Net metapackage: - [Discord.Net](https://www.nuget.org/packages/Discord.Net/) The individual components may also be installed from NuGet: +- [Discord.Net.Commands](https://www.nuget.org/packages/Discord.Net.Commands/) - [Discord.Net.Rest](https://www.nuget.org/packages/Discord.Net.Rest/) - [Discord.Net.Rpc](https://www.nuget.org/packages/Discord.Net.Rpc/) - [Discord.Net.WebSocket](https://www.nuget.org/packages/Discord.Net.WebSocket/) - [Discord.Net.Webhook](https://www.nuget.org/packages/Discord.Net.Webhook/) -- [Discord.Net.Commands](https://www.nuget.org/packages/Discord.Net.Commands/) -The following providers are available for platforms not supporting .NET Standard 1.3: -- [Discord.Net.Providers.UdpClient](https://www.nuget.org/packages/Discord.Net.Providers.UdpClient/) +The following provider is available for platforms not supporting .NET Standard 1.3: - [Discord.Net.Providers.WS4Net](https://www.nuget.org/packages/Discord.Net.Providers.WS4Net/) ### Unstable (MyGet) diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 000000000..f1b4bf992 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,55 @@ +version: build-{build} +branches: + only: + - dev +image: Visual Studio 2017 + +nuget: + disable_publish_on_pr: true +pull_requests: + do_not_increment_build_number: true +clone_folder: C:\Projects\Discord.Net +cache: test/Discord.Net.Tests/cache.db + +environment: + DOTNET_CLI_TELEMETRY_OPTOUT: 1 + DNET_TEST_TOKEN: + secure: l7h5e7UE7yRd70hAB97kjPiQpPOShwqoBbOzEAYQ+XBd/Pre5OA33IXa3uisdUeQJP/nPFhcOsI+yn7WpuFaoQ== + DNET_TEST_GUILDID: 273160668180381696 +init: +- ps: $Env:BUILD = "$($Env:APPVEYOR_BUILD_NUMBER.PadLeft(5, "0"))" + +build_script: +- ps: appveyor-retry dotnet restore Discord.Net.sln -v Minimal /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" +- ps: dotnet build Discord.Net.sln -c "Release" /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" +- ps: dotnet pack "src\Discord.Net.Core\Discord.Net.Core.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" +- ps: dotnet pack "src\Discord.Net.Rest\Discord.Net.Rest.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" +- ps: dotnet pack "src\Discord.Net.WebSocket\Discord.Net.WebSocket.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" +- ps: dotnet pack "src\Discord.Net.Rpc\Discord.Net.Rpc.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" +- ps: dotnet pack "src\Discord.Net.Commands\Discord.Net.Commands.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" +- ps: dotnet pack "src\Discord.Net.Webhook\Discord.Net.Webhook.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" +- ps: dotnet pack "src\Discord.Net.Providers.WS4Net\Discord.Net.Providers.WS4Net.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" +- ps: >- + if ($Env:APPVEYOR_REPO_TAG -eq "true") { + nuget pack src\Discord.Net\Discord.Net.nuspec -OutputDirectory "artifacts" -properties suffix="" + } else { + nuget pack src\Discord.Net\Discord.Net.nuspec -OutputDirectory "artifacts" -properties suffix="-$Env:BUILD" + } +test_script: +- ps: >- + if ($APPVEYOR_PULL_REQUEST_NUMBER -ne "") { + dotnet test test/Discord.Net.Tests/Discord.Net.Tests.csproj -c "Release" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" + } + +artifacts: +- path: artifacts/*.nupkg + name: MyGet +deploy: +- provider: NuGet + server: https://www.myget.org/F/discord-net/api/v2/package + api_key: + secure: Jl7BXeUjRnkVHDMBuUWSXcEOkrli1PBleW2IiLyUs5j63UNUNp1hcjaUJRujx9lz + symbol_server: https://www.myget.org/F/discord-net/symbols/api/v2/package + artifact: MyGet + on: + branch: dev \ No newline at end of file diff --git a/build.ps1 b/build.ps1 deleted file mode 100644 index 1b960011a..000000000 --- a/build.ps1 +++ /dev/null @@ -1,4 +0,0 @@ -appveyor-retry dotnet restore Discord.Net.sln -v Minimal /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" -if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) } -dotnet build Discord.Net.sln -c "Release" /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" -if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) } \ No newline at end of file diff --git a/pack.ps1 b/pack.ps1 deleted file mode 100644 index 39b9c21ce..000000000 --- a/pack.ps1 +++ /dev/null @@ -1,22 +0,0 @@ -dotnet pack "src\Discord.Net.Core\Discord.Net.Core.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" -if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) } -dotnet pack "src\Discord.Net.Rest\Discord.Net.Rest.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" -if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) } -dotnet pack "src\Discord.Net.WebSocket\Discord.Net.WebSocket.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" -if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) } -dotnet pack "src\Discord.Net.Rpc\Discord.Net.Rpc.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" -if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) } -dotnet pack "src\Discord.Net.Commands\Discord.Net.Commands.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" -if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) } -dotnet pack "src\Discord.Net.Webhook\Discord.Net.Webhook.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" -if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) } -dotnet pack "src\Discord.Net.Providers.WS4Net\Discord.Net.Providers.WS4Net.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" -if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) } - -if ($Env:APPVEYOR_REPO_TAG -eq "true") { - nuget pack src\Discord.Net\Discord.Net.nuspec -OutputDirectory "artifacts" -properties suffix="" -} -else { - nuget pack src\Discord.Net\Discord.Net.nuspec -OutputDirectory "artifacts" -properties suffix="-$Env:BUILD" -} -if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) } diff --git a/test.ps1 b/test.ps1 deleted file mode 100644 index d73a01f23..000000000 --- a/test.ps1 +++ /dev/null @@ -1,2 +0,0 @@ -dotnet test test/Discord.Net.Tests/Discord.Net.Tests.csproj -c "Release" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" -if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) } \ No newline at end of file From 4d7a97c37af81e705d6a5e9e041d1d6c113ec22a Mon Sep 17 00:00:00 2001 From: RogueException Date: Tue, 2 May 2017 01:49:57 -0300 Subject: [PATCH 163/243] Push artifacts from build script --- appveyor.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index f1b4bf992..3355b3bd9 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -22,6 +22,7 @@ init: build_script: - ps: appveyor-retry dotnet restore Discord.Net.sln -v Minimal /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" - ps: dotnet build Discord.Net.sln -c "Release" /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" +after_build: - ps: dotnet pack "src\Discord.Net.Core\Discord.Net.Core.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" - ps: dotnet pack "src\Discord.Net.Rest\Discord.Net.Rest.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" - ps: dotnet pack "src\Discord.Net.WebSocket\Discord.Net.WebSocket.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" @@ -35,21 +36,19 @@ build_script: } else { nuget pack src\Discord.Net\Discord.Net.nuspec -OutputDirectory "artifacts" -properties suffix="-$Env:BUILD" } +- ps: Get-ChildItem artifacts\*.nupkg | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name } + test_script: - ps: >- if ($APPVEYOR_PULL_REQUEST_NUMBER -ne "") { dotnet test test/Discord.Net.Tests/Discord.Net.Tests.csproj -c "Release" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" } -artifacts: -- path: artifacts/*.nupkg - name: MyGet deploy: - provider: NuGet server: https://www.myget.org/F/discord-net/api/v2/package api_key: secure: Jl7BXeUjRnkVHDMBuUWSXcEOkrli1PBleW2IiLyUs5j63UNUNp1hcjaUJRujx9lz symbol_server: https://www.myget.org/F/discord-net/symbols/api/v2/package - artifact: MyGet on: branch: dev \ No newline at end of file From c40857bb1e453f0d166ca19301a11ccf00f7a201 Mon Sep 17 00:00:00 2001 From: RogueException Date: Tue, 2 May 2017 02:05:57 -0300 Subject: [PATCH 164/243] Added personal myget --- appveyor.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/appveyor.yml b/appveyor.yml index 3355b3bd9..311e61950 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -50,5 +50,12 @@ deploy: api_key: secure: Jl7BXeUjRnkVHDMBuUWSXcEOkrli1PBleW2IiLyUs5j63UNUNp1hcjaUJRujx9lz symbol_server: https://www.myget.org/F/discord-net/symbols/api/v2/package + on: + branch: dev +- provider: NuGet + server: https://www.myget.org/F/rogueexception/api/v2/package + api_key: + secure: D+vW2O2LBf/iJb4f+q8fkyIW2VdIYIGxSYLWNrOD4BHlDBZQlJipDbNarWjUr2Kn + symbol_server: https://www.myget.org/F/rogueexception/symbols/api/v2/package on: branch: dev \ No newline at end of file From 05f8f415670a32c60d71c5661927245f7b33bd93 Mon Sep 17 00:00:00 2001 From: RogueException Date: Thu, 4 May 2017 12:45:24 -0300 Subject: [PATCH 165/243] Fixed PR check in appveyor.yml --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 311e61950..3bf70c09c 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -40,7 +40,7 @@ after_build: test_script: - ps: >- - if ($APPVEYOR_PULL_REQUEST_NUMBER -ne "") { + if ($APPVEYOR_PULL_REQUEST_NUMBER -eq "") { dotnet test test/Discord.Net.Tests/Discord.Net.Tests.csproj -c "Release" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG" } From 7f1fc286cfb15f340838079923aa6208b8828ae2 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Thu, 4 May 2017 11:49:32 -0400 Subject: [PATCH 166/243] Add IChannel.Nsfw, RequireNsfw precondition (#633) * Add IChannel.Nsfw, RequireNsfw precondition * Refactored IChannel.Nsfw to IsNsfw, removed NsfwUtils Per pull-request feedback * proper nsfw channel check --- .../Preconditions/RequireNsfwAttribute.cs | 20 +++++++++++++++++++ .../Entities/Channels/IChannel.cs | 3 +++ .../Entities/Channels/ChannelHelper.cs | 5 +++++ .../Entities/Channels/RestChannel.cs | 1 + .../Channels/RpcVirtualMessageChannel.cs | 1 + .../Entities/Channels/RpcChannel.cs | 4 +++- .../Entities/Channels/SocketChannel.cs | 4 +++- 7 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 src/Discord.Net.Commands/Attributes/Preconditions/RequireNsfwAttribute.cs diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireNsfwAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireNsfwAttribute.cs new file mode 100644 index 000000000..a52ec6ddd --- /dev/null +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireNsfwAttribute.cs @@ -0,0 +1,20 @@ +using System; +using System.Threading.Tasks; + +namespace Discord.Commands +{ + /// + /// Require that the command is invoked in a channel marked NSFW + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public class RequireNsfwAttribute : PreconditionAttribute + { + public override Task CheckPermissions(ICommandContext context, CommandInfo command, IDependencyMap map) + { + if (context.Channel.IsNsfw) + return Task.FromResult(PreconditionResult.FromSuccess()); + else + return Task.FromResult(PreconditionResult.FromError("This command may only be invoked in an NSFW channel.")); + } + } +} diff --git a/src/Discord.Net.Core/Entities/Channels/IChannel.cs b/src/Discord.Net.Core/Entities/Channels/IChannel.cs index 72608ec6a..fbb979951 100644 --- a/src/Discord.Net.Core/Entities/Channels/IChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IChannel.cs @@ -8,6 +8,9 @@ namespace Discord /// Gets the name of this channel. string Name { get; } + /// Checks if the channel is NSFW. + bool IsNsfw { get; } + /// Gets a collection of all users in this channel. IAsyncEnumerable> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); diff --git a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs index e6d017a89..284decd8c 100644 --- a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs +++ b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs @@ -289,5 +289,10 @@ namespace Discord.Rest author = RestUser.Create(client, guild, model, webhookId); return author; } + + public static bool IsNsfw(IChannel channel) => + IsNsfw(channel.Name); + public static bool IsNsfw(string channelName) => + channelName == "nsfw" || channelName.StartsWith("nsfw-"); } } diff --git a/src/Discord.Net.Rest/Entities/Channels/RestChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestChannel.cs index bc521784d..7291b591e 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestChannel.cs @@ -46,6 +46,7 @@ namespace Discord.Rest //IChannel string IChannel.Name => null; + bool IChannel.IsNsfw => ChannelHelper.IsNsfw(this); Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(null); //Overriden diff --git a/src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs index 7e515978d..dfd996ee1 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs @@ -97,6 +97,7 @@ namespace Discord.Rest //IChannel string IChannel.Name { get { throw new NotSupportedException(); } } + bool IChannel.IsNsfw { get { throw new NotSupportedException(); } } IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) { throw new NotSupportedException(); diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcChannel.cs b/src/Discord.Net.Rpc/Entities/Channels/RpcChannel.cs index cca559a31..d26c593ba 100644 --- a/src/Discord.Net.Rpc/Entities/Channels/RpcChannel.cs +++ b/src/Discord.Net.Rpc/Entities/Channels/RpcChannel.cs @@ -1,4 +1,5 @@ -using System; +using Discord.Rest; +using System; using Model = Discord.API.Rpc.Channel; @@ -7,6 +8,7 @@ namespace Discord.Rpc public class RpcChannel : RpcEntity { public string Name { get; private set; } + public bool IsNsfw => ChannelHelper.IsNsfw(Name); public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketChannel.cs index 319e17c50..42c4156f3 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketChannel.cs @@ -1,4 +1,5 @@ -using System; +using Discord.Rest; +using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -40,6 +41,7 @@ namespace Discord.WebSocket //IChannel string IChannel.Name => null; + bool IChannel.IsNsfw => ChannelHelper.IsNsfw(this); Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(null); //Overridden From ba1982a3f9f8770a384013809825c52639e954a8 Mon Sep 17 00:00:00 2001 From: RogueException Date: Thu, 4 May 2017 12:52:26 -0300 Subject: [PATCH 167/243] Replaced DependencyMap with ServiceCollection and IServiceProvider (#625) --- .../ParameterPreconditionAttribute.cs | 3 +- .../Attributes/PreconditionAttribute.cs | 2 +- .../RequireBotPermissionAttribute.cs | 3 +- .../Preconditions/RequireContextAttribute.cs | 3 +- .../Preconditions/RequireOwnerAttribute.cs | 3 +- .../RequireUserPermissionAttribute.cs | 3 +- .../Builders/CommandBuilder.cs | 5 +- .../Builders/ModuleBuilder.cs | 3 +- .../Builders/ModuleClassBuilder.cs | 2 +- src/Discord.Net.Commands/CommandMatch.cs | 16 +-- src/Discord.Net.Commands/CommandService.cs | 22 +++- .../Dependencies/DependencyMap.cs | 102 ---------------- .../Dependencies/IDependencyMap.cs | 89 -------------- .../Discord.Net.Commands.csproj | 3 + .../EmptyServiceProvider.cs | 11 ++ src/Discord.Net.Commands/Info/CommandInfo.cs | 31 +++-- .../Info/ParameterInfo.cs | 8 +- .../Utilities/ReflectionUtils.cs | 115 ++++++++---------- 18 files changed, 128 insertions(+), 296 deletions(-) delete mode 100644 src/Discord.Net.Commands/Dependencies/DependencyMap.cs delete mode 100644 src/Discord.Net.Commands/Dependencies/IDependencyMap.cs create mode 100644 src/Discord.Net.Commands/EmptyServiceProvider.cs diff --git a/src/Discord.Net.Commands/Attributes/ParameterPreconditionAttribute.cs b/src/Discord.Net.Commands/Attributes/ParameterPreconditionAttribute.cs index 168d15e5f..49dae6080 100644 --- a/src/Discord.Net.Commands/Attributes/ParameterPreconditionAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/ParameterPreconditionAttribute.cs @@ -1,11 +1,12 @@ using System; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; namespace Discord.Commands { [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = true, Inherited = true)] public abstract class ParameterPreconditionAttribute : Attribute { - public abstract Task CheckPermissions(ICommandContext context, ParameterInfo parameter, object value, IDependencyMap map); + public abstract Task CheckPermissions(ICommandContext context, ParameterInfo parameter, object value, IServiceProvider services); } } \ No newline at end of file diff --git a/src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs b/src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs index 7755d459b..e099380f6 100644 --- a/src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs @@ -6,6 +6,6 @@ namespace Discord.Commands [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true, Inherited = true)] public abstract class PreconditionAttribute : Attribute { - public abstract Task CheckPermissions(ICommandContext context, CommandInfo command, IDependencyMap map); + public abstract Task CheckPermissions(ICommandContext context, CommandInfo command, IServiceProvider services); } } diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs index 520cfa6fd..82975a2f6 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; namespace Discord.Commands { @@ -41,7 +42,7 @@ namespace Discord.Commands GuildPermission = null; } - public override async Task CheckPermissions(ICommandContext context, CommandInfo command, IDependencyMap map) + public override async Task CheckPermissions(ICommandContext context, CommandInfo command, IServiceProvider services) { var guildUser = await context.Guild.GetCurrentUserAsync(); diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs index 42d835c30..a221eb4a9 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; namespace Discord.Commands { @@ -37,7 +38,7 @@ namespace Discord.Commands Contexts = contexts; } - public override Task CheckPermissions(ICommandContext context, CommandInfo command, IDependencyMap map) + public override Task CheckPermissions(ICommandContext context, CommandInfo command, IServiceProvider services) { bool isValid = false; diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs index 0f4e8255d..0852ce39c 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; namespace Discord.Commands { @@ -10,7 +11,7 @@ namespace Discord.Commands [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] public class RequireOwnerAttribute : PreconditionAttribute { - public override async Task CheckPermissions(ICommandContext context, CommandInfo command, IDependencyMap map) + public override async Task CheckPermissions(ICommandContext context, CommandInfo command, IServiceProvider services) { switch (context.Client.TokenType) { diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs index c5b79c5b9..44c69d76a 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; namespace Discord.Commands { @@ -42,7 +43,7 @@ namespace Discord.Commands GuildPermission = null; } - public override Task CheckPermissions(ICommandContext context, CommandInfo command, IDependencyMap map) + public override Task CheckPermissions(ICommandContext context, CommandInfo command, IServiceProvider services) { var guildUser = context.User as IGuildUser; diff --git a/src/Discord.Net.Commands/Builders/CommandBuilder.cs b/src/Discord.Net.Commands/Builders/CommandBuilder.cs index c13ca10d4..ff89b7559 100644 --- a/src/Discord.Net.Commands/Builders/CommandBuilder.cs +++ b/src/Discord.Net.Commands/Builders/CommandBuilder.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Threading.Tasks; using System.Collections.Generic; +using Microsoft.Extensions.DependencyInjection; namespace Discord.Commands.Builders { @@ -12,7 +13,7 @@ namespace Discord.Commands.Builders private readonly List _aliases; public ModuleBuilder Module { get; } - internal Func Callback { get; set; } + internal Func Callback { get; set; } public string Name { get; set; } public string Summary { get; set; } @@ -35,7 +36,7 @@ namespace Discord.Commands.Builders _aliases = new List(); } //User-defined - internal CommandBuilder(ModuleBuilder module, string primaryAlias, Func callback) + internal CommandBuilder(ModuleBuilder module, string primaryAlias, Func callback) : this(module) { Discord.Preconditions.NotNull(primaryAlias, nameof(primaryAlias)); diff --git a/src/Discord.Net.Commands/Builders/ModuleBuilder.cs b/src/Discord.Net.Commands/Builders/ModuleBuilder.cs index 45c0034f2..d79239057 100644 --- a/src/Discord.Net.Commands/Builders/ModuleBuilder.cs +++ b/src/Discord.Net.Commands/Builders/ModuleBuilder.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; namespace Discord.Commands.Builders { @@ -73,7 +74,7 @@ namespace Discord.Commands.Builders _preconditions.Add(precondition); return this; } - public ModuleBuilder AddCommand(string primaryAlias, Func callback, Action createFunc) + public ModuleBuilder AddCommand(string primaryAlias, Func callback, Action createFunc) { var builder = new CommandBuilder(this, primaryAlias, callback); createFunc(builder); diff --git a/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs index 25b6e034b..d8464ea72 100644 --- a/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs +++ b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs @@ -243,7 +243,7 @@ namespace Discord.Commands } //We dont have a cached type reader, create one - reader = ReflectionUtils.CreateObject(typeReaderType.GetTypeInfo(), service, DependencyMap.Empty); + reader = ReflectionUtils.CreateObject(typeReaderType.GetTypeInfo(), service, EmptyServiceProvider.Instance); service.AddTypeReader(paramType, reader); return reader; diff --git a/src/Discord.Net.Commands/CommandMatch.cs b/src/Discord.Net.Commands/CommandMatch.cs index 6e78b8509..04a2d040f 100644 --- a/src/Discord.Net.Commands/CommandMatch.cs +++ b/src/Discord.Net.Commands/CommandMatch.cs @@ -1,5 +1,7 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; namespace Discord.Commands { @@ -14,13 +16,13 @@ namespace Discord.Commands Alias = alias; } - public Task CheckPreconditionsAsync(ICommandContext context, IDependencyMap map = null) - => Command.CheckPreconditionsAsync(context, map); + public Task CheckPreconditionsAsync(ICommandContext context, IServiceProvider services = null) + => Command.CheckPreconditionsAsync(context, services); public Task ParseAsync(ICommandContext context, SearchResult searchResult, PreconditionResult? preconditionResult = null) => Command.ParseAsync(context, Alias.Length, searchResult, preconditionResult); - public Task ExecuteAsync(ICommandContext context, IEnumerable argList, IEnumerable paramList, IDependencyMap map) - => Command.ExecuteAsync(context, argList, paramList, map); - public Task ExecuteAsync(ICommandContext context, ParseResult parseResult, IDependencyMap map) - => Command.ExecuteAsync(context, parseResult, map); + public Task ExecuteAsync(ICommandContext context, IEnumerable argList, IEnumerable paramList, IServiceProvider services) + => Command.ExecuteAsync(context, argList, paramList, services); + public Task ExecuteAsync(ICommandContext context, ParseResult parseResult, IServiceProvider services) + => Command.ExecuteAsync(context, parseResult, services); } } diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index 945db33a8..bcfb54d96 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Reflection; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; namespace Discord.Commands { @@ -247,11 +248,11 @@ namespace Discord.Commands return SearchResult.FromError(CommandError.UnknownCommand, "Unknown command."); } - public Task ExecuteAsync(ICommandContext context, int argPos, IDependencyMap dependencyMap = null, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) - => ExecuteAsync(context, context.Message.Content.Substring(argPos), dependencyMap, multiMatchHandling); - public async Task ExecuteAsync(ICommandContext context, string input, IDependencyMap dependencyMap = null, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) + public Task ExecuteAsync(ICommandContext context, int argPos, IServiceProvider services = null, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) + => ExecuteAsync(context, context.Message.Content.Substring(argPos), services, multiMatchHandling); + public async Task ExecuteAsync(ICommandContext context, string input, IServiceProvider services = null, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) { - dependencyMap = dependencyMap ?? DependencyMap.Empty; + services = services ?? EmptyServiceProvider.Instance; var searchResult = Search(context, input); if (!searchResult.IsSuccess) @@ -260,7 +261,7 @@ namespace Discord.Commands var commands = searchResult.Commands; for (int i = 0; i < commands.Count; i++) { - var preconditionResult = await commands[i].CheckPreconditionsAsync(context, dependencyMap).ConfigureAwait(false); + var preconditionResult = await commands[i].CheckPreconditionsAsync(context, services).ConfigureAwait(false); if (!preconditionResult.IsSuccess) { if (commands.Count == 1) @@ -294,10 +295,19 @@ namespace Discord.Commands } } - return await commands[i].ExecuteAsync(context, parseResult, dependencyMap).ConfigureAwait(false); + return await commands[i].ExecuteAsync(context, parseResult, services).ConfigureAwait(false); } return SearchResult.FromError(CommandError.UnknownCommand, "This input does not match any overload."); } + + public ServiceCollection CreateServiceCollection() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton(this); + serviceCollection.AddSingleton(serviceCollection); + serviceCollection.AddSingleton(serviceCollection); + return serviceCollection; + } } } diff --git a/src/Discord.Net.Commands/Dependencies/DependencyMap.cs b/src/Discord.Net.Commands/Dependencies/DependencyMap.cs deleted file mode 100644 index 55092961a..000000000 --- a/src/Discord.Net.Commands/Dependencies/DependencyMap.cs +++ /dev/null @@ -1,102 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Discord.Commands -{ - public class DependencyMap : IDependencyMap - { - private static readonly Type[] _typeBlacklist = new[] { - typeof(IDependencyMap), - typeof(CommandService) - }; - - private Dictionary> map; - - public static DependencyMap Empty => new DependencyMap(); - - public DependencyMap() - { - map = new Dictionary>(); - } - - /// - public void Add(T obj) where T : class - => AddFactory(() => obj); - /// - public bool TryAdd(T obj) where T : class - => TryAddFactory(() => obj); - /// - public void AddTransient() where T : class, new() - => AddFactory(() => new T()); - /// - public bool TryAddTransient() where T : class, new() - => TryAddFactory(() => new T()); - /// - public void AddTransient() where TKey : class - where TImpl : class, TKey, new() - => AddFactory(() => new TImpl()); - public bool TryAddTransient() where TKey : class - where TImpl : class, TKey, new() - => TryAddFactory(() => new TImpl()); - - /// - public void AddFactory(Func factory) where T : class - { - if (!TryAddFactory(factory)) - throw new InvalidOperationException($"The dependency map already contains \"{typeof(T).FullName}\""); - } - /// - public bool TryAddFactory(Func factory) where T : class - { - var type = typeof(T); - if (_typeBlacklist.Contains(type) || map.ContainsKey(type)) - return false; - map.Add(type, factory); - return true; - } - - /// - public T Get() where T : class - { - return (T)Get(typeof(T)); - } - /// - public object Get(Type t) - { - object result; - if (!TryGet(t, out result)) - throw new KeyNotFoundException($"The dependency map does not contain \"{t.FullName}\""); - else - return result; - } - - /// - public bool TryGet(out T result) where T : class - { - object untypedResult; - if (TryGet(typeof(T), out untypedResult)) - { - result = (T)untypedResult; - return true; - } - else - { - result = default(T); - return false; - } - } - /// - public bool TryGet(Type t, out object result) - { - Func func; - if (map.TryGetValue(t, out func)) - { - result = func(); - return true; - } - result = null; - return false; - } - } -} diff --git a/src/Discord.Net.Commands/Dependencies/IDependencyMap.cs b/src/Discord.Net.Commands/Dependencies/IDependencyMap.cs deleted file mode 100644 index fa76709b6..000000000 --- a/src/Discord.Net.Commands/Dependencies/IDependencyMap.cs +++ /dev/null @@ -1,89 +0,0 @@ -using System; - -namespace Discord.Commands -{ - public interface IDependencyMap - { - /// - /// Add an instance of a service to be injected. - /// - /// The type of service. - /// The instance of a service. - void Add(T obj) where T : class; - /// - /// Tries to add an instance of a service to be injected. - /// - /// The type of service. - /// The instance of a service. - /// A bool, indicating if the service was successfully added to the DependencyMap. - bool TryAdd(T obj) where T : class; - /// - /// Add a service that will be injected by a new instance every time. - /// - /// The type of instance to inject. - void AddTransient() where T : class, new(); - /// - /// Tries to add a service that will be injected by a new instance every time. - /// - /// The type of instance to inject. - /// A bool, indicating if the service was successfully added to the DependencyMap. - bool TryAddTransient() where T : class, new(); - /// - /// Add a service that will be injected by a new instance every time. - /// - /// The type to look for when injecting. - /// The type to inject when injecting. - /// - /// map.AddTransient<IService, Service> - /// - void AddTransient() where TKey: class where TImpl : class, TKey, new(); - /// - /// Tries to add a service that will be injected by a new instance every time. - /// - /// The type to look for when injecting. - /// The type to inject when injecting. - /// A bool, indicating if the service was successfully added to the DependencyMap. - bool TryAddTransient() where TKey : class where TImpl : class, TKey, new(); - /// - /// Add a service that will be injected by a factory. - /// - /// The type to look for when injecting. - /// The factory that returns a type of this service. - void AddFactory(Func factory) where T : class; - /// - /// Tries to add a service that will be injected by a factory. - /// - /// The type to look for when injecting. - /// The factory that returns a type of this service. - /// A bool, indicating if the service was successfully added to the DependencyMap. - bool TryAddFactory(Func factory) where T : class; - - /// - /// Pull an object from the map. - /// - /// The type of service. - /// An instance of this service. - T Get() where T : class; - /// - /// Try to pull an object from the map. - /// - /// The type of service. - /// The instance of this service. - /// Whether or not this object could be found in the map. - bool TryGet(out T result) where T : class; - - /// - /// Pull an object from the map. - /// - /// The type of service. - /// An instance of this service. - object Get(Type t); - /// - /// Try to pull an object from the map. - /// - /// The type of service. - /// An instance of this service. - /// Whether or not this object could be found in the map. - bool TryGet(Type t, out object result); - } -} diff --git a/src/Discord.Net.Commands/Discord.Net.Commands.csproj b/src/Discord.Net.Commands/Discord.Net.Commands.csproj index 40f130d7b..a9cfc8e60 100644 --- a/src/Discord.Net.Commands/Discord.Net.Commands.csproj +++ b/src/Discord.Net.Commands/Discord.Net.Commands.csproj @@ -9,4 +9,7 @@ + + + \ No newline at end of file diff --git a/src/Discord.Net.Commands/EmptyServiceProvider.cs b/src/Discord.Net.Commands/EmptyServiceProvider.cs new file mode 100644 index 000000000..0bef3760e --- /dev/null +++ b/src/Discord.Net.Commands/EmptyServiceProvider.cs @@ -0,0 +1,11 @@ +using System; + +namespace Discord.Commands +{ + internal class EmptyServiceProvider : IServiceProvider + { + public static readonly EmptyServiceProvider Instance = new EmptyServiceProvider(); + + public object GetService(Type serviceType) => null; + } +} diff --git a/src/Discord.Net.Commands/Info/CommandInfo.cs b/src/Discord.Net.Commands/Info/CommandInfo.cs index 9abe6de32..5acd1f648 100644 --- a/src/Discord.Net.Commands/Info/CommandInfo.cs +++ b/src/Discord.Net.Commands/Info/CommandInfo.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Reflection; using System.Runtime.ExceptionServices; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; namespace Discord.Commands { @@ -17,7 +18,7 @@ namespace Discord.Commands private static readonly System.Reflection.MethodInfo _convertParamsMethod = typeof(CommandInfo).GetTypeInfo().GetDeclaredMethod(nameof(ConvertParamsList)); private static readonly ConcurrentDictionary, object>> _arrayConverters = new ConcurrentDictionary, object>>(); - private readonly Func _action; + private readonly Func _action; public ModuleInfo Module { get; } public string Name { get; } @@ -63,21 +64,20 @@ namespace Discord.Commands _action = builder.Callback; } - public async Task CheckPreconditionsAsync(ICommandContext context, IDependencyMap map = null) + public async Task CheckPreconditionsAsync(ICommandContext context, IServiceProvider services = null) { - if (map == null) - map = DependencyMap.Empty; + services = services ?? EmptyServiceProvider.Instance; foreach (PreconditionAttribute precondition in Module.Preconditions) { - var result = await precondition.CheckPermissions(context, this, map).ConfigureAwait(false); + var result = await precondition.CheckPermissions(context, this, services).ConfigureAwait(false); if (!result.IsSuccess) return result; } foreach (PreconditionAttribute precondition in Preconditions) { - var result = await precondition.CheckPermissions(context, this, map).ConfigureAwait(false); + var result = await precondition.CheckPermissions(context, this, services).ConfigureAwait(false); if (!result.IsSuccess) return result; } @@ -96,7 +96,7 @@ namespace Discord.Commands return await CommandParser.ParseArgs(this, context, input, 0).ConfigureAwait(false); } - public Task ExecuteAsync(ICommandContext context, ParseResult parseResult, IDependencyMap map) + public Task ExecuteAsync(ICommandContext context, ParseResult parseResult, IServiceProvider services) { if (!parseResult.IsSuccess) return Task.FromResult(ExecuteResult.FromError(parseResult)); @@ -117,12 +117,11 @@ namespace Discord.Commands paramList[i] = parseResult.ParamValues[i].Values.First().Value; } - return ExecuteAsync(context, argList, paramList, map); + return ExecuteAsync(context, argList, paramList, services); } - public async Task ExecuteAsync(ICommandContext context, IEnumerable argList, IEnumerable paramList, IDependencyMap map) + public async Task ExecuteAsync(ICommandContext context, IEnumerable argList, IEnumerable paramList, IServiceProvider services) { - if (map == null) - map = DependencyMap.Empty; + services = services ?? EmptyServiceProvider.Instance; try { @@ -132,7 +131,7 @@ namespace Discord.Commands { var parameter = Parameters[position]; var argument = args[position]; - var result = await parameter.CheckPreconditionsAsync(context, argument, map).ConfigureAwait(false); + var result = await parameter.CheckPreconditionsAsync(context, argument, services).ConfigureAwait(false); if (!result.IsSuccess) return ExecuteResult.FromError(result); } @@ -140,12 +139,12 @@ namespace Discord.Commands switch (RunMode) { case RunMode.Sync: //Always sync - await ExecuteAsyncInternal(context, args, map).ConfigureAwait(false); + await ExecuteAsyncInternal(context, args, services).ConfigureAwait(false); break; case RunMode.Async: //Always async var t2 = Task.Run(async () => { - await ExecuteAsyncInternal(context, args, map).ConfigureAwait(false); + await ExecuteAsyncInternal(context, args, services).ConfigureAwait(false); }); break; } @@ -157,12 +156,12 @@ namespace Discord.Commands } } - private async Task ExecuteAsyncInternal(ICommandContext context, object[] args, IDependencyMap map) + private async Task ExecuteAsyncInternal(ICommandContext context, object[] args, IServiceProvider services) { await Module.Service._cmdLogger.DebugAsync($"Executing {GetLogText(context)}").ConfigureAwait(false); try { - await _action(context, args, map).ConfigureAwait(false); + await _action(context, args, services).ConfigureAwait(false); } catch (Exception ex) { diff --git a/src/Discord.Net.Commands/Info/ParameterInfo.cs b/src/Discord.Net.Commands/Info/ParameterInfo.cs index 4ef145b9e..9eea82cb2 100644 --- a/src/Discord.Net.Commands/Info/ParameterInfo.cs +++ b/src/Discord.Net.Commands/Info/ParameterInfo.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; namespace Discord.Commands { @@ -39,14 +40,13 @@ namespace Discord.Commands _reader = builder.TypeReader; } - public async Task CheckPreconditionsAsync(ICommandContext context, object arg, IDependencyMap map = null) + public async Task CheckPreconditionsAsync(ICommandContext context, object arg, IServiceProvider services = null) { - if (map == null) - map = DependencyMap.Empty; + services = EmptyServiceProvider.Instance; foreach (var precondition in Preconditions) { - var result = await precondition.CheckPermissions(context, this, arg, map).ConfigureAwait(false); + var result = await precondition.CheckPermissions(context, this, arg, services).ConfigureAwait(false); if (!result.IsSuccess) return result; } diff --git a/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs b/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs index 5c817183b..4cca0e864 100644 --- a/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs +++ b/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs @@ -2,88 +2,79 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using Microsoft.Extensions.DependencyInjection; namespace Discord.Commands { internal static class ReflectionUtils { - private static readonly TypeInfo objectTypeInfo = typeof(object).GetTypeInfo(); + private static readonly TypeInfo _objectTypeInfo = typeof(object).GetTypeInfo(); - internal static T CreateObject(TypeInfo typeInfo, CommandService service, IDependencyMap map = null) - => CreateBuilder(typeInfo, service)(map); + internal static T CreateObject(TypeInfo typeInfo, CommandService commands, IServiceProvider services = null) + => CreateBuilder(typeInfo, commands)(services); + internal static Func CreateBuilder(TypeInfo typeInfo, CommandService commands) + { + var constructor = GetConstructor(typeInfo); + var parameters = constructor.GetParameters(); + var properties = GetProperties(typeInfo); + + return (services) => + { + var args = new object[parameters.Length]; + for (int i = 0; i < parameters.Length; i++) + args[i] = GetMember(commands, services, parameters[i].ParameterType, typeInfo); + var obj = InvokeConstructor(constructor, args, typeInfo); - private static System.Reflection.PropertyInfo[] GetProperties(TypeInfo typeInfo) + foreach(var property in properties) + property.SetValue(obj, GetMember(commands, services, property.PropertyType, typeInfo)); + return obj; + }; + } + private static T InvokeConstructor(ConstructorInfo constructor, object[] args, TypeInfo ownerType) { - var result = new List(); - while (typeInfo != objectTypeInfo) + try { - foreach (var prop in typeInfo.DeclaredProperties) - { - if (prop.SetMethod?.IsPublic == true && prop.GetCustomAttribute() == null) - result.Add(prop); - } - typeInfo = typeInfo.BaseType.GetTypeInfo(); + return (T)constructor.Invoke(args); + } + catch (Exception ex) + { + throw new Exception($"Failed to create \"{ownerType.FullName}\"", ex); } - return result.ToArray(); } - internal static Func CreateBuilder(TypeInfo typeInfo, CommandService service) + private static ConstructorInfo GetConstructor(TypeInfo ownerType) { - var constructors = typeInfo.DeclaredConstructors.Where(x => !x.IsStatic).ToArray(); + var constructors = ownerType.DeclaredConstructors.Where(x => !x.IsStatic).ToArray(); if (constructors.Length == 0) - throw new InvalidOperationException($"No constructor found for \"{typeInfo.FullName}\""); + throw new InvalidOperationException($"No constructor found for \"{ownerType.FullName}\""); else if (constructors.Length > 1) - throw new InvalidOperationException($"Multiple constructors found for \"{typeInfo.FullName}\""); - - var constructor = constructors[0]; - System.Reflection.ParameterInfo[] parameters = constructor.GetParameters(); - System.Reflection.PropertyInfo[] properties = GetProperties(typeInfo) - .Where(p => p.SetMethod?.IsPublic == true && p.GetCustomAttribute() == null) - .ToArray(); - - return (map) => + throw new InvalidOperationException($"Multiple constructors found for \"{ownerType.FullName}\""); + return constructors[0]; + } + private static System.Reflection.PropertyInfo[] GetProperties(TypeInfo ownerType) + { + var result = new List(); + while (ownerType != _objectTypeInfo) { - object[] args = new object[parameters.Length]; - - for (int i = 0; i < parameters.Length; i++) - { - var parameter = parameters[i]; - args[i] = GetMember(parameter.ParameterType, map, service, typeInfo); - } - - T obj; - try - { - obj = (T)constructor.Invoke(args); - } - catch (Exception ex) - { - throw new Exception($"Failed to create \"{typeInfo.FullName}\"", ex); - } - - foreach(var property in properties) + foreach (var prop in ownerType.DeclaredProperties) { - property.SetValue(obj, GetMember(property.PropertyType, map, service, typeInfo)); + if (prop.SetMethod?.IsPublic == true && prop.GetCustomAttribute() == null) + result.Add(prop); } - return obj; - }; + ownerType = ownerType.BaseType.GetTypeInfo(); + } + return result.ToArray(); } - - private static readonly TypeInfo _dependencyTypeInfo = typeof(IDependencyMap).GetTypeInfo(); - - internal static object GetMember(Type targetType, IDependencyMap map, CommandService service, TypeInfo baseType) + private static object GetMember(CommandService commands, IServiceProvider services, Type memberType, TypeInfo ownerType) { - object arg; - if (map == null || !map.TryGet(targetType, out arg)) - { - if (targetType == typeof(CommandService)) - arg = service; - else if (targetType == typeof(IDependencyMap) || targetType == map.GetType()) - arg = map; - else - throw new InvalidOperationException($"Failed to create \"{baseType.FullName}\", dependency \"{targetType.Name}\" was not found."); - } - return arg; + if (memberType == typeof(CommandService)) + return commands; + if (memberType == typeof(IServiceProvider) || memberType == services.GetType()) + return services; + var service = services?.GetService(memberType); + if (service != null) + return service; + throw new InvalidOperationException($"Failed to create \"{ownerType.FullName}\", dependency \"{memberType.Name}\" was not found."); } } } From 576a52cdc6c8573d886cf81942fef55eaca8872a Mon Sep 17 00:00:00 2001 From: Christopher F Date: Thu, 4 May 2017 11:52:48 -0400 Subject: [PATCH 168/243] Restructure and replace emojis with a new emote system (#619) --- src/Discord.Net.Core/Entities/Emotes/Emoji.cs | 23 +++++++ src/Discord.Net.Core/Entities/Emotes/Emote.cs | 63 +++++++++++++++++++ .../GuildEmoji.cs => Emotes/GuildEmote.cs} | 11 ++-- .../Entities/Emotes/IEmote.cs | 13 ++++ .../Entities/Guilds/IGuild.cs | 2 +- .../Entities/Messages/Emoji.cs | 51 --------------- .../Entities/Messages/IReaction.cs | 2 +- .../Entities/Messages/IUserMessage.cs | 10 +-- src/Discord.Net.Core/Utils/MentionUtils.cs | 2 +- .../Entities/Guilds/RestGuild.cs | 16 ++--- .../Entities/Messages/MessageHelper.cs | 17 ++--- .../Entities/Messages/RestReaction.cs | 13 ++-- .../Entities/Messages/RestUserMessage.cs | 18 ++---- .../Extensions/EntityExtensions.cs | 4 +- .../Entities/Messages/RpcUserMessage.cs | 16 ++--- .../Entities/Guilds/SocketGuild.cs | 18 +++--- .../Entities/Messages/SocketReaction.cs | 13 ++-- .../Entities/Messages/SocketUserMessage.cs | 16 ++--- 18 files changed, 169 insertions(+), 139 deletions(-) create mode 100644 src/Discord.Net.Core/Entities/Emotes/Emoji.cs create mode 100644 src/Discord.Net.Core/Entities/Emotes/Emote.cs rename src/Discord.Net.Core/Entities/{Guilds/GuildEmoji.cs => Emotes/GuildEmote.cs} (66%) create mode 100644 src/Discord.Net.Core/Entities/Emotes/IEmote.cs delete mode 100644 src/Discord.Net.Core/Entities/Messages/Emoji.cs diff --git a/src/Discord.Net.Core/Entities/Emotes/Emoji.cs b/src/Discord.Net.Core/Entities/Emotes/Emoji.cs new file mode 100644 index 000000000..96226c715 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Emotes/Emoji.cs @@ -0,0 +1,23 @@ +namespace Discord +{ + /// + /// A unicode emoji + /// + public class Emoji : IEmote + { + // TODO: need to constrain this to unicode-only emojis somehow + /// + /// Creates a unciode emoji. + /// + /// The pure UTF-8 encoding of an emoji + public Emoji(string unicode) + { + Name = unicode; + } + + /// + /// The unicode representation of this emote. + /// + public string Name { get; } + } +} diff --git a/src/Discord.Net.Core/Entities/Emotes/Emote.cs b/src/Discord.Net.Core/Entities/Emotes/Emote.cs new file mode 100644 index 000000000..b1ca272eb --- /dev/null +++ b/src/Discord.Net.Core/Entities/Emotes/Emote.cs @@ -0,0 +1,63 @@ +using System; +using System.Globalization; + +namespace Discord +{ + /// + /// A custom image-based emote + /// + public class Emote : IEmote, ISnowflakeEntity + { + /// + /// The display name (tooltip) of this emote + /// + public string Name { get; } + /// + /// The ID of this emote + /// + public ulong Id { get; } + public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); + public string Url => CDN.GetEmojiUrl(Id); + + internal Emote(ulong id, string name) + { + Id = id; + Name = name; + } + + /// + /// Parse an Emote from its raw format + /// + /// The raw encoding of an emote; for example, <:dab:277855270321782784> + /// An emote + public static Emote Parse(string text) + { + if (TryParse(text, out Emote result)) + return result; + throw new ArgumentException("Invalid emote format", nameof(text)); + } + + public static bool TryParse(string text, out Emote result) + { + result = null; + if (text.Length >= 4 && text[0] == '<' && text[1] == ':' && text[text.Length - 1] == '>') + { + int splitIndex = text.IndexOf(':', 2); + if (splitIndex == -1) + return false; + + if (!ulong.TryParse(text.Substring(splitIndex + 1, text.Length - splitIndex - 2), NumberStyles.None, CultureInfo.InvariantCulture, out ulong id)) + return false; + + string name = text.Substring(2, splitIndex - 2); + result = new Emote(id, name); + return true; + } + return false; + + } + + private string DebuggerDisplay => $"{Name} ({Id})"; + public override string ToString() => Name; + } +} diff --git a/src/Discord.Net.Core/Entities/Guilds/GuildEmoji.cs b/src/Discord.Net.Core/Entities/Emotes/GuildEmote.cs similarity index 66% rename from src/Discord.Net.Core/Entities/Guilds/GuildEmoji.cs rename to src/Discord.Net.Core/Entities/Emotes/GuildEmote.cs index e925991eb..e883c707e 100644 --- a/src/Discord.Net.Core/Entities/Guilds/GuildEmoji.cs +++ b/src/Discord.Net.Core/Entities/Emotes/GuildEmote.cs @@ -3,19 +3,18 @@ using System.Diagnostics; namespace Discord { + /// + /// An image-based emote that is attached to a guild + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public struct GuildEmoji + public class GuildEmote : Emote { - public ulong Id { get; } - public string Name { get; } public bool IsManaged { get; } public bool RequireColons { get; } public IReadOnlyList RoleIds { get; } - internal GuildEmoji(ulong id, string name, bool isManaged, bool requireColons, IReadOnlyList roleIds) + internal GuildEmote(ulong id, string name, bool isManaged, bool requireColons, IReadOnlyList roleIds) : base(id, name) { - Id = id; - Name = name; IsManaged = isManaged; RequireColons = requireColons; RoleIds = roleIds; diff --git a/src/Discord.Net.Core/Entities/Emotes/IEmote.cs b/src/Discord.Net.Core/Entities/Emotes/IEmote.cs new file mode 100644 index 000000000..fac61402a --- /dev/null +++ b/src/Discord.Net.Core/Entities/Emotes/IEmote.cs @@ -0,0 +1,13 @@ +namespace Discord +{ + /// + /// A general container for any type of emote in a message. + /// + public interface IEmote + { + /// + /// The display name or unicode representation of this emote + /// + string Name { get; } + } +} diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs index 8da731855..506cbd3e4 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs @@ -45,7 +45,7 @@ namespace Discord /// Gets the built-in role containing all users in this guild. IRole EveryoneRole { get; } /// Gets a collection of all custom emojis for this guild. - IReadOnlyCollection Emojis { get; } + IReadOnlyCollection Emotes { get; } /// Gets a collection of all extra features added to this guild. IReadOnlyCollection Features { get; } /// Gets a collection of all roles in this guild. diff --git a/src/Discord.Net.Core/Entities/Messages/Emoji.cs b/src/Discord.Net.Core/Entities/Messages/Emoji.cs deleted file mode 100644 index a9c5a6bbd..000000000 --- a/src/Discord.Net.Core/Entities/Messages/Emoji.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using System.Diagnostics; -using System.Globalization; - -namespace Discord -{ - [DebuggerDisplay("{DebuggerDisplay,nq}")] - public struct Emoji - { - public ulong? Id { get; } - public string Name { get; } - - public string Url => Id != null ? CDN.GetEmojiUrl(Id.Value) : null; - - internal Emoji(ulong? id, string name) - { - Id = id; - Name = name; - } - - public static Emoji Parse(string text) - { - if (TryParse(text, out Emoji result)) - return result; - throw new ArgumentException("Invalid emoji format", nameof(text)); - } - - public static bool TryParse(string text, out Emoji result) - { - result = default(Emoji); - if (text.Length >= 4 && text[0] == '<' && text[1] == ':' && text[text.Length - 1] == '>') - { - int splitIndex = text.IndexOf(':', 2); - if (splitIndex == -1) - return false; - - if (!ulong.TryParse(text.Substring(splitIndex + 1, text.Length - splitIndex - 2), NumberStyles.None, CultureInfo.InvariantCulture, out ulong id)) - return false; - - string name = text.Substring(2, splitIndex - 2); - result = new Emoji(id, name); - return true; - } - return false; - - } - - private string DebuggerDisplay => $"{Name} ({Id})"; - public override string ToString() => Name; - } -} diff --git a/src/Discord.Net.Core/Entities/Messages/IReaction.cs b/src/Discord.Net.Core/Entities/Messages/IReaction.cs index 7145fce7f..37ead42ae 100644 --- a/src/Discord.Net.Core/Entities/Messages/IReaction.cs +++ b/src/Discord.Net.Core/Entities/Messages/IReaction.cs @@ -2,6 +2,6 @@ { public interface IReaction { - Emoji Emoji { get; } + IEmote Emote { get; } } } diff --git a/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs b/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs index f238ca6bc..61f908394 100644 --- a/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs +++ b/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs @@ -14,16 +14,12 @@ namespace Discord Task UnpinAsync(RequestOptions options = null); /// Returns all reactions included in this message. - IReadOnlyDictionary Reactions { get; } + IReadOnlyDictionary Reactions { get; } /// Adds a reaction to this message. - Task AddReactionAsync(Emoji emoji, RequestOptions options = null); - /// Adds a reaction to this message. - Task AddReactionAsync(string emoji, RequestOptions options = null); + Task AddReactionAsync(IEmote emote, RequestOptions options = null); /// Removes a reaction from message. - Task RemoveReactionAsync(Emoji emoji, IUser user, RequestOptions options = null); - /// Removes a reaction from this message. - Task RemoveReactionAsync(string emoji, IUser user, RequestOptions options = null); + Task RemoveReactionAsync(IEmote emote, IUser user, RequestOptions options = null); /// Removes all reactions from this message. Task RemoveAllReactionsAsync(RequestOptions options = null); Task> GetReactionUsersAsync(string emoji, int limit = 100, ulong? afterUserId = null, RequestOptions options = null); diff --git a/src/Discord.Net.Core/Utils/MentionUtils.cs b/src/Discord.Net.Core/Utils/MentionUtils.cs index 2af254fde..6c69827b4 100644 --- a/src/Discord.Net.Core/Utils/MentionUtils.cs +++ b/src/Discord.Net.Core/Utils/MentionUtils.cs @@ -252,7 +252,7 @@ namespace Discord { if (mode != TagHandling.Remove) { - Emoji emoji = (Emoji)tag.Value; + Emote emoji = (Emote)tag.Value; //Remove if its name contains any bad chars (prevents a few tag exploits) for (int i = 0; i < emoji.Name.Length; i++) diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs index 5a47fce81..8b5598ffe 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -3,10 +3,10 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; +using System.Linq; using System.Threading.Tasks; -using Model = Discord.API.Guild; using EmbedModel = Discord.API.GuildEmbed; -using System.Linq; +using Model = Discord.API.Guild; namespace Discord.Rest { @@ -14,7 +14,7 @@ namespace Discord.Rest public class RestGuild : RestEntity, IGuild, IUpdateable { private ImmutableDictionary _roles; - private ImmutableArray _emojis; + private ImmutableArray _emotes; private ImmutableArray _features; public string Name { get; private set; } @@ -39,7 +39,7 @@ namespace Discord.Rest public RestRole EveryoneRole => GetRole(Id); public IReadOnlyCollection Roles => _roles.ToReadOnlyCollection(); - public IReadOnlyCollection Emojis => _emojis; + public IReadOnlyCollection Emotes => _emotes; public IReadOnlyCollection Features => _features; internal RestGuild(BaseDiscordClient client, ulong id) @@ -69,13 +69,13 @@ namespace Discord.Rest if (model.Emojis != null) { - var emojis = ImmutableArray.CreateBuilder(model.Emojis.Length); + var emotes = ImmutableArray.CreateBuilder(model.Emojis.Length); for (int i = 0; i < model.Emojis.Length; i++) - emojis.Add(model.Emojis[i].ToEntity()); - _emojis = emojis.ToImmutableArray(); + emotes.Add(model.Emojis[i].ToEntity()); + _emotes = emotes.ToImmutableArray(); } else - _emojis = ImmutableArray.Create(); + _emotes = ImmutableArray.Create(); if (model.Features != null) _features = model.Features.ToImmutableArray(); diff --git a/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs b/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs index 285cf0e74..ccb683d1f 100644 --- a/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs +++ b/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs @@ -28,19 +28,14 @@ namespace Discord.Rest await client.ApiClient.DeleteMessageAsync(msg.Channel.Id, msg.Id, options).ConfigureAwait(false); } - public static async Task AddReactionAsync(IMessage msg, Emoji emoji, BaseDiscordClient client, RequestOptions options) - => await AddReactionAsync(msg, $"{emoji.Name}:{emoji.Id}", client, options).ConfigureAwait(false); - public static async Task AddReactionAsync(IMessage msg, string emoji, BaseDiscordClient client, RequestOptions options) + public static async Task AddReactionAsync(IMessage msg, IEmote emote, BaseDiscordClient client, RequestOptions options) { - await client.ApiClient.AddReactionAsync(msg.Channel.Id, msg.Id, emoji, options).ConfigureAwait(false); + await client.ApiClient.AddReactionAsync(msg.Channel.Id, msg.Id, emote is Emote e ? $"{e.Name}:{e.Id}" : emote.Name, options).ConfigureAwait(false); } - public static async Task RemoveReactionAsync(IMessage msg, IUser user, Emoji emoji, BaseDiscordClient client, RequestOptions options) - => await RemoveReactionAsync(msg, user, emoji.Id == null ? emoji.Name : $"{emoji.Name}:{emoji.Id}", client, options).ConfigureAwait(false); - public static async Task RemoveReactionAsync(IMessage msg, IUser user, string emoji, BaseDiscordClient client, - RequestOptions options) + public static async Task RemoveReactionAsync(IMessage msg, IUser user, IEmote emote, BaseDiscordClient client, RequestOptions options) { - await client.ApiClient.RemoveReactionAsync(msg.Channel.Id, msg.Id, user.Id, emoji, options).ConfigureAwait(false); + await client.ApiClient.RemoveReactionAsync(msg.Channel.Id, msg.Id, user.Id, emote is Emote e ? $"{e.Name}:{e.Id}" : emote.Name, options).ConfigureAwait(false); } public static async Task RemoveAllReactionsAsync(IMessage msg, BaseDiscordClient client, RequestOptions options) @@ -109,8 +104,8 @@ namespace Discord.Rest mentionedRole = guild.GetRole(id); tags.Add(new Tag(TagType.RoleMention, index, content.Length, id, mentionedRole)); } - else if (Emoji.TryParse(content, out var emoji)) - tags.Add(new Tag(TagType.Emoji, index, content.Length, emoji.Id ?? 0, emoji)); + else if (Emote.TryParse(content, out var emoji)) + tags.Add(new Tag(TagType.Emoji, index, content.Length, emoji.Id, emoji)); else //Bad Tag { index = index + 1; diff --git a/src/Discord.Net.Rest/Entities/Messages/RestReaction.cs b/src/Discord.Net.Rest/Entities/Messages/RestReaction.cs index 933833d56..05c817935 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestReaction.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestReaction.cs @@ -4,19 +4,24 @@ namespace Discord.Rest { public class RestReaction : IReaction { - public Emoji Emoji { get; } + public IEmote Emote { get; } public int Count { get; } public bool Me { get; } - internal RestReaction(Emoji emoji, int count, bool me) + internal RestReaction(IEmote emote, int count, bool me) { - Emoji = emoji; + Emote = emote; Count = count; Me = me; } internal static RestReaction Create(Model model) { - return new RestReaction(new Emoji(model.Emoji.Id, model.Emoji.Name), model.Count, model.Me); + IEmote emote; + if (model.Emoji.Id.HasValue) + emote = new Emote(model.Emoji.Id.Value, model.Emoji.Name); + else + emote = new Emoji(model.Emoji.Name); + return new RestReaction(emote, model.Count, model.Me); } } } diff --git a/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs b/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs index 00ab0c299..c79c67b38 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs @@ -27,7 +27,7 @@ namespace Discord.Rest public override IReadOnlyCollection MentionedRoleIds => MessageHelper.FilterTagsByKey(TagType.RoleMention, _tags); public override IReadOnlyCollection MentionedUsers => MessageHelper.FilterTagsByValue(TagType.UserMention, _tags); public override IReadOnlyCollection Tags => _tags; - public IReadOnlyDictionary Reactions => _reactions.ToDictionary(x => x.Emoji, x => new ReactionMetadata { ReactionCount = x.Count, IsMe = x.Me }); + public IReadOnlyDictionary Reactions => _reactions.ToDictionary(x => x.Emote, x => new ReactionMetadata { ReactionCount = x.Count, IsMe = x.Me }); internal RestUserMessage(BaseDiscordClient discord, ulong id, IMessageChannel channel, IUser author, MessageSource source) : base(discord, id, channel, author, source) @@ -130,21 +130,15 @@ namespace Discord.Rest Update(model); } - public Task AddReactionAsync(Emoji emoji, RequestOptions options = null) - => MessageHelper.AddReactionAsync(this, emoji, Discord, options); - public Task AddReactionAsync(string emoji, RequestOptions options = null) - => MessageHelper.AddReactionAsync(this, emoji, Discord, options); - - public Task RemoveReactionAsync(Emoji emoji, IUser user, RequestOptions options = null) - => MessageHelper.RemoveReactionAsync(this, user, emoji, Discord, options); - public Task RemoveReactionAsync(string emoji, IUser user, RequestOptions options = null) - => MessageHelper.RemoveReactionAsync(this, user, emoji, Discord, options); - + public Task AddReactionAsync(IEmote emote, RequestOptions options = null) + => MessageHelper.AddReactionAsync(this, emote, Discord, options); + public Task RemoveReactionAsync(IEmote emote, IUser user, RequestOptions options = null) + => MessageHelper.RemoveReactionAsync(this, user, emote, Discord, options); public Task RemoveAllReactionsAsync(RequestOptions options = null) => MessageHelper.RemoveAllReactionsAsync(this, Discord, options); public Task> GetReactionUsersAsync(string emoji, int limit = 100, ulong? afterUserId = null, RequestOptions options = null) - => MessageHelper.GetReactionUsersAsync(this, emoji, x => { x.Limit = limit; x.AfterUserId = afterUserId.HasValue ? afterUserId.Value : Optional.Create(); }, Discord, options); + => MessageHelper.GetReactionUsersAsync(this, emoji, x => { x.Limit = limit; x.AfterUserId = afterUserId ?? Optional.Create(); }, Discord, options); public Task PinAsync(RequestOptions options = null) diff --git a/src/Discord.Net.Rest/Extensions/EntityExtensions.cs b/src/Discord.Net.Rest/Extensions/EntityExtensions.cs index f59b8f7a3..b88a5b515 100644 --- a/src/Discord.Net.Rest/Extensions/EntityExtensions.cs +++ b/src/Discord.Net.Rest/Extensions/EntityExtensions.cs @@ -5,9 +5,9 @@ namespace Discord.Rest { internal static class EntityExtensions { - public static GuildEmoji ToEntity(this API.Emoji model) + public static GuildEmote ToEntity(this API.Emoji model) { - return new GuildEmoji(model.Id.Value, model.Name, model.Managed, model.RequireColons, ImmutableArray.Create(model.Roles)); + return new GuildEmote(model.Id.Value, model.Name, model.Managed, model.RequireColons, ImmutableArray.Create(model.Roles)); } public static Embed ToEntity(this API.Embed model) diff --git a/src/Discord.Net.Rpc/Entities/Messages/RpcUserMessage.cs b/src/Discord.Net.Rpc/Entities/Messages/RpcUserMessage.cs index cdcff4a07..91a8d7b31 100644 --- a/src/Discord.Net.Rpc/Entities/Messages/RpcUserMessage.cs +++ b/src/Discord.Net.Rpc/Entities/Messages/RpcUserMessage.cs @@ -29,7 +29,7 @@ namespace Discord.Rpc public override IReadOnlyCollection MentionedRoleIds => MessageHelper.FilterTagsByKey(TagType.RoleMention, _tags); public override IReadOnlyCollection MentionedUserIds => MessageHelper.FilterTagsByKey(TagType.UserMention, _tags); public override IReadOnlyCollection Tags => _tags; - public IReadOnlyDictionary Reactions => ImmutableDictionary.Create(); + public IReadOnlyDictionary Reactions => ImmutableDictionary.Create(); internal RpcUserMessage(DiscordRpcClient discord, ulong id, RestVirtualMessageChannel channel, RpcUser author, MessageSource source) : base(discord, id, channel, author, source) @@ -102,16 +102,10 @@ namespace Discord.Rpc public Task ModifyAsync(Action func, RequestOptions options) => MessageHelper.ModifyAsync(this, Discord, func, options); - public Task AddReactionAsync(Emoji emoji, RequestOptions options = null) - => MessageHelper.AddReactionAsync(this, emoji, Discord, options); - public Task AddReactionAsync(string emoji, RequestOptions options = null) - => MessageHelper.AddReactionAsync(this, emoji, Discord, options); - - public Task RemoveReactionAsync(Emoji emoji, IUser user, RequestOptions options = null) - => MessageHelper.RemoveReactionAsync(this, user, emoji, Discord, options); - public Task RemoveReactionAsync(string emoji, IUser user, RequestOptions options = null) - => MessageHelper.RemoveReactionAsync(this, user, emoji, Discord, options); - + public Task AddReactionAsync(IEmote emote, RequestOptions options = null) + => MessageHelper.AddReactionAsync(this, emote, Discord, options); + public Task RemoveReactionAsync(IEmote emote, IUser user, RequestOptions options = null) + => MessageHelper.RemoveReactionAsync(this, user, emote, Discord, options); public Task RemoveAllReactionsAsync(RequestOptions options = null) => MessageHelper.RemoveAllReactionsAsync(this, Discord, options); diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index 6535ecf9a..f396243d8 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -30,7 +30,7 @@ namespace Discord.WebSocket private ConcurrentDictionary _members; private ConcurrentDictionary _roles; private ConcurrentDictionary _voiceStates; - private ImmutableArray _emojis; + private ImmutableArray _emotes; private ImmutableArray _features; private AudioClient _audioClient; @@ -93,7 +93,7 @@ namespace Discord.WebSocket return channels.Select(x => state.GetChannel(x) as SocketGuildChannel).Where(x => x != null).ToReadOnlyCollection(channels); } } - public IReadOnlyCollection Emojis => _emojis; + public IReadOnlyCollection Emotes => _emotes; public IReadOnlyCollection Features => _features; public IReadOnlyCollection Users => _members.ToReadOnlyCollection(); public IReadOnlyCollection Roles => _roles.ToReadOnlyCollection(); @@ -102,7 +102,7 @@ namespace Discord.WebSocket : base(client, id) { _audioLock = new SemaphoreSlim(1, 1); - _emojis = ImmutableArray.Create(); + _emotes = ImmutableArray.Create(); _features = ImmutableArray.Create(); } internal static SocketGuild Create(DiscordSocketClient discord, ClientState state, ExtendedModel model) @@ -201,13 +201,13 @@ namespace Discord.WebSocket if (model.Emojis != null) { - var emojis = ImmutableArray.CreateBuilder(model.Emojis.Length); + var emojis = ImmutableArray.CreateBuilder(model.Emojis.Length); for (int i = 0; i < model.Emojis.Length; i++) emojis.Add(model.Emojis[i].ToEntity()); - _emojis = emojis.ToImmutable(); + _emotes = emojis.ToImmutable(); } else - _emojis = ImmutableArray.Create(); + _emotes = ImmutableArray.Create(); if (model.Features != null) _features = model.Features.ToImmutableArray(); @@ -253,10 +253,10 @@ namespace Discord.WebSocket internal void Update(ClientState state, EmojiUpdateModel model) { - var emojis = ImmutableArray.CreateBuilder(model.Emojis.Length); + var emotes = ImmutableArray.CreateBuilder(model.Emojis.Length); for (int i = 0; i < model.Emojis.Length; i++) - emojis.Add(model.Emojis[i].ToEntity()); - _emojis = emojis.ToImmutable(); + emotes.Add(model.Emojis[i].ToEntity()); + _emotes = emotes.ToImmutable(); } //General diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketReaction.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketReaction.cs index c12d0fdea..9f58f1cf6 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketReaction.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketReaction.cs @@ -9,20 +9,25 @@ namespace Discord.WebSocket public ulong MessageId { get; } public Optional Message { get; } public ISocketMessageChannel Channel { get; } - public Emoji Emoji { get; } + public IEmote Emote { get; } - internal SocketReaction(ISocketMessageChannel channel, ulong messageId, Optional message, ulong userId, Optional user, Emoji emoji) + internal SocketReaction(ISocketMessageChannel channel, ulong messageId, Optional message, ulong userId, Optional user, IEmote emoji) { Channel = channel; MessageId = messageId; Message = message; UserId = userId; User = user; - Emoji = emoji; + Emote = emoji; } internal static SocketReaction Create(Model model, ISocketMessageChannel channel, Optional message, Optional user) { - return new SocketReaction(channel, model.MessageId, message, model.UserId, user, new Emoji(model.Emoji.Id, model.Emoji.Name)); + IEmote emote; + if (model.Emoji.Id.HasValue) + emote = new Emote(model.Emoji.Id.Value, model.Emoji.Name); + else + emote = new Emoji(model.Emoji.Name); + return new SocketReaction(channel, model.MessageId, message, model.UserId, user, emote); } } } diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs index 8b9acf118..40588e55a 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs @@ -28,7 +28,7 @@ namespace Discord.WebSocket public override IReadOnlyCollection MentionedChannels => MessageHelper.FilterTagsByValue(TagType.ChannelMention, _tags); public override IReadOnlyCollection MentionedRoles => MessageHelper.FilterTagsByValue(TagType.RoleMention, _tags); public override IReadOnlyCollection MentionedUsers => MessageHelper.FilterTagsByValue(TagType.UserMention, _tags); - public IReadOnlyDictionary Reactions => _reactions.GroupBy(r => r.Emoji).ToDictionary(x => x.Key, x => new ReactionMetadata { ReactionCount = x.Count(), IsMe = x.Any(y => y.UserId == Discord.CurrentUser.Id) }); + public IReadOnlyDictionary Reactions => _reactions.GroupBy(r => r.Emote).ToDictionary(x => x.Key, x => new ReactionMetadata { ReactionCount = x.Count(), IsMe = x.Any(y => y.UserId == Discord.CurrentUser.Id) }); internal SocketUserMessage(DiscordSocketClient discord, ulong id, ISocketMessageChannel channel, SocketUser author, MessageSource source) : base(discord, id, channel, author, source) @@ -124,16 +124,10 @@ namespace Discord.WebSocket public Task ModifyAsync(Action func, RequestOptions options = null) => MessageHelper.ModifyAsync(this, Discord, func, options); - public Task AddReactionAsync(Emoji emoji, RequestOptions options = null) - => MessageHelper.AddReactionAsync(this, emoji, Discord, options); - public Task AddReactionAsync(string emoji, RequestOptions options = null) - => MessageHelper.AddReactionAsync(this, emoji, Discord, options); - - public Task RemoveReactionAsync(Emoji emoji, IUser user, RequestOptions options = null) - => MessageHelper.RemoveReactionAsync(this, user, emoji, Discord, options); - public Task RemoveReactionAsync(string emoji, IUser user, RequestOptions options = null) - => MessageHelper.RemoveReactionAsync(this, user, emoji, Discord, options); - + public Task AddReactionAsync(IEmote emote, RequestOptions options = null) + => MessageHelper.AddReactionAsync(this, emote, Discord, options); + public Task RemoveReactionAsync(IEmote emote, IUser user, RequestOptions options = null) + => MessageHelper.RemoveReactionAsync(this, user, emote, Discord, options); public Task RemoveAllReactionsAsync(RequestOptions options = null) => MessageHelper.RemoveAllReactionsAsync(this, Discord, options); From d189bb97489412d47b165f836abdbe182cf7d855 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Thu, 4 May 2017 11:53:40 -0400 Subject: [PATCH 169/243] Expose the 'fields' collection on EmbedBuilder (#603) * remove tip in docs about SocketEntity.Discord * Expose the 'Fields' collection on EmbedBuilder After some discussion I decided that there was really no reason to keep this private, and it didn't really go along with the rest of the design of the EmbedBuilder. This is NOT a breaking change. Exposing this property should not have any negative effects. * Don't allow EmbedBuilder's Fields to be set to null --- docs/guides/concepts/entities.md | 3 --- .../Entities/Messages/EmbedBuilder.cs | 27 ++++++++++++------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/docs/guides/concepts/entities.md b/docs/guides/concepts/entities.md index b753293bb..a38651829 100644 --- a/docs/guides/concepts/entities.md +++ b/docs/guides/concepts/entities.md @@ -35,9 +35,6 @@ you to easily navigate to an entity's parent or children. As explained above, you will sometimes need to cast to a more detailed version of an entity to navigate to its parent. -All socket entities have a `Discord` property, which will allow you -to access the parent `DiscordSocketClient`. - ### Accessing Entities The most basic forms of entities, `SocketGuild`, `SocketUser`, and diff --git a/src/Discord.Net.Rest/Entities/Messages/EmbedBuilder.cs b/src/Discord.Net.Rest/Entities/Messages/EmbedBuilder.cs index 74f1441e1..98a191379 100644 --- a/src/Discord.Net.Rest/Entities/Messages/EmbedBuilder.cs +++ b/src/Discord.Net.Rest/Entities/Messages/EmbedBuilder.cs @@ -7,12 +7,11 @@ namespace Discord public class EmbedBuilder { private readonly Embed _embed; - private readonly List _fields; public EmbedBuilder() { _embed = new Embed("rich"); - _fields = new List(); + Fields = new List(); } public string Title { get { return _embed.Title; } set { _embed.Title = value; } } @@ -25,6 +24,16 @@ namespace Discord public EmbedAuthorBuilder Author { get; set; } public EmbedFooterBuilder Footer { get; set; } + private List _fields; + public List Fields + { + get => _fields; + set + { + if (value != null) _fields = value; + else throw new ArgumentNullException("Cannot set an embed builder's fields collection to null", nameof(value)); + } + } public EmbedBuilder WithTitle(string title) { @@ -98,7 +107,7 @@ namespace Discord .WithIsInline(false) .WithName(name) .WithValue(value); - _fields.Add(field); + Fields.Add(field); return this; } public EmbedBuilder AddInlineField(string name, object value) @@ -107,19 +116,19 @@ namespace Discord .WithIsInline(true) .WithName(name) .WithValue(value); - _fields.Add(field); + Fields.Add(field); return this; } public EmbedBuilder AddField(EmbedFieldBuilder field) { - _fields.Add(field); + Fields.Add(field); return this; } public EmbedBuilder AddField(Action action) { var field = new EmbedFieldBuilder(); action(field); - _fields.Add(field); + Fields.Add(field); return this; } @@ -127,9 +136,9 @@ namespace Discord { _embed.Footer = Footer?.Build(); _embed.Author = Author?.Build(); - var fields = ImmutableArray.CreateBuilder(_fields.Count); - for (int i = 0; i < _fields.Count; i++) - fields.Add(_fields[i].Build()); + var fields = ImmutableArray.CreateBuilder(Fields.Count); + for (int i = 0; i < Fields.Count; i++) + fields.Add(Fields[i].Build()); _embed.Fields = fields.ToImmutable(); return _embed; } From bd5ec0a29afaddf07d65215f8cbcfdba4d637534 Mon Sep 17 00:00:00 2001 From: RogueException Date: Thu, 4 May 2017 13:09:46 -0300 Subject: [PATCH 170/243] Increment GlobalUser reference count on GuildUser creation --- src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index f396243d8..f32c6dccf 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -369,6 +369,7 @@ namespace Discord.WebSocket else { member = SocketGuildUser.Create(this, Discord.State, model); + member.GlobalUser.AddRef(); _members[member.Id] = member; DownloadedMemberCount++; } @@ -381,6 +382,7 @@ namespace Discord.WebSocket else { member = SocketGuildUser.Create(this, Discord.State, model); + member.GlobalUser.AddRef(); _members[member.Id] = member; DownloadedMemberCount++; } @@ -393,6 +395,7 @@ namespace Discord.WebSocket else { member = SocketGuildUser.Create(this, Discord.State, model); + member.GlobalUser.AddRef(); _members[member.Id] = member; DownloadedMemberCount++; } From 4a128b326b25f3cd034d7e76bf5a21efadd2d05d Mon Sep 17 00:00:00 2001 From: RogueException Date: Thu, 4 May 2017 13:14:35 -0300 Subject: [PATCH 171/243] Increment globaluser refs on private channel creation --- src/Discord.Net.WebSocket/ClientState.cs | 22 ++++++++++--------- .../Entities/Channels/SocketDMChannel.cs | 1 + .../Entities/Channels/SocketGroupChannel.cs | 1 + 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/Discord.Net.WebSocket/ClientState.cs b/src/Discord.Net.WebSocket/ClientState.cs index 8bc745211..f07976a0a 100644 --- a/src/Discord.Net.WebSocket/ClientState.cs +++ b/src/Discord.Net.WebSocket/ClientState.cs @@ -55,26 +55,28 @@ namespace Discord.WebSocket { _channels[channel.Id] = channel; - if (channel is SocketDMChannel dmChannel) - _dmChannels[dmChannel.Recipient.Id] = dmChannel; - else + switch (channel) { - if (channel is SocketGroupChannel groupChannel) + case SocketDMChannel dmChannel: + _dmChannels[dmChannel.Recipient.Id] = dmChannel; + break; + case SocketGroupChannel groupChannel: _groupChannels.TryAdd(groupChannel.Id); + break; } } internal SocketChannel RemoveChannel(ulong id) { if (_channels.TryRemove(id, out SocketChannel channel)) { - if (channel is SocketDMChannel dmChannel) + switch (channel) { - _dmChannels.TryRemove(dmChannel.Recipient.Id, out SocketDMChannel ignored); - } - else - { - if (channel is SocketGroupChannel groupChannel) + case SocketDMChannel dmChannel: + _dmChannels.TryRemove(dmChannel.Recipient.Id, out var ignored); + break; + case SocketGroupChannel groupChannel: _groupChannels.TryRemove(id); + break; } return channel; } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs index 4d79d1ab4..8e9272ff0 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs @@ -24,6 +24,7 @@ namespace Discord.WebSocket : base(discord, id) { Recipient = recipient; + recipient.GlobalUser.AddRef(); if (Discord.MessageCacheSize > 0) _messages = new MessageCache(Discord, this); } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs index bdf9dbc2b..d7d80214f 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs @@ -131,6 +131,7 @@ namespace Discord.WebSocket else { var privateUser = SocketGroupUser.Create(this, Discord.State, model); + privateUser.GlobalUser.AddRef(); _users[privateUser.Id] = privateUser; return privateUser; } From 870dc50a68218d2b528f70f2eb9e14034570bcb7 Mon Sep 17 00:00:00 2001 From: RogueException Date: Thu, 4 May 2017 13:15:51 -0300 Subject: [PATCH 172/243] Fixed RequireNsfwAttribute definition --- .../Attributes/Preconditions/RequireNsfwAttribute.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireNsfwAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireNsfwAttribute.cs index a52ec6ddd..94235b1ae 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireNsfwAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireNsfwAttribute.cs @@ -9,7 +9,7 @@ namespace Discord.Commands [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] public class RequireNsfwAttribute : PreconditionAttribute { - public override Task CheckPermissions(ICommandContext context, CommandInfo command, IDependencyMap map) + public override Task CheckPermissions(ICommandContext context, CommandInfo command, IServiceProvider services) { if (context.Channel.IsNsfw) return Task.FromResult(PreconditionResult.FromSuccess()); From 82a413ace68cb367cb7cd612611f0dc05f7b1b98 Mon Sep 17 00:00:00 2001 From: Sindre Langhus Date: Thu, 4 May 2017 18:16:33 +0200 Subject: [PATCH 173/243] Fix for empty user objects after GUILD_MEMBER_REMOVE (#641) * Made GetOrCreateUser always call AddRef and added check to PRESENCE_UPDATE to avoid readding users who have been removed from guilds * Removed AddRef as per dev guild discussion --- src/Discord.Net.WebSocket/DiscordSocketClient.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index 5e19e14e6..4476b78c4 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -1328,7 +1328,13 @@ namespace Discord.WebSocket var user = guild.GetUser(data.User.Id); if (user == null) + { + if (data.Status == UserStatus.Offline) + { + return; + } user = guild.AddOrUpdateUser(data); + } else { var globalBefore = user.GlobalUser.Clone(); From 8f59d4423fc1001520015d5ebe127b59eeb8cfb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20M=C3=B6ller?= Date: Thu, 4 May 2017 18:29:04 +0200 Subject: [PATCH 174/243] Fixed exemple calling old non existing function. --- docs/guides/commands/samples/command_handler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/commands/samples/command_handler.cs b/docs/guides/commands/samples/command_handler.cs index 71869415b..9adfcc71d 100644 --- a/docs/guides/commands/samples/command_handler.cs +++ b/docs/guides/commands/samples/command_handler.cs @@ -24,7 +24,7 @@ public class Program await InstallCommands(); await client.LoginAsync(TokenType.Bot, token); - await client.ConnectAsync(); + await client.StartAsync(); await Task.Delay(-1); } From 112a434424debf0d3bdd4bc440b68a4ea90b0e2f Mon Sep 17 00:00:00 2001 From: Finite Reality Date: Sat, 6 May 2017 06:39:46 +0100 Subject: [PATCH 175/243] Allow for case-insensitive HasStringPrefix (#644) This was :+1:'d in the dev chat, I forgot to make a PR for it (whoops!) --- src/Discord.Net.Commands/Extensions/MessageExtensions.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Discord.Net.Commands/Extensions/MessageExtensions.cs b/src/Discord.Net.Commands/Extensions/MessageExtensions.cs index 4354cbb88..096b03f6b 100644 --- a/src/Discord.Net.Commands/Extensions/MessageExtensions.cs +++ b/src/Discord.Net.Commands/Extensions/MessageExtensions.cs @@ -1,4 +1,6 @@ -namespace Discord.Commands +using System; + +namespace Discord.Commands { public static class MessageExtensions { @@ -12,10 +14,10 @@ } return false; } - public static bool HasStringPrefix(this IUserMessage msg, string str, ref int argPos) + public static bool HasStringPrefix(this IUserMessage msg, string str, ref int argPos, StringComparison comparisonType = StringComparison.Ordinal) { var text = msg.Content; - if (text.StartsWith(str)) + if (text.StartsWith(str, comparisonType)) { argPos = str.Length; return true; From a1a90ae46e02ec09d96896da83800a043404c4bf Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Sat, 6 May 2017 11:33:33 +0200 Subject: [PATCH 176/243] Update the example precondition to use IServiceProvider --- docs/guides/commands/samples/require_owner.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/guides/commands/samples/require_owner.cs b/docs/guides/commands/samples/require_owner.cs index 137446553..3611afab8 100644 --- a/docs/guides/commands/samples/require_owner.cs +++ b/docs/guides/commands/samples/require_owner.cs @@ -2,16 +2,18 @@ using Discord.Commands; using Discord.WebSocket; +using Microsoft.Extensions.DependencyInjection; +using System; using System.Threading.Tasks; // Inherit from PreconditionAttribute public class RequireOwnerAttribute : PreconditionAttribute { // Override the CheckPermissions method - public async override Task CheckPermissions(ICommandContext context, CommandInfo command, IDependencyMap map) + public async override Task CheckPermissions(ICommandContext context, CommandInfo command, IServiceProvider services) { // Get the ID of the bot's owner - var ownerId = (await map.Get().GetApplicationInfoAsync()).Owner.Id; + var ownerId = (await services.GetService().GetApplicationInfoAsync()).Owner.Id; // If this command was executed by that user, return a success if (context.User.Id == ownerId) return PreconditionResult.FromSuccess(); From 00895b122708ea7323e94c46b5b38d97ba94be38 Mon Sep 17 00:00:00 2001 From: FiniteReality Date: Sat, 6 May 2017 18:25:44 +0100 Subject: [PATCH 177/243] Remove CommandService.BuildServiceCollection :boom: --- src/Discord.Net.Commands/CommandService.cs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index bcfb54d96..f526e8f3b 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -300,14 +300,5 @@ namespace Discord.Commands return SearchResult.FromError(CommandError.UnknownCommand, "This input does not match any overload."); } - - public ServiceCollection CreateServiceCollection() - { - var serviceCollection = new ServiceCollection(); - serviceCollection.AddSingleton(this); - serviceCollection.AddSingleton(serviceCollection); - serviceCollection.AddSingleton(serviceCollection); - return serviceCollection; - } } } From 1e888cde24c42cfb86badf2216fe8579912317da Mon Sep 17 00:00:00 2001 From: FiniteReality Date: Mon, 8 May 2017 22:15:47 +0100 Subject: [PATCH 178/243] Fix CheckPreconditions giving empty service provider Parameter preconditions were always getting the empty service provider, even when a custom one was provided in ExecuteAsync, which means that preconditions which use services cannot work properly. --- src/Discord.Net.Commands/Info/ParameterInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.Commands/Info/ParameterInfo.cs b/src/Discord.Net.Commands/Info/ParameterInfo.cs index 9eea82cb2..2ecf26a9f 100644 --- a/src/Discord.Net.Commands/Info/ParameterInfo.cs +++ b/src/Discord.Net.Commands/Info/ParameterInfo.cs @@ -42,7 +42,7 @@ namespace Discord.Commands public async Task CheckPreconditionsAsync(ICommandContext context, object arg, IServiceProvider services = null) { - services = EmptyServiceProvider.Instance; + services = services ?? EmptyServiceProvider.Instance; foreach (var precondition in Preconditions) { From 285a0e5817ddcb9e2dca5c153558cfb0bb8299a3 Mon Sep 17 00:00:00 2001 From: RogueException Date: Tue, 9 May 2017 20:51:00 -0300 Subject: [PATCH 179/243] Updated deps, cleaned csprojs --- .../Discord.Net.Analyzers/Discord.Net.Analyzers.csproj | 4 ++-- experiment/Discord.Net.Relay/Discord.Net.Relay.csproj | 6 +++--- src/Discord.Net.Commands/Discord.Net.Commands.csproj | 4 ++-- src/Discord.Net.Core/Discord.Net.Core.csproj | 8 ++++---- src/Discord.Net.DebugTools/Discord.Net.DebugTools.csproj | 2 +- .../Discord.Net.Providers.WS4Net.csproj | 2 +- src/Discord.Net.Rest/Discord.Net.Rest.csproj | 5 ++++- src/Discord.Net.Rpc/Discord.Net.Rpc.csproj | 4 ++-- src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj | 4 ++-- src/Discord.Net.Webhook/Discord.Net.Webhook.csproj | 2 +- test/Discord.Net.Tests/Discord.Net.Tests.csproj | 4 ++-- 11 files changed, 24 insertions(+), 21 deletions(-) diff --git a/experiment/Discord.Net.Analyzers/Discord.Net.Analyzers.csproj b/experiment/Discord.Net.Analyzers/Discord.Net.Analyzers.csproj index 129da98fe..86541691d 100644 --- a/experiment/Discord.Net.Analyzers/Discord.Net.Analyzers.csproj +++ b/experiment/Discord.Net.Analyzers/Discord.Net.Analyzers.csproj @@ -1,4 +1,4 @@ - + Discord.Net.Analyzers @@ -11,7 +11,7 @@ - + all diff --git a/experiment/Discord.Net.Relay/Discord.Net.Relay.csproj b/experiment/Discord.Net.Relay/Discord.Net.Relay.csproj index 8942a9b28..2e9101600 100644 --- a/experiment/Discord.Net.Relay/Discord.Net.Relay.csproj +++ b/experiment/Discord.Net.Relay/Discord.Net.Relay.csproj @@ -1,4 +1,4 @@ - + Discord.Net.Relay @@ -12,7 +12,7 @@ - - + + \ No newline at end of file diff --git a/src/Discord.Net.Commands/Discord.Net.Commands.csproj b/src/Discord.Net.Commands/Discord.Net.Commands.csproj index a9cfc8e60..eaac79a55 100644 --- a/src/Discord.Net.Commands/Discord.Net.Commands.csproj +++ b/src/Discord.Net.Commands/Discord.Net.Commands.csproj @@ -1,4 +1,4 @@ - + Discord.Net.Commands @@ -10,6 +10,6 @@ - + \ No newline at end of file diff --git a/src/Discord.Net.Core/Discord.Net.Core.csproj b/src/Discord.Net.Core/Discord.Net.Core.csproj index 4f20dad3b..bce577ddd 100644 --- a/src/Discord.Net.Core/Discord.Net.Core.csproj +++ b/src/Discord.Net.Core/Discord.Net.Core.csproj @@ -1,4 +1,4 @@ - + Discord.Net.Core @@ -7,8 +7,8 @@ net45;netstandard1.1;netstandard1.3 - - - + + + \ No newline at end of file diff --git a/src/Discord.Net.DebugTools/Discord.Net.DebugTools.csproj b/src/Discord.Net.DebugTools/Discord.Net.DebugTools.csproj index 1a0d08c4d..43d627c99 100644 --- a/src/Discord.Net.DebugTools/Discord.Net.DebugTools.csproj +++ b/src/Discord.Net.DebugTools/Discord.Net.DebugTools.csproj @@ -1,4 +1,4 @@ - + Discord.Net.DebugTools diff --git a/src/Discord.Net.Providers.WS4Net/Discord.Net.Providers.WS4Net.csproj b/src/Discord.Net.Providers.WS4Net/Discord.Net.Providers.WS4Net.csproj index 5e52a1e5e..0115d91c0 100644 --- a/src/Discord.Net.Providers.WS4Net/Discord.Net.Providers.WS4Net.csproj +++ b/src/Discord.Net.Providers.WS4Net/Discord.Net.Providers.WS4Net.csproj @@ -1,4 +1,4 @@ - + Discord.Net.Providers.WS4Net diff --git a/src/Discord.Net.Rest/Discord.Net.Rest.csproj b/src/Discord.Net.Rest/Discord.Net.Rest.csproj index c8da70c6b..a463b2878 100644 --- a/src/Discord.Net.Rest/Discord.Net.Rest.csproj +++ b/src/Discord.Net.Rest/Discord.Net.Rest.csproj @@ -1,4 +1,4 @@ - + Discord.Net.Rest @@ -9,4 +9,7 @@ + + + \ No newline at end of file diff --git a/src/Discord.Net.Rpc/Discord.Net.Rpc.csproj b/src/Discord.Net.Rpc/Discord.Net.Rpc.csproj index 3561e6da5..5572d69c5 100644 --- a/src/Discord.Net.Rpc/Discord.Net.Rpc.csproj +++ b/src/Discord.Net.Rpc/Discord.Net.Rpc.csproj @@ -1,4 +1,4 @@ - + Discord.Net.Rpc @@ -19,6 +19,6 @@ - + \ No newline at end of file diff --git a/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj b/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj index 2b0dcf1fe..e4de43e51 100644 --- a/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj +++ b/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj @@ -1,4 +1,4 @@ - + Discord.Net.WebSocket @@ -12,6 +12,6 @@ - + \ No newline at end of file diff --git a/src/Discord.Net.Webhook/Discord.Net.Webhook.csproj b/src/Discord.Net.Webhook/Discord.Net.Webhook.csproj index 7246624e5..7c224e01e 100644 --- a/src/Discord.Net.Webhook/Discord.Net.Webhook.csproj +++ b/src/Discord.Net.Webhook/Discord.Net.Webhook.csproj @@ -1,4 +1,4 @@ - + Discord.Net.Webhook diff --git a/test/Discord.Net.Tests/Discord.Net.Tests.csproj b/test/Discord.Net.Tests/Discord.Net.Tests.csproj index 926699324..9e734641c 100644 --- a/test/Discord.Net.Tests/Discord.Net.Tests.csproj +++ b/test/Discord.Net.Tests/Discord.Net.Tests.csproj @@ -1,4 +1,4 @@ - + Exe Discord @@ -18,7 +18,7 @@ - + From feebcb48381b6cab13fa9fe2e530a0086636f19d Mon Sep 17 00:00:00 2001 From: RogueException Date: Tue, 9 May 2017 20:55:54 -0300 Subject: [PATCH 180/243] Update System.Net.Http to 4.3.2 --- src/Discord.Net.Rest/Discord.Net.Rest.csproj | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Discord.Net.Rest/Discord.Net.Rest.csproj b/src/Discord.Net.Rest/Discord.Net.Rest.csproj index a463b2878..439b7bbb1 100644 --- a/src/Discord.Net.Rest/Discord.Net.Rest.csproj +++ b/src/Discord.Net.Rest/Discord.Net.Rest.csproj @@ -9,6 +9,9 @@ + + + From 4c7fad78e1ff4f6701fcce87ba4e1bc6dce1cdfe Mon Sep 17 00:00:00 2001 From: RogueException Date: Wed, 10 May 2017 19:28:25 -0300 Subject: [PATCH 181/243] Build promises when guild is unavailable --- src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index f32c6dccf..5358605c8 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -126,6 +126,8 @@ namespace Discord.WebSocket _emojis = ImmutableArray.Create(); if (Features == null) _features = ImmutableArray.Create();*/ + _syncPromise = new TaskCompletionSource(); + _downloaderPromise = new TaskCompletionSource(); return; } From c01769ef4a679ef7b57c774f10f9004de6395b03 Mon Sep 17 00:00:00 2001 From: RogueException Date: Thu, 11 May 2017 00:03:38 -0300 Subject: [PATCH 182/243] Updated version to 1.0.0-rc3 --- Discord.Net.targets | 2 +- src/Discord.Net/Discord.Net.nuspec | 38 +++++++++++++++--------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/Discord.Net.targets b/Discord.Net.targets index 3bfa199c1..947819898 100644 --- a/Discord.Net.targets +++ b/Discord.Net.targets @@ -1,7 +1,7 @@ 1.0.0 - rc2 + rc3 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 a410294b1..2a637dbfb 100644 --- a/src/Discord.Net/Discord.Net.nuspec +++ b/src/Discord.Net/Discord.Net.nuspec @@ -2,7 +2,7 @@ Discord.Net - 1.0.0-rc2$suffix$ + 1.0.0-rc3$suffix$ Discord.Net RogueException RogueException @@ -13,28 +13,28 @@ false - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + From a92c27da3bb119691a2ba094cd6c79a0a2b5a264 Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Thu, 11 May 2017 18:01:39 +0200 Subject: [PATCH 183/243] Remove wrong parameter from FFMPEG audio example This parameter was samples per frame but changed to bitrate. (1920 is a way to low bitrate :) ) --- docs/guides/voice/samples/audio_ffmpeg.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/voice/samples/audio_ffmpeg.cs b/docs/guides/voice/samples/audio_ffmpeg.cs index 716ec3d6c..b9430ac11 100644 --- a/docs/guides/voice/samples/audio_ffmpeg.cs +++ b/docs/guides/voice/samples/audio_ffmpeg.cs @@ -3,7 +3,7 @@ private async Task SendAsync(IAudioClient client, string path) // Create FFmpeg using the previous example var ffmpeg = CreateStream(path); var output = ffmpeg.StandardOutput.BaseStream; - var discord = client.CreatePCMStream(AudioApplication.Mixed, 1920); + var discord = client.CreatePCMStream(AudioApplication.Mixed); await output.CopyToAsync(discord); await discord.FlushAsync(); } From af5fdec48696755d93d51f3aadb9284cc11bf03a Mon Sep 17 00:00:00 2001 From: Joe4evr Date: Sat, 13 May 2017 03:15:55 +0200 Subject: [PATCH 184/243] Update the quickstart structure to rc3. --- .../samples/intro/structure.cs | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/docs/guides/getting_started/samples/intro/structure.cs b/docs/guides/getting_started/samples/intro/structure.cs index 01dff7bc6..9e783bb9b 100644 --- a/docs/guides/getting_started/samples/intro/structure.cs +++ b/docs/guides/getting_started/samples/intro/structure.cs @@ -1,6 +1,7 @@ using System; using System.Reflection; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; using Discord; using Discord.Commands; using Discord.WebSocket; @@ -9,8 +10,8 @@ class Program { private readonly DiscordSocketClient _client; - // Keep the CommandService and IDependencyMap around for use with commands. - private readonly IDependencyMap _map = new DependencyMap(); + // Keep the CommandService and IServiceCollection around for use with commands. + private readonly IServiceCollection _map = new ServiceCollection(); private readonly CommandService _commands = new CommandService(); // Program entry point @@ -78,22 +79,29 @@ class Program // Login and connect. await _client.LoginAsync(TokenType.Bot, /* */); await _client.StartAsync(); - + // Wait infinitely so your bot actually stays connected. await Task.Delay(-1); } + private IServiceProvider _services; + private async Task InitCommands() { // Repeat this for all the service classes // and other dependencies that your commands might need. - _map.Add(new SomeServiceClass()); + _map.AddSingleton(new SomeServiceClass()); // Either search the program and add all Module classes that can be found: await _commands.AddModulesAsync(Assembly.GetEntryAssembly()); // Or add Modules manually if you prefer to be a little more explicit: await _commands.AddModuleAsync(); + // When all your required services are in the collection, build the container. + // Tip: There's an overload taking in a 'validateScopes' bool to make sure + // you haven't made any mistakes in your dependency graph. + _services = _map.BuildServiceProvider(); + // Subscribe a handler to see if a message invokes a command. _client.MessageReceived += HandleCommandAsync; } @@ -110,14 +118,14 @@ class Program // you want to prefix your commands with. // Uncomment the second half if you also want // commands to be invoked by mentioning the bot instead. - if (msg.HasCharPrefix('!', ref pos) /* || msg.HasMentionPrefix(msg.Discord.CurrentUser, ref pos) */) + if (msg.HasCharPrefix('!', ref pos) /* || msg.HasMentionPrefix(_client.CurrentUser, ref pos) */) { // Create a Command Context - var context = new SocketCommandContext(msg.Discord, msg); + var context = new SocketCommandContext(_client, msg); // Execute the command. (result does not indicate a return value, // rather an object stating if the command executed succesfully). - var result = await _commands.ExecuteAsync(context, pos, _map); + var result = await _commands.ExecuteAsync(context, pos, _services); // Uncomment the following lines if you want the bot // to send a message if it failed (not advised for most situations). From 6fed78025c8c2cfe76917cd3078f9e5312fb1b53 Mon Sep 17 00:00:00 2001 From: AntiTcb Date: Tue, 16 May 2017 20:02:32 -0400 Subject: [PATCH 185/243] Create DM channel if one does not exist. --- src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs index 1b599bf7e..7575309cb 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs @@ -66,8 +66,8 @@ namespace Discord.WebSocket internal SocketUser Clone() => MemberwiseClone() as SocketUser; //IUser - Task IUser.GetDMChannelAsync(CacheMode mode, RequestOptions options) - => Task.FromResult(GlobalUser.DMChannel); + async Task IUser.GetDMChannelAsync(CacheMode mode, RequestOptions options) + => await Task.FromResult(GlobalUser.DMChannel ?? await CreateDMChannelAsync(options) as IDMChannel); async Task IUser.CreateDMChannelAsync(RequestOptions options) => await CreateDMChannelAsync(options).ConfigureAwait(false); } From aeef5d08935544c10233c8741eca03ea1be67a11 Mon Sep 17 00:00:00 2001 From: AntiTcb Date: Tue, 16 May 2017 20:03:17 -0400 Subject: [PATCH 186/243] Update DM channel on entity updates. --- src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs index 0cd5f749e..3117eb14c 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.Linq; using Model = Discord.API.User; using PresenceModel = Discord.API.Presence; @@ -51,6 +52,7 @@ namespace Discord.WebSocket internal void Update(ClientState state, PresenceModel model) { Presence = SocketPresence.Create(model); + DMChannel = state.DMChannels.FirstOrDefault(x => x.Recipient.Id == Id); } internal new SocketGlobalUser Clone() => MemberwiseClone() as SocketGlobalUser; From 33a91ba3dee4b1dd68ebd895b5408584a22ec398 Mon Sep 17 00:00:00 2001 From: AntiTcb Date: Tue, 16 May 2017 20:03:38 -0400 Subject: [PATCH 187/243] Remove redundant explicit interface definition. --- src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs index b92559a40..05aa132a5 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs @@ -146,11 +146,7 @@ namespace Discord.WebSocket IGuild IGuildUser.Guild => Guild; ulong IGuildUser.GuildId => Guild.Id; IReadOnlyCollection IGuildUser.RoleIds => _roleIds; - - //IUser - Task IUser.GetDMChannelAsync(CacheMode mode, RequestOptions options) - => Task.FromResult(GlobalUser.DMChannel); - + //IVoiceState IVoiceChannel IVoiceState.VoiceChannel => VoiceChannel; } From 7db38f32bb481d790362ea6a5bc998045373113a Mon Sep 17 00:00:00 2001 From: AntiTcb Date: Tue, 16 May 2017 20:04:25 -0400 Subject: [PATCH 188/243] Attach/Remove DMChannel to SocketGlobalUser.DMChannel property --- src/Discord.Net.WebSocket/DiscordSocketClient.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index 4476b78c4..d42df7b55 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -1634,6 +1634,9 @@ namespace Discord.WebSocket { var channel = SocketChannel.CreatePrivate(this, state, model); state.AddChannel(channel as SocketChannel); + if (channel is SocketDMChannel dm) + dm.Recipient.GlobalUser.DMChannel = dm; + return channel; } internal ISocketPrivateChannel RemovePrivateChannel(ulong id) @@ -1641,6 +1644,9 @@ namespace Discord.WebSocket var channel = State.RemoveChannel(id) as ISocketPrivateChannel; if (channel != null) { + if (channel is SocketDMChannel dmChannel) + dmChannel.Recipient.GlobalUser.DMChannel = null; + foreach (var recipient in channel.Recipients) recipient.GlobalUser.RemoveRef(this); } From 3fb661a33abb36ac6a59e72e66797959181ca82b Mon Sep 17 00:00:00 2001 From: Christopher F Date: Sun, 21 May 2017 14:55:47 -0400 Subject: [PATCH 189/243] fix docs compile issue --- docs/guides/migrating/migrating.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guides/migrating/migrating.md b/docs/guides/migrating/migrating.md index 8f96dff98..bc628a5f8 100644 --- a/docs/guides/migrating/migrating.md +++ b/docs/guides/migrating/migrating.md @@ -42,7 +42,7 @@ events are delegates, but are still registered the same. For example, let's look at [DiscordSocketClient.MessageReceived](xref:Discord.WebSocket.DiscordSocketClient#Discord_WebSocket_DiscordSocketClient_MessageReceived) To hook an event into MessageReceived, we now use the following code: -[!code-csharp[Event Registration](guides/samples/migrating/event.cs)] +[!code-csharp[Event Registration](samples/event.cs)] > **All Event Handlers in 1.0 MUST return Task!** @@ -50,7 +50,7 @@ If your event handler is marked as `async`, it will automatically return `Task`. if you do not need to execute asynchronus code, do _not_ mark your handler as `async`, and instead, stick a `return Task.CompletedTask` at the bottom. -[!code-csharp[Sync Event Registration](guides/samples/migrating/sync_event.cs)] +[!code-csharp[Sync Event Registration](samples/sync_event.cs)] **Event handlers no longer require a sender.** The only arguments your event handler needs to accept are the parameters used by the event. It is recommended to look at the event in IntelliSense or on the From 892eca39fd90c43825a58bb4c080be762fe631c4 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Wed, 24 May 2017 21:05:06 +0200 Subject: [PATCH 190/243] Update cmd docs to use IServiceProvider --- docs/guides/commands/commands.md | 21 +++++++------ .../commands/samples/command_handler.cs | 30 +++++++++++-------- .../commands/samples/dependency_map_setup.cs | 9 +++--- 3 files changed, 31 insertions(+), 29 deletions(-) diff --git a/docs/guides/commands/commands.md b/docs/guides/commands/commands.md index 6dd595861..e021b1eb3 100644 --- a/docs/guides/commands/commands.md +++ b/docs/guides/commands/commands.md @@ -45,7 +45,7 @@ Discord.Net's implementation of Modules is influenced heavily from ASP.Net Core's Controller pattern. This means that the lifetime of a module instance is only as long as the command being invoked. -**Avoid using long-running code** in your modules whereever possible. +**Avoid using long-running code** in your modules wherever possible. You should **not** be implementing very much logic into your modules; outsource to a service for that. @@ -167,8 +167,8 @@ a dependency map. Modules are constructed using Dependency Injection. Any parameters that are placed in the constructor must be injected into an -@Discord.Commands.IDependencyMap. Alternatively, you may accept an -IDependencyMap as an argument and extract services yourself. +@System.IServiceProvider. Alternatively, you may accept an +IServiceProvider as an argument and extract services yourself. ### Module Properties @@ -205,21 +205,20 @@ you use DI when writing your modules. ### Setup -First, you need to create an @Discord.Commands.IDependencyMap. -The library includes @Discord.Commands.DependencyMap to help with -this, however you may create your own IDependencyMap if you wish. +First, you need to create an @System.IServiceProvider +You may create your own IServiceProvider if you wish. Next, add the dependencies your modules will use to the map. Finally, pass the map into the `LoadAssembly` method. Your modules will automatically be loaded with this dependency map. -[!code-csharp[DependencyMap Setup](samples/dependency_map_setup.cs)] +[!code-csharp[IServiceProvider Setup](samples/dependency_map_setup.cs)] ### Usage in Modules In the constructor of your module, any parameters will be filled in by -the @Discord.Commands.IDependencyMap you pass into `LoadAssembly`. +the @System.IServiceProvider you pass into `LoadAssembly`. Any publicly settable properties will also be filled in the same manner. @@ -228,12 +227,12 @@ Any publicly settable properties will also be filled in the same manner. being injected. >[!NOTE] ->If you accept `CommandService` or `IDependencyMap` as a parameter in +>If you accept `CommandService` or `IServiceProvider` as a parameter in your constructor or as an injectable property, these entries will be filled -by the CommandService the module was loaded from, and the DependencyMap passed +by the CommandService the module was loaded from, and the ServiceProvider passed into it, respectively. -[!code-csharp[DependencyMap in Modules](samples/dependency_module.cs)] +[!code-csharp[ServiceProvider in Modules](samples/dependency_module.cs)] # Preconditions diff --git a/docs/guides/commands/samples/command_handler.cs b/docs/guides/commands/samples/command_handler.cs index 71869415b..6b5d4ad2b 100644 --- a/docs/guides/commands/samples/command_handler.cs +++ b/docs/guides/commands/samples/command_handler.cs @@ -1,14 +1,16 @@ +using System; using System.Threading.Tasks; using System.Reflection; using Discord; using Discord.WebSocket; using Discord.Commands; +using Microsoft.Extensions.DependencyInjection; public class Program { private CommandService commands; private DiscordSocketClient client; - private DependencyMap map; + private IServiceProvider services; static void Main(string[] args) => new Program().Start().GetAwaiter().GetResult(); @@ -19,38 +21,40 @@ public class Program string token = "bot token here"; - map = new DependencyMap(); + services = new ServiceCollection() + .BuildServiceProvider(); await InstallCommands(); await client.LoginAsync(TokenType.Bot, token); - await client.ConnectAsync(); + await client.StartAsync(); await Task.Delay(-1); } + public async Task InstallCommands() { // Hook the MessageReceived Event into our Command Handler client.MessageReceived += HandleCommand; - // Discover all of the commands in this assembly and load them. + // Discover all of the commands in this assembly and load them. await commands.AddModulesAsync(Assembly.GetEntryAssembly()); } + public async Task HandleCommand(SocketMessage messageParam) - { + { // Don't process the command if it was a System Message var message = messageParam as SocketUserMessage; if (message == null) return; - // Create a number to track where the prefix ends and the command begins - int argPos = 0; - // Determine if the message is a command, based on if it starts with '!' or a mention prefix - if (!(message.HasCharPrefix('!', ref argPos) || message.HasMentionPrefix(client.CurrentUser, ref argPos))) return; + // Create a number to track where the prefix ends and the command begins + int argPos = 0; + // Determine if the message is a command, based on if it starts with '!' or a mention prefix + if (!(message.HasCharPrefix('!', ref argPos) || message.HasMentionPrefix(client.CurrentUser, ref argPos))) return; // Create a Command Context var context = new CommandContext(client, message); // Execute the command. (result does not indicate a return value, - // rather an object stating if the command executed succesfully) - var result = await commands.ExecuteAsync(context, argPos, map); + // rather an object stating if the command executed successfully) + var result = await commands.ExecuteAsync(context, argPos, service); if (!result.IsSuccess) await context.Channel.SendMessageAsync(result.ErrorReason); - } - + } } diff --git a/docs/guides/commands/samples/dependency_map_setup.cs b/docs/guides/commands/samples/dependency_map_setup.cs index aa39150e7..e205d891d 100644 --- a/docs/guides/commands/samples/dependency_map_setup.cs +++ b/docs/guides/commands/samples/dependency_map_setup.cs @@ -7,12 +7,11 @@ public class Commands { public async Task Install(DiscordSocketClient client) { - // Here, we will inject the Dependency Map with + // Here, we will inject the ServiceProvider with // all of the services our client will use. - _map.Add(client); - _map.Add(commands); - _map.Add(new NotificationService(_map)); - _map.Add(new DatabaseService(_map)); + _serviceCollection.AddSingleton(client) + _serviceCollection.AddSingleton(new NotificationService()) + _serviceCollection.AddSingleton(new DatabaseService()) // ... await _commands.AddModulesAsync(Assembly.GetEntryAssembly()); } From 333881a7115a3d9d2d241d4e7c34d1312b9190ac Mon Sep 17 00:00:00 2001 From: RogueException Date: Thu, 25 May 2017 13:54:57 -0300 Subject: [PATCH 191/243] Expose audio header more often --- src/Discord.Net.Core/Audio/AudioStream.cs | 5 ++- .../Audio/AudioClient.cs | 40 +++++++++---------- .../Audio/Streams/BufferedWriteStream.cs | 7 +++- .../Audio/Streams/JitterBuffer.cs | 4 +- .../Audio/Streams/OpusDecodeStream.cs | 17 ++++---- .../Audio/Streams/OpusEncodeStream.cs | 20 +++++++--- .../Audio/Streams/OutputStream.cs | 3 +- .../Audio/Streams/RTPWriteStream.cs | 7 ++-- .../Audio/Streams/SodiumEncryptStream.cs | 15 +++++++ 9 files changed, 75 insertions(+), 43 deletions(-) diff --git a/src/Discord.Net.Core/Audio/AudioStream.cs b/src/Discord.Net.Core/Audio/AudioStream.cs index d39bcc48a..97820ea73 100644 --- a/src/Discord.Net.Core/Audio/AudioStream.cs +++ b/src/Discord.Net.Core/Audio/AudioStream.cs @@ -11,7 +11,10 @@ namespace Discord.Audio public override bool CanSeek => false; public override bool CanWrite => false; - public virtual void WriteHeader(ushort seq, uint timestamp, bool missed) { } + public virtual void WriteHeader(ushort seq, uint timestamp, bool missed) + { + throw new InvalidOperationException("This stream does not accept headers"); + } public override void Write(byte[] buffer, int offset, int count) { WriteAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult(); diff --git a/src/Discord.Net.WebSocket/Audio/AudioClient.cs b/src/Discord.Net.WebSocket/Audio/AudioClient.cs index 19639a418..0ca45a557 100644 --- a/src/Discord.Net.WebSocket/Audio/AudioClient.cs +++ b/src/Discord.Net.WebSocket/Audio/AudioClient.cs @@ -142,31 +142,31 @@ namespace Discord.Audio public AudioOutStream CreateOpusStream(int bufferMillis) { - var outputStream = new OutputStream(ApiClient); - var sodiumEncrypter = new SodiumEncryptStream( outputStream, this); - var rtpWriter = new RTPWriteStream(sodiumEncrypter, _ssrc); - return new BufferedWriteStream(rtpWriter, this, bufferMillis, _connection.CancelToken, _audioLogger); + var outputStream = new OutputStream(ApiClient); //Ignores header + var sodiumEncrypter = new SodiumEncryptStream( outputStream, this); //Passes header + var rtpWriter = new RTPWriteStream(sodiumEncrypter, _ssrc); //Consumes header, passes + return new BufferedWriteStream(rtpWriter, this, bufferMillis, _connection.CancelToken, _audioLogger); //Generates header } public AudioOutStream CreateDirectOpusStream() { - var outputStream = new OutputStream(ApiClient); - var sodiumEncrypter = new SodiumEncryptStream(outputStream, this); - return new RTPWriteStream(sodiumEncrypter, _ssrc); + var outputStream = new OutputStream(ApiClient); //Ignores header + var sodiumEncrypter = new SodiumEncryptStream(outputStream, this); //Passes header + return new RTPWriteStream(sodiumEncrypter, _ssrc); //Consumes header (external input), passes } public AudioOutStream CreatePCMStream(AudioApplication application, int? bitrate, int bufferMillis) { - var outputStream = new OutputStream(ApiClient); - var sodiumEncrypter = new SodiumEncryptStream(outputStream, this); - var rtpWriter = new RTPWriteStream(sodiumEncrypter, _ssrc); - var bufferedStream = new BufferedWriteStream(rtpWriter, this, bufferMillis, _connection.CancelToken, _audioLogger); - return new OpusEncodeStream(bufferedStream, bitrate ?? (96 * 1024), application); + var outputStream = new OutputStream(ApiClient); //Ignores header + var sodiumEncrypter = new SodiumEncryptStream(outputStream, this); //Passes header + var rtpWriter = new RTPWriteStream(sodiumEncrypter, _ssrc); //Consumes header, passes + var bufferedStream = new BufferedWriteStream(rtpWriter, this, bufferMillis, _connection.CancelToken, _audioLogger); //Ignores header, generates header + return new OpusEncodeStream(bufferedStream, bitrate ?? (96 * 1024), application); //Generates header } public AudioOutStream CreateDirectPCMStream(AudioApplication application, int? bitrate) { - var outputStream = new OutputStream(ApiClient); - var sodiumEncrypter = new SodiumEncryptStream(outputStream, this); - var rtpWriter = new RTPWriteStream(sodiumEncrypter, _ssrc); - return new OpusEncodeStream(rtpWriter, bitrate ?? (96 * 1024), application); + var outputStream = new OutputStream(ApiClient); //Ignores header + var sodiumEncrypter = new SodiumEncryptStream(outputStream, this); //Passes header + var rtpWriter = new RTPWriteStream(sodiumEncrypter, _ssrc); //Consumes header, passes + return new OpusEncodeStream(rtpWriter, bitrate ?? (96 * 1024), application); //Generates header } internal async Task CreateInputStreamAsync(ulong userId) @@ -174,11 +174,11 @@ namespace Discord.Audio //Assume Thread-safe if (!_streams.ContainsKey(userId)) { - var readerStream = new InputStream(); - var opusDecoder = new OpusDecodeStream(readerStream); + var readerStream = new InputStream(); //Consumes header + var opusDecoder = new OpusDecodeStream(readerStream); //Passes header //var jitterBuffer = new JitterBuffer(opusDecoder, _audioLogger); - var rtpReader = new RTPReadStream(opusDecoder); - var decryptStream = new SodiumDecryptStream(rtpReader, this); + var rtpReader = new RTPReadStream(opusDecoder); //Generates header + var decryptStream = new SodiumDecryptStream(rtpReader, this); //No header _streams.TryAdd(userId, new StreamPair(readerStream, decryptStream)); await _streamCreatedEvent.InvokeAsync(userId, readerStream); } diff --git a/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs index 29586389c..fb302f132 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs @@ -88,11 +88,12 @@ namespace Discord.Audio.Streams if (_queuedFrames.TryDequeue(out Frame frame)) { await _client.SetSpeakingAsync(true).ConfigureAwait(false); - _next.WriteHeader(seq++, timestamp, false); + _next.WriteHeader(seq, timestamp, false); await _next.WriteAsync(frame.Buffer, 0, frame.Bytes).ConfigureAwait(false); _bufferPool.Enqueue(frame.Buffer); _queueLock.Release(); nextTick += _ticksPerFrame; + seq++; timestamp += OpusEncoder.FrameSamplesPerChannel; _silenceFrames = 0; #if DEBUG @@ -105,12 +106,13 @@ namespace Discord.Audio.Streams { if (_silenceFrames++ < MaxSilenceFrames) { - _next.WriteHeader(seq++, timestamp, false); + _next.WriteHeader(seq, timestamp, false); await _next.WriteAsync(_silenceFrame, 0, _silenceFrame.Length).ConfigureAwait(false); } else await _client.SetSpeakingAsync(false).ConfigureAwait(false); nextTick += _ticksPerFrame; + seq++; timestamp += OpusEncoder.FrameSamplesPerChannel; } #if DEBUG @@ -126,6 +128,7 @@ namespace Discord.Audio.Streams }); } + public override void WriteHeader(ushort seq, uint timestamp, bool missed) { } //Ignore, we use our own timing public override async Task WriteAsync(byte[] data, int offset, int count, CancellationToken cancelToken) { if (cancelToken.CanBeCanceled) diff --git a/src/Discord.Net.WebSocket/Audio/Streams/JitterBuffer.cs b/src/Discord.Net.WebSocket/Audio/Streams/JitterBuffer.cs index a5ecdea6f..10f842a9d 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/JitterBuffer.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/JitterBuffer.cs @@ -1,4 +1,4 @@ -using Discord.Logging; +/*using Discord.Logging; using System; using System.Collections.Concurrent; using System.Threading; @@ -243,4 +243,4 @@ namespace Discord.Audio.Streams return Task.Delay(0); } } -} \ No newline at end of file +}*/ \ No newline at end of file diff --git a/src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs index 43289c60e..58c4f4c70 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs @@ -25,12 +25,13 @@ namespace Discord.Audio.Streams public override void WriteHeader(ushort seq, uint timestamp, bool missed) { if (_hasHeader) - throw new InvalidOperationException("Header received with no payload"); - _nextMissed = missed; + throw new InvalidOperationException("Header received with no payload"); _hasHeader = true; + + _nextMissed = missed; _next.WriteHeader(seq, timestamp, missed); } - public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancelToken) { if (!_hasHeader) throw new InvalidOperationException("Received payload without an RTP header"); @@ -39,17 +40,17 @@ namespace Discord.Audio.Streams if (!_nextMissed) { count = _decoder.DecodeFrame(buffer, offset, count, _buffer, 0, false); - await _next.WriteAsync(_buffer, 0, count, cancellationToken).ConfigureAwait(false); + await _next.WriteAsync(_buffer, 0, count, cancelToken).ConfigureAwait(false); } else if (count > 0) { - count = _decoder.DecodeFrame(buffer, offset, count, _buffer, 0, true); - await _next.WriteAsync(_buffer, 0, count, cancellationToken).ConfigureAwait(false); + count = _decoder.DecodeFrame(buffer, offset, count, _buffer, 0, true); + await _next.WriteAsync(_buffer, 0, count, cancelToken).ConfigureAwait(false); } else { - count = _decoder.DecodeFrame(null, 0, 0, _buffer, 0, true); - await _next.WriteAsync(_buffer, 0, count, cancellationToken).ConfigureAwait(false); + count = _decoder.DecodeFrame(null, 0, 0, _buffer, 0, true); + await _next.WriteAsync(_buffer, 0, count, cancelToken).ConfigureAwait(false); } } diff --git a/src/Discord.Net.WebSocket/Audio/Streams/OpusEncodeStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/OpusEncodeStream.cs index 2a3c03a47..a7779a84c 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/OpusEncodeStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/OpusEncodeStream.cs @@ -13,6 +13,8 @@ namespace Discord.Audio.Streams private readonly OpusEncoder _encoder; private readonly byte[] _buffer; private int _partialFramePos; + private ushort _seq; + private uint _timestamp; public OpusEncodeStream(AudioStream next, int bitrate, AudioApplication application) { @@ -21,7 +23,7 @@ namespace Discord.Audio.Streams _buffer = new byte[OpusConverter.FrameBytes]; } - public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancelToken) { //Assume threadsafe while (count > 0) @@ -30,10 +32,13 @@ namespace Discord.Audio.Streams { //We have enough data and no partial frames. Pass the buffer directly to the encoder int encFrameSize = _encoder.EncodeFrame(buffer, offset, _buffer, 0); - await _next.WriteAsync(_buffer, 0, encFrameSize, cancellationToken).ConfigureAwait(false); + _next.WriteHeader(_seq, _timestamp, false); + await _next.WriteAsync(_buffer, 0, encFrameSize, cancelToken).ConfigureAwait(false); offset += OpusConverter.FrameBytes; count -= OpusConverter.FrameBytes; + _seq++; + _timestamp += OpusConverter.FrameBytes; } else if (_partialFramePos + count >= OpusConverter.FrameBytes) { @@ -41,11 +46,14 @@ namespace Discord.Audio.Streams int partialSize = OpusConverter.FrameBytes - _partialFramePos; Buffer.BlockCopy(buffer, offset, _buffer, _partialFramePos, partialSize); int encFrameSize = _encoder.EncodeFrame(_buffer, 0, _buffer, 0); - await _next.WriteAsync(_buffer, 0, encFrameSize, cancellationToken).ConfigureAwait(false); + _next.WriteHeader(_seq, _timestamp, false); + await _next.WriteAsync(_buffer, 0, encFrameSize, cancelToken).ConfigureAwait(false); offset += partialSize; count -= partialSize; _partialFramePos = 0; + _seq++; + _timestamp += OpusConverter.FrameBytes; } else { @@ -57,8 +65,8 @@ namespace Discord.Audio.Streams } } - /* - public override async Task FlushAsync(CancellationToken cancellationToken) + /* //Opus throws memory errors on bad frames + public override async Task FlushAsync(CancellationToken cancelToken) { try { @@ -67,7 +75,7 @@ namespace Discord.Audio.Streams } catch (Exception) { } //Incomplete frame _partialFramePos = 0; - await base.FlushAsync(cancellationToken).ConfigureAwait(false); + await base.FlushAsync(cancelToken).ConfigureAwait(false); }*/ public override async Task FlushAsync(CancellationToken cancelToken) diff --git a/src/Discord.Net.WebSocket/Audio/Streams/OutputStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/OutputStream.cs index 6238e93b4..cba4e3cb6 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/OutputStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/OutputStream.cs @@ -13,7 +13,8 @@ namespace Discord.Audio.Streams { _client = client; } - + + public override void WriteHeader(ushort seq, uint timestamp, bool missed) { } //Ignore public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancelToken) { cancelToken.ThrowIfCancellationRequested(); diff --git a/src/Discord.Net.WebSocket/Audio/Streams/RTPWriteStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/RTPWriteStream.cs index 78f895381..ce407eada 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/RTPWriteStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/RTPWriteStream.cs @@ -33,14 +33,14 @@ namespace Discord.Audio.Streams { if (_hasHeader) throw new InvalidOperationException("Header received with no payload"); + _hasHeader = true; _nextSeq = seq; _nextTimestamp = timestamp; } - public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancelToken) { - cancellationToken.ThrowIfCancellationRequested(); - + cancelToken.ThrowIfCancellationRequested(); if (!_hasHeader) throw new InvalidOperationException("Received payload without an RTP header"); _hasHeader = false; @@ -57,6 +57,7 @@ namespace Discord.Audio.Streams Buffer.BlockCopy(_header, 0, _buffer, 0, 12); //Copy RTP header from to the buffer Buffer.BlockCopy(buffer, offset, _buffer, 12, count); + _next.WriteHeader(_nextSeq, _nextTimestamp, false); await _next.WriteAsync(_buffer, 0, count + 12).ConfigureAwait(false); } diff --git a/src/Discord.Net.WebSocket/Audio/Streams/SodiumEncryptStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/SodiumEncryptStream.cs index b00a7f403..2e7a7e276 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/SodiumEncryptStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/SodiumEncryptStream.cs @@ -10,6 +10,9 @@ namespace Discord.Audio.Streams private readonly AudioClient _client; private readonly AudioStream _next; private readonly byte[] _nonce; + private bool _hasHeader; + private ushort _nextSeq; + private uint _nextTimestamp; public SodiumEncryptStream(AudioStream next, IAudioClient client) { @@ -17,16 +20,28 @@ namespace Discord.Audio.Streams _client = (AudioClient)client; _nonce = new byte[24]; } + + public override void WriteHeader(ushort seq, uint timestamp, bool missed) + { + if (_hasHeader) + throw new InvalidOperationException("Header received with no payload"); + _nextSeq = seq; + _nextTimestamp = timestamp; + } public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancelToken) { cancelToken.ThrowIfCancellationRequested(); + if (!_hasHeader) + throw new InvalidOperationException("Received payload without an RTP header"); + _hasHeader = false; if (_client.SecretKey == null) return; Buffer.BlockCopy(buffer, offset, _nonce, 0, 12); //Copy nonce from RTP header count = SecretBox.Encrypt(buffer, offset + 12, count - 12, buffer, 12, _nonce, _client.SecretKey); + _next.WriteHeader(_nextSeq, _nextTimestamp, false); await _next.WriteAsync(buffer, 0, count + 12, cancelToken).ConfigureAwait(false); } From 8eb9b2071cbb0f4d0efaff4b578c484a68f1dc41 Mon Sep 17 00:00:00 2001 From: RogueException Date: Thu, 25 May 2017 21:45:41 -0300 Subject: [PATCH 192/243] Set hasHeader in SodiumEncrypt --- src/Discord.Net.WebSocket/Audio/Streams/SodiumEncryptStream.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Discord.Net.WebSocket/Audio/Streams/SodiumEncryptStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/SodiumEncryptStream.cs index 2e7a7e276..bacc9be47 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/SodiumEncryptStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/SodiumEncryptStream.cs @@ -28,6 +28,7 @@ namespace Discord.Audio.Streams _nextSeq = seq; _nextTimestamp = timestamp; + _hasHeader = true; } public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancelToken) { From 73611d1fab6c55ca21d0115040dc2fd72dcf07c7 Mon Sep 17 00:00:00 2001 From: AntiTcb Date: Sat, 27 May 2017 14:47:12 -0400 Subject: [PATCH 193/243] Remove IUser.CreateDMChannelAsync, implicitly implement IUser.GetDMChannelAsync --- src/Discord.Net.Core/Entities/Users/IUser.cs | 4 +--- src/Discord.Net.Rest/Entities/Users/RestUser.cs | 8 +++----- src/Discord.Net.Rpc/Entities/Users/RpcUser.cs | 8 +++----- .../Entities/Users/SocketUser.cs | 12 +++++------- 4 files changed, 12 insertions(+), 20 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Users/IUser.cs b/src/Discord.Net.Core/Entities/Users/IUser.cs index 45d8862f1..249100d37 100644 --- a/src/Discord.Net.Core/Entities/Users/IUser.cs +++ b/src/Discord.Net.Core/Entities/Users/IUser.cs @@ -20,8 +20,6 @@ namespace Discord string Username { get; } /// Returns a private message channel to this user, creating one if it does not already exist. - Task GetDMChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); - /// Returns a private message channel to this user, creating one if it does not already exist. - Task CreateDMChannelAsync(RequestOptions options = null); + Task GetDMChannelAsync(RequestOptions options = null); } } diff --git a/src/Discord.Net.Rest/Entities/Users/RestUser.cs b/src/Discord.Net.Rest/Entities/Users/RestUser.cs index cded876c8..36ca242d8 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestUser.cs @@ -54,7 +54,7 @@ namespace Discord.Rest Update(model); } - public Task CreateDMChannelAsync(RequestOptions options = null) + public Task GetDMChannelAsync(RequestOptions options = null) => UserHelper.CreateDMChannelAsync(this, Discord, options); public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) @@ -64,9 +64,7 @@ namespace Discord.Rest private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")})"; //IUser - Task IUser.GetDMChannelAsync(CacheMode mode, RequestOptions options) - => Task.FromResult(null); - async Task IUser.CreateDMChannelAsync(RequestOptions options) - => await CreateDMChannelAsync(options).ConfigureAwait(false); + async Task IUser.GetDMChannelAsync(RequestOptions options) + => await GetDMChannelAsync(options); } } diff --git a/src/Discord.Net.Rpc/Entities/Users/RpcUser.cs b/src/Discord.Net.Rpc/Entities/Users/RpcUser.cs index 7ed11e57d..71de1f804 100644 --- a/src/Discord.Net.Rpc/Entities/Users/RpcUser.cs +++ b/src/Discord.Net.Rpc/Entities/Users/RpcUser.cs @@ -49,7 +49,7 @@ namespace Discord.Rpc Username = model.Username.Value; } - public Task CreateDMChannelAsync(RequestOptions options = null) + public Task GetDMChannelAsync(RequestOptions options = null) => UserHelper.CreateDMChannelAsync(this, Discord, options); public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) @@ -59,9 +59,7 @@ namespace Discord.Rpc private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")})"; //IUser - Task IUser.GetDMChannelAsync(CacheMode mode, RequestOptions options) - => Task.FromResult(null); - async Task IUser.CreateDMChannelAsync(RequestOptions options) - => await CreateDMChannelAsync(options).ConfigureAwait(false); + async Task IUser.GetDMChannelAsync(RequestOptions options) + => await GetDMChannelAsync(options); } } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs index 7575309cb..60fca73b2 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs @@ -53,10 +53,10 @@ namespace Discord.WebSocket hasChanges = true; } return hasChanges; - } + } - public Task CreateDMChannelAsync(RequestOptions options = null) - => UserHelper.CreateDMChannelAsync(this, Discord, options); + public async Task GetDMChannelAsync(RequestOptions options = null) + => GlobalUser.DMChannel ?? await UserHelper.CreateDMChannelAsync(this, Discord, options) as IDMChannel; public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) => CDN.GetUserAvatarUrl(Id, AvatarId, size, format); @@ -66,9 +66,7 @@ namespace Discord.WebSocket internal SocketUser Clone() => MemberwiseClone() as SocketUser; //IUser - async Task IUser.GetDMChannelAsync(CacheMode mode, RequestOptions options) - => await Task.FromResult(GlobalUser.DMChannel ?? await CreateDMChannelAsync(options) as IDMChannel); - async Task IUser.CreateDMChannelAsync(RequestOptions options) - => await CreateDMChannelAsync(options).ConfigureAwait(false); + Task IUser.GetDMChannelAsync(RequestOptions options) + => GetDMChannelAsync(options); } } From 2ef53330fb8b19b5a150ec1b3a2b16f6896835a5 Mon Sep 17 00:00:00 2001 From: BlockBuilder57 Date: Mon, 29 May 2017 13:33:28 -0500 Subject: [PATCH 194/243] Add newest verification level Users must have a verified phone on their Discord account. http://i.imgur.com/BexDgzS.png --- src/Discord.Net.Core/Entities/Guilds/VerificationLevel.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net.Core/Entities/Guilds/VerificationLevel.cs b/src/Discord.Net.Core/Entities/Guilds/VerificationLevel.cs index d6828b5c9..ac51fe927 100644 --- a/src/Discord.Net.Core/Entities/Guilds/VerificationLevel.cs +++ b/src/Discord.Net.Core/Entities/Guilds/VerificationLevel.cs @@ -9,6 +9,8 @@ /// Users must fulfill the requirements of Low, and be registered on Discord for at least 5 minutes. Medium = 2, /// Users must fulfill the requirements of Medium, and be a member of this guild for at least 10 minutes. - High = 3 + High = 3, + /// Users must fulfill the requirements of High, and must have a verified phone on their Discord account. + Extreme = 4 } } From d05191ed051aa269bd8ae30f97ca3ecbd258e1f4 Mon Sep 17 00:00:00 2001 From: Joe4evr Date: Tue, 30 May 2017 17:54:32 +0200 Subject: [PATCH 195/243] Added/clarified some comments in structure.cs --- .../getting_started/samples/intro/structure.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/docs/guides/getting_started/samples/intro/structure.cs b/docs/guides/getting_started/samples/intro/structure.cs index 9e783bb9b..d611e1240 100644 --- a/docs/guides/getting_started/samples/intro/structure.cs +++ b/docs/guides/getting_started/samples/intro/structure.cs @@ -30,8 +30,8 @@ class Program LogLevel = LogSeverity.Info, // If you or another service needs to do anything with messages - // (eg. checking Reactions), you should probably - // set the MessageCacheSize here. + // (eg. checking Reactions, checking the content of edited/deleted messages), + // you must set the MessageCacheSize. You may adjust the number as needed. //MessageCacheSize = 50, // If your platform doesn't have native websockets, @@ -41,7 +41,7 @@ class Program }); } - // Create a named logging handler, so it can be re-used by addons + // Example of a logging handler. This can be re-used by addons // that ask for a Func. private static Task Logger(LogMessage message) { @@ -65,6 +65,11 @@ class Program } Console.WriteLine($"{DateTime.Now,-19} [{message.Severity,8}] {message.Source}: {message.Message}"); Console.ForegroundColor = cc; + + // If you get an error saying 'CompletedTask' doesn't exist, + // your project is targeting .NET 4.5.2 or lower. You'll need + // to adjust your project's target framework to 4.6 or higher + // (instructions for this are easily Googled). return Task.CompletedTask; } @@ -120,7 +125,7 @@ class Program // commands to be invoked by mentioning the bot instead. if (msg.HasCharPrefix('!', ref pos) /* || msg.HasMentionPrefix(_client.CurrentUser, ref pos) */) { - // Create a Command Context + // Create a Command Context. var context = new SocketCommandContext(_client, msg); // Execute the command. (result does not indicate a return value, From 12acfec1dbdedfa79fde4e3a5d11c26ebe35ffb0 Mon Sep 17 00:00:00 2001 From: Joe4evr Date: Sun, 4 Jun 2017 23:44:39 +0200 Subject: [PATCH 196/243] Respond to feedback --- docs/guides/getting_started/samples/intro/structure.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/guides/getting_started/samples/intro/structure.cs b/docs/guides/getting_started/samples/intro/structure.cs index d611e1240..bdbdf8fe4 100644 --- a/docs/guides/getting_started/samples/intro/structure.cs +++ b/docs/guides/getting_started/samples/intro/structure.cs @@ -70,6 +70,8 @@ class Program // your project is targeting .NET 4.5.2 or lower. You'll need // to adjust your project's target framework to 4.6 or higher // (instructions for this are easily Googled). + // If you *need* to run on .NET 4.5 for compat/other reasons, + // the alternative is to 'return Task.Delay(0);' instead. return Task.CompletedTask; } From 6cdc48bfa66bd48108e9931778ad4fbb95607ea1 Mon Sep 17 00:00:00 2001 From: Joe4evr Date: Wed, 7 Jun 2017 04:32:59 +0200 Subject: [PATCH 197/243] Move instructions about BuildServiceProvider() up --- docs/guides/getting_started/samples/intro/structure.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/guides/getting_started/samples/intro/structure.cs b/docs/guides/getting_started/samples/intro/structure.cs index bdbdf8fe4..000ee2d02 100644 --- a/docs/guides/getting_started/samples/intro/structure.cs +++ b/docs/guides/getting_started/samples/intro/structure.cs @@ -99,16 +99,16 @@ class Program // and other dependencies that your commands might need. _map.AddSingleton(new SomeServiceClass()); - // Either search the program and add all Module classes that can be found: - await _commands.AddModulesAsync(Assembly.GetEntryAssembly()); - // Or add Modules manually if you prefer to be a little more explicit: - await _commands.AddModuleAsync(); - // When all your required services are in the collection, build the container. // Tip: There's an overload taking in a 'validateScopes' bool to make sure // you haven't made any mistakes in your dependency graph. _services = _map.BuildServiceProvider(); + // Either search the program and add all Module classes that can be found: + await _commands.AddModulesAsync(Assembly.GetEntryAssembly()); + // Or add Modules manually if you prefer to be a little more explicit: + await _commands.AddModuleAsync(); + // Subscribe a handler to see if a message invokes a command. _client.MessageReceived += HandleCommandAsync; } From 1d096a7fc519f96251bc005e9c2fc64202c524e6 Mon Sep 17 00:00:00 2001 From: Izumemori Date: Tue, 13 Jun 2017 01:58:54 +0200 Subject: [PATCH 198/243] Fix spelling --- src/Discord.Net.Core/Entities/Emotes/Emoji.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.Core/Entities/Emotes/Emoji.cs b/src/Discord.Net.Core/Entities/Emotes/Emoji.cs index 96226c715..5c1969613 100644 --- a/src/Discord.Net.Core/Entities/Emotes/Emoji.cs +++ b/src/Discord.Net.Core/Entities/Emotes/Emoji.cs @@ -7,7 +7,7 @@ { // TODO: need to constrain this to unicode-only emojis somehow /// - /// Creates a unciode emoji. + /// Creates a unicode emoji. /// /// The pure UTF-8 encoding of an emoji public Emoji(string unicode) From b0a3ce5e7cd600ce32f7c8cd98e9ae59bc10e5bc Mon Sep 17 00:00:00 2001 From: Joe4evr Date: Tue, 13 Jun 2017 10:58:06 +0200 Subject: [PATCH 199/243] Respond to feedback. --- docs/guides/getting_started/samples/intro/structure.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/guides/getting_started/samples/intro/structure.cs b/docs/guides/getting_started/samples/intro/structure.cs index 000ee2d02..706d0a38d 100644 --- a/docs/guides/getting_started/samples/intro/structure.cs +++ b/docs/guides/getting_started/samples/intro/structure.cs @@ -104,7 +104,8 @@ class Program // you haven't made any mistakes in your dependency graph. _services = _map.BuildServiceProvider(); - // Either search the program and add all Module classes that can be found: + // Either search the program and add all Module classes that can be found. + // Module classes *must* be marked 'public' or they will be ignored. await _commands.AddModulesAsync(Assembly.GetEntryAssembly()); // Or add Modules manually if you prefer to be a little more explicit: await _commands.AddModuleAsync(); From fb01e16b36aa44bdf877b9b4efe574862632e526 Mon Sep 17 00:00:00 2001 From: Drew Date: Thu, 15 Jun 2017 10:43:06 -0400 Subject: [PATCH 200/243] Fixed dead link (#662) * Update intro.md * Update intro.md * Update intro.md * Update intro.md * Update intro.md * Update intro.md * Update intro.md --- docs/guides/getting_started/intro.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guides/getting_started/intro.md b/docs/guides/getting_started/intro.md index 8bcfa9086..837814511 100644 --- a/docs/guides/getting_started/intro.md +++ b/docs/guides/getting_started/intro.md @@ -211,7 +211,7 @@ For your reference, you may view the [completed program]. # Building a bot with commands This section will show you how to write a program that is ready for -[commands](commands.md). Note that this will not be explaining _how_ +[commands](commands/commands.md). Note that this will not be explaining _how_ to write commands or services, it will only be covering the general structure. @@ -224,4 +224,4 @@ should be to separate the program (initialization and command handler), the modules (handle commands), and the services (persistent storage, pure functions, data manipulation). -**todo:** diagram of bot structure \ No newline at end of file +**todo:** diagram of bot structure From 8c2a46e9e751e927d7bce7581e20e71069a8c3c4 Mon Sep 17 00:00:00 2001 From: Alex Gravely Date: Thu, 15 Jun 2017 11:05:41 -0400 Subject: [PATCH 201/243] Add ulong overload to IMessageChannel.DeleteMessagesAsync (#649) --- src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs | 4 +++- src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs | 8 ++++---- src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs | 4 +++- .../Entities/Channels/RestGroupChannel.cs | 4 +++- src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs | 4 +++- .../Entities/Channels/RpcVirtualMessageChannel.cs | 4 +++- src/Discord.Net.Rpc/Entities/Channels/RpcDMChannel.cs | 4 +++- src/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs | 4 +++- src/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs | 4 +++- .../Entities/Channels/SocketDMChannel.cs | 4 +++- .../Entities/Channels/SocketGroupChannel.cs | 4 +++- .../Entities/Channels/SocketTextChannel.cs | 4 +++- 12 files changed, 37 insertions(+), 15 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs b/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs index 7fce1e855..a465b3ad8 100644 --- a/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs @@ -30,7 +30,9 @@ namespace Discord /// Gets a collection of pinned messages in this channel. Task> GetPinnedMessagesAsync(RequestOptions options = null); /// Bulk deletes multiple messages. - Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null); + Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null); + /// Bulk deletes multiple messages. + Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null); /// Broadcasts the "user is typing" message to all users in this channel, lasting 10 seconds. Task TriggerTypingAsync(RequestOptions options = null); diff --git a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs index 284decd8c..6b7dca3a9 100644 --- a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs +++ b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs @@ -178,12 +178,12 @@ namespace Discord.Rest var args = new UploadFileParams(stream) { Filename = filename, Content = text, IsTTS = isTTS }; var model = await client.ApiClient.UploadFileAsync(channel.Id, args, options).ConfigureAwait(false); return RestUserMessage.Create(client, channel, client.CurrentUser, model); - } + } - public static async Task DeleteMessagesAsync(IMessageChannel channel, BaseDiscordClient client, - IEnumerable messages, RequestOptions options) + public static async Task DeleteMessagesAsync(IMessageChannel channel, BaseDiscordClient client, + IEnumerable messageIds, RequestOptions options) { - var msgs = messages.Select(x => x.Id).ToArray(); + var msgs = messageIds.ToArray(); if (msgs.Length < 100) { var args = new DeleteMessagesParams(msgs); diff --git a/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs index 0a4bc9522..8a31da3f1 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs @@ -73,7 +73,9 @@ namespace Discord.Rest => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) - => ChannelHelper.DeleteMessagesAsync(this, Discord, messages, options); + => ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options); + public Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) + => ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options); public Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); diff --git a/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs index b324422a5..44c118fee 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs @@ -86,7 +86,9 @@ namespace Discord.Rest => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) - => ChannelHelper.DeleteMessagesAsync(this, Discord, messages, options); + => ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options); + public Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) + => ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options); public Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); diff --git a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs index 2fe9feb91..d7405fb4a 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs @@ -64,7 +64,9 @@ namespace Discord.Rest => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) - => ChannelHelper.DeleteMessagesAsync(this, Discord, messages, options); + => ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options); + public Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) + => ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options); public Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); diff --git a/src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs index dfd996ee1..775f2ea82 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs @@ -43,7 +43,9 @@ namespace Discord.Rest => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) - => ChannelHelper.DeleteMessagesAsync(this, Discord, messages, options); + => ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options); + public Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) + => ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options); public Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcDMChannel.cs b/src/Discord.Net.Rpc/Entities/Channels/RpcDMChannel.cs index b2c3daaa2..da9bce700 100644 --- a/src/Discord.Net.Rpc/Entities/Channels/RpcDMChannel.cs +++ b/src/Discord.Net.Rpc/Entities/Channels/RpcDMChannel.cs @@ -54,7 +54,9 @@ namespace Discord.Rpc => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) - => ChannelHelper.DeleteMessagesAsync(this, Discord, messages, options); + => ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options); + public Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) + => ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options); public Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs b/src/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs index b5effacc6..d449688a4 100644 --- a/src/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs +++ b/src/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs @@ -57,7 +57,9 @@ namespace Discord.Rpc => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) - => ChannelHelper.DeleteMessagesAsync(this, Discord, messages, options); + => ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options); + public Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) + => ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options); public Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs b/src/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs index bdfafa561..72b45e466 100644 --- a/src/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs +++ b/src/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs @@ -58,7 +58,9 @@ namespace Discord.Rpc => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) - => ChannelHelper.DeleteMessagesAsync(this, Discord, messages, options); + => ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options); + public Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) + => ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options); public Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs index 8e9272ff0..322a99496 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs @@ -77,7 +77,9 @@ namespace Discord.WebSocket => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) - => ChannelHelper.DeleteMessagesAsync(this, Discord, messages, options); + => ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options); + public Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) + => ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options); public Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs index d7d80214f..dc1853e73 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs @@ -105,7 +105,9 @@ namespace Discord.WebSocket => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) - => ChannelHelper.DeleteMessagesAsync(this, Discord, messages, options); + => ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options); + public Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) + => ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options); public Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs index e75b6a4f1..c22523e00 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs @@ -81,7 +81,9 @@ namespace Discord.WebSocket => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) - => ChannelHelper.DeleteMessagesAsync(this, Discord, messages, options); + => ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options); + public Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) + => ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options); public Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); From fb57a61432d3e800c881067983fcceaf6eac2339 Mon Sep 17 00:00:00 2001 From: AntiTcb Date: Fri, 16 Jun 2017 20:43:50 -0400 Subject: [PATCH 202/243] Rename to GetOrCreateDMChannelAsync --- src/Discord.Net.Core/Entities/Users/IUser.cs | 2 +- src/Discord.Net.Rest/Entities/Users/RestUser.cs | 6 +++--- src/Discord.Net.Rpc/Entities/Users/RpcUser.cs | 6 +++--- src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs | 6 +----- 4 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Users/IUser.cs b/src/Discord.Net.Core/Entities/Users/IUser.cs index 249100d37..e3f270f6f 100644 --- a/src/Discord.Net.Core/Entities/Users/IUser.cs +++ b/src/Discord.Net.Core/Entities/Users/IUser.cs @@ -20,6 +20,6 @@ namespace Discord string Username { get; } /// Returns a private message channel to this user, creating one if it does not already exist. - Task GetDMChannelAsync(RequestOptions options = null); + Task GetOrCreateDMChannelAsync(RequestOptions options = null); } } diff --git a/src/Discord.Net.Rest/Entities/Users/RestUser.cs b/src/Discord.Net.Rest/Entities/Users/RestUser.cs index 36ca242d8..d8ade3a6b 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestUser.cs @@ -54,7 +54,7 @@ namespace Discord.Rest Update(model); } - public Task GetDMChannelAsync(RequestOptions options = null) + public Task GetOrCreateDMChannelAsync(RequestOptions options = null) => UserHelper.CreateDMChannelAsync(this, Discord, options); public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) @@ -64,7 +64,7 @@ namespace Discord.Rest private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")})"; //IUser - async Task IUser.GetDMChannelAsync(RequestOptions options) - => await GetDMChannelAsync(options); + async Task IUser.GetOrCreateDMChannelAsync(RequestOptions options) + => await GetOrCreateDMChannelAsync(options); } } diff --git a/src/Discord.Net.Rpc/Entities/Users/RpcUser.cs b/src/Discord.Net.Rpc/Entities/Users/RpcUser.cs index 71de1f804..c6b0b2fd8 100644 --- a/src/Discord.Net.Rpc/Entities/Users/RpcUser.cs +++ b/src/Discord.Net.Rpc/Entities/Users/RpcUser.cs @@ -49,7 +49,7 @@ namespace Discord.Rpc Username = model.Username.Value; } - public Task GetDMChannelAsync(RequestOptions options = null) + public Task GetOrCreateDMChannelAsync(RequestOptions options = null) => UserHelper.CreateDMChannelAsync(this, Discord, options); public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) @@ -59,7 +59,7 @@ namespace Discord.Rpc private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")})"; //IUser - async Task IUser.GetDMChannelAsync(RequestOptions options) - => await GetDMChannelAsync(options); + async Task IUser.GetOrCreateDMChannelAsync(RequestOptions options) + => await GetOrCreateDMChannelAsync(options); } } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs index 60fca73b2..a0c78b93f 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs @@ -55,7 +55,7 @@ namespace Discord.WebSocket return hasChanges; } - public async Task GetDMChannelAsync(RequestOptions options = null) + public async Task GetOrCreateDMChannelAsync(RequestOptions options = null) => GlobalUser.DMChannel ?? await UserHelper.CreateDMChannelAsync(this, Discord, options) as IDMChannel; public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) @@ -64,9 +64,5 @@ namespace Discord.WebSocket public override string ToString() => $"{Username}#{Discriminator}"; private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")})"; internal SocketUser Clone() => MemberwiseClone() as SocketUser; - - //IUser - Task IUser.GetDMChannelAsync(RequestOptions options) - => GetDMChannelAsync(options); } } From 0708bc5d48dee30c398a5ff13c5884788b85dc59 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Fri, 16 Jun 2017 22:39:40 -0400 Subject: [PATCH 203/243] Add EmbedType enum --- src/Discord.Net.Core/Entities/Messages/Embed.cs | 6 +++--- src/Discord.Net.Core/Entities/Messages/EmbedType.cs | 11 +++++++++++ src/Discord.Net.Core/Entities/Messages/IEmbed.cs | 2 +- src/Discord.Net.Rest/API/Common/Embed.cs | 4 ++-- .../Entities/Messages/EmbedBuilder.cs | 2 +- 5 files changed, 18 insertions(+), 7 deletions(-) create mode 100644 src/Discord.Net.Core/Entities/Messages/EmbedType.cs diff --git a/src/Discord.Net.Core/Entities/Messages/Embed.cs b/src/Discord.Net.Core/Entities/Messages/Embed.cs index ebde05d4c..c80151375 100644 --- a/src/Discord.Net.Core/Entities/Messages/Embed.cs +++ b/src/Discord.Net.Core/Entities/Messages/Embed.cs @@ -7,7 +7,7 @@ namespace Discord [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class Embed : IEmbed { - public string Type { get; } + public EmbedType Type { get; } public string Description { get; internal set; } public string Url { get; internal set; } @@ -22,12 +22,12 @@ namespace Discord public EmbedThumbnail? Thumbnail { get; internal set; } public ImmutableArray Fields { get; internal set; } - internal Embed(string type) + internal Embed(EmbedType type) { Type = type; Fields = ImmutableArray.Create(); } - internal Embed(string type, + internal Embed(EmbedType type, string title, string description, string url, diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedType.cs b/src/Discord.Net.Core/Entities/Messages/EmbedType.cs new file mode 100644 index 000000000..e071e7dc8 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Messages/EmbedType.cs @@ -0,0 +1,11 @@ +namespace Discord +{ + public enum EmbedType + { + Rich, + Link, + Video, + Image, + Gifv + } +} diff --git a/src/Discord.Net.Core/Entities/Messages/IEmbed.cs b/src/Discord.Net.Core/Entities/Messages/IEmbed.cs index 5eef5ec9b..f390c4c28 100644 --- a/src/Discord.Net.Core/Entities/Messages/IEmbed.cs +++ b/src/Discord.Net.Core/Entities/Messages/IEmbed.cs @@ -6,9 +6,9 @@ namespace Discord public interface IEmbed { string Url { get; } - string Type { get; } string Title { get; } string Description { get; } + EmbedType Type { get; } DateTimeOffset? Timestamp { get; } Color? Color { get; } EmbedImage? Image { get; } diff --git a/src/Discord.Net.Rest/API/Common/Embed.cs b/src/Discord.Net.Rest/API/Common/Embed.cs index f6325efbb..7e8dd9b90 100644 --- a/src/Discord.Net.Rest/API/Common/Embed.cs +++ b/src/Discord.Net.Rest/API/Common/Embed.cs @@ -8,14 +8,14 @@ namespace Discord.API { [JsonProperty("title")] public string Title { get; set; } - [JsonProperty("type")] - public string Type { get; set; } [JsonProperty("description")] public string Description { get; set; } [JsonProperty("url")] public string Url { get; set; } [JsonProperty("color")] public uint? Color { get; set; } + [JsonProperty("type")] + public EmbedType Type { get; set; } [JsonProperty("timestamp")] public DateTimeOffset? Timestamp { get; set; } [JsonProperty("author")] diff --git a/src/Discord.Net.Rest/Entities/Messages/EmbedBuilder.cs b/src/Discord.Net.Rest/Entities/Messages/EmbedBuilder.cs index 98a191379..7f037c0c0 100644 --- a/src/Discord.Net.Rest/Entities/Messages/EmbedBuilder.cs +++ b/src/Discord.Net.Rest/Entities/Messages/EmbedBuilder.cs @@ -10,7 +10,7 @@ namespace Discord public EmbedBuilder() { - _embed = new Embed("rich"); + _embed = new Embed(EmbedType.Rich); Fields = new List(); } From 0550006d567fe28a52d6c63c03a3c28c843b9506 Mon Sep 17 00:00:00 2001 From: FiniteReality Date: Sat, 17 Jun 2017 15:10:35 +0100 Subject: [PATCH 204/243] Change wording of permission preconditions Also fix an issue where RequireBotPermission may throw if used in a non-guild channel which required guild permissions. --- .../Preconditions/RequireBotPermissionAttribute.cs | 8 +++++--- .../Preconditions/RequireUserPermissionAttribute.cs | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs index 82975a2f6..0f865e864 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs @@ -44,14 +44,16 @@ namespace Discord.Commands public override async Task CheckPermissions(ICommandContext context, CommandInfo command, IServiceProvider services) { - var guildUser = await context.Guild.GetCurrentUserAsync(); + IGuildUser guildUser = null; + if (context.Guild != null) + guildUser = await context.Guild.GetCurrentUserAsync().ConfigureAwait(false); if (GuildPermission.HasValue) { if (guildUser == null) return PreconditionResult.FromError("Command must be used in a guild channel"); if (!guildUser.GuildPermissions.Has(GuildPermission.Value)) - return PreconditionResult.FromError($"Command requires guild permission {GuildPermission.Value}"); + return PreconditionResult.FromError($"Bot requires guild permission {GuildPermission.Value}"); } if (ChannelPermission.HasValue) @@ -65,7 +67,7 @@ namespace Discord.Commands perms = ChannelPermissions.All(guildChannel); if (!perms.Has(ChannelPermission.Value)) - return PreconditionResult.FromError($"Command requires channel permission {ChannelPermission.Value}"); + return PreconditionResult.FromError($"Bot requires channel permission {ChannelPermission.Value}"); } return PreconditionResult.FromSuccess(); diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs index 44c69d76a..b7729b0c8 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs @@ -52,7 +52,7 @@ namespace Discord.Commands if (guildUser == null) return Task.FromResult(PreconditionResult.FromError("Command must be used in a guild channel")); if (!guildUser.GuildPermissions.Has(GuildPermission.Value)) - return Task.FromResult(PreconditionResult.FromError($"Command requires guild permission {GuildPermission.Value}")); + return Task.FromResult(PreconditionResult.FromError($"User requires guild permission {GuildPermission.Value}")); } if (ChannelPermission.HasValue) @@ -66,7 +66,7 @@ namespace Discord.Commands perms = ChannelPermissions.All(guildChannel); if (!perms.Has(ChannelPermission.Value)) - return Task.FromResult(PreconditionResult.FromError($"Command requires channel permission {ChannelPermission.Value}")); + return Task.FromResult(PreconditionResult.FromError($"User requires channel permission {ChannelPermission.Value}")); } return Task.FromResult(PreconditionResult.FromSuccess()); From 33e765f8f5c4fd9be839ab0fee418d4c62be1bef Mon Sep 17 00:00:00 2001 From: Christopher F Date: Sat, 17 Jun 2017 19:00:22 -0400 Subject: [PATCH 205/243] Use StringEnum converter in API model --- src/Discord.Net.Rest/API/Common/Embed.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net.Rest/API/Common/Embed.cs b/src/Discord.Net.Rest/API/Common/Embed.cs index 7e8dd9b90..1c9fa34e2 100644 --- a/src/Discord.Net.Rest/API/Common/Embed.cs +++ b/src/Discord.Net.Rest/API/Common/Embed.cs @@ -1,6 +1,7 @@ #pragma warning disable CS1591 using System; using Newtonsoft.Json; +using Newtonsoft.Json.Converters; namespace Discord.API { @@ -14,7 +15,7 @@ namespace Discord.API public string Url { get; set; } [JsonProperty("color")] public uint? Color { get; set; } - [JsonProperty("type")] + [JsonProperty("type"), JsonConverter(typeof(StringEnumConverter))] public EmbedType Type { get; set; } [JsonProperty("timestamp")] public DateTimeOffset? Timestamp { get; set; } From 759da09c38631efe66630036fa8aa61cd6e92fa8 Mon Sep 17 00:00:00 2001 From: Alex Gravely Date: Mon, 19 Jun 2017 15:21:46 -0400 Subject: [PATCH 206/243] Update events.cs Gladly taking suggestions for a better comments. --- docs/guides/concepts/samples/events.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/guides/concepts/samples/events.cs b/docs/guides/concepts/samples/events.cs index c662b51a9..f268b6e49 100644 --- a/docs/guides/concepts/samples/events.cs +++ b/docs/guides/concepts/samples/events.cs @@ -8,7 +8,11 @@ public class Program public async Task MainAsync() { - _client = new DiscordSocketClient(); + // When working with events that have Cacheable parameters, + // you must enable the message cache in your config settings if you plan to + // use the cached message entity. + _config = new DiscordSocketConfig { MessageCacheSize = 100 }; + _client = new DiscordSocketClient(_config); await _client.LoginAsync(TokenType.Bot, "bot token"); await _client.StartAsync(); @@ -25,7 +29,8 @@ public class Program private async Task MessageUpdated(Cacheable before, SocketMessage after, ISocketMessageChannel channel) { + // If the message was not in the cache, downloading it will result in getting a copy of `after`. var message = await before.GetOrDownloadAsync(); Console.WriteLine($"{message} -> {after}"); } -} \ No newline at end of file +} From 6e21d33999b493783a68500cabbffdefa7c986ab Mon Sep 17 00:00:00 2001 From: Alex Gravely Date: Tue, 20 Jun 2017 20:44:33 -0400 Subject: [PATCH 207/243] Update events.cs Forgot a var >_> --- docs/guides/concepts/samples/events.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/concepts/samples/events.cs b/docs/guides/concepts/samples/events.cs index f268b6e49..cf0492cb5 100644 --- a/docs/guides/concepts/samples/events.cs +++ b/docs/guides/concepts/samples/events.cs @@ -11,7 +11,7 @@ public class Program // When working with events that have Cacheable parameters, // you must enable the message cache in your config settings if you plan to // use the cached message entity. - _config = new DiscordSocketConfig { MessageCacheSize = 100 }; + var _config = new DiscordSocketConfig { MessageCacheSize = 100 }; _client = new DiscordSocketClient(_config); await _client.LoginAsync(TokenType.Bot, "bot token"); From 4a9c8168a9e395bc93b29722d682fdfe98ac2f33 Mon Sep 17 00:00:00 2001 From: Joe4evr Date: Fri, 23 Jun 2017 16:28:22 +0200 Subject: [PATCH 208/243] Add grouping of preconditions to allow for flexible precondition logic. (#672) * Add grouping of preconditions to allow for flexible precondition logic. * Fix checking Module Preconditions twice (and none of the command's own) * Fix command preconditions group 0 looping over every other precondition anyway #whoopsies * Use custom message when a non-zero Precondition Group fails. * Fix doc comment rendering. * Refactor loops into local function * Considering a new result type * Switch to IReadOnlyCollection and fix compiler errors * Revert PreconditionResult -> IResult in return types - Change PreconditionResult to a class that PreconditionGroupResult inherits. * Feedback on property name. * Change grouping type int -> string * Explicitly use an ordinal StringComparer * Full stops on error messages * Remove some sillyness. * Remove unneeded using. --- .../Attributes/PreconditionAttribute.cs | 7 +++ src/Discord.Net.Commands/CommandMatch.cs | 2 +- src/Discord.Net.Commands/Info/CommandInfo.cs | 46 +++++++++++++------ .../Results/PreconditionGroupResult.cs | 27 +++++++++++ .../Results/PreconditionResult.cs | 4 +- 5 files changed, 70 insertions(+), 16 deletions(-) create mode 100644 src/Discord.Net.Commands/Results/PreconditionGroupResult.cs diff --git a/src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs b/src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs index e099380f6..3727510d9 100644 --- a/src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs @@ -6,6 +6,13 @@ namespace Discord.Commands [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true, Inherited = true)] public abstract class PreconditionAttribute : Attribute { + /// + /// Specify a group that this precondition belongs to. Preconditions of the same group require only one + /// of the preconditions to pass in order to be successful (A || B). Specifying = + /// or not at all will require *all* preconditions to pass, just like normal (A && B). + /// + public string Group { get; set; } = null; + public abstract Task CheckPermissions(ICommandContext context, CommandInfo command, IServiceProvider services); } } diff --git a/src/Discord.Net.Commands/CommandMatch.cs b/src/Discord.Net.Commands/CommandMatch.cs index 04a2d040f..74c0de73e 100644 --- a/src/Discord.Net.Commands/CommandMatch.cs +++ b/src/Discord.Net.Commands/CommandMatch.cs @@ -18,7 +18,7 @@ namespace Discord.Commands public Task CheckPreconditionsAsync(ICommandContext context, IServiceProvider services = null) => Command.CheckPreconditionsAsync(context, services); - public Task ParseAsync(ICommandContext context, SearchResult searchResult, PreconditionResult? preconditionResult = null) + public Task ParseAsync(ICommandContext context, SearchResult searchResult, PreconditionResult preconditionResult = null) => Command.ParseAsync(context, Alias.Length, searchResult, preconditionResult); public Task ExecuteAsync(ICommandContext context, IEnumerable argList, IEnumerable paramList, IServiceProvider services) => Command.ExecuteAsync(context, argList, paramList, services); diff --git a/src/Discord.Net.Commands/Info/CommandInfo.cs b/src/Discord.Net.Commands/Info/CommandInfo.cs index 5acd1f648..ae350e592 100644 --- a/src/Discord.Net.Commands/Info/CommandInfo.cs +++ b/src/Discord.Net.Commands/Info/CommandInfo.cs @@ -68,29 +68,49 @@ namespace Discord.Commands { services = services ?? EmptyServiceProvider.Instance; - foreach (PreconditionAttribute precondition in Module.Preconditions) + async Task CheckGroups(IEnumerable preconditions, string type) { - var result = await precondition.CheckPermissions(context, this, services).ConfigureAwait(false); - if (!result.IsSuccess) - return result; + foreach (IGrouping preconditionGroup in preconditions.GroupBy(p => p.Group, StringComparer.Ordinal)) + { + if (preconditionGroup.Key == null) + { + foreach (PreconditionAttribute precondition in preconditionGroup) + { + var result = await precondition.CheckPermissions(context, this, services).ConfigureAwait(false); + if (!result.IsSuccess) + return PreconditionGroupResult.FromError($"{type} default precondition group failed.", new[] { result }); + } + } + else + { + var results = new List(); + foreach (PreconditionAttribute precondition in preconditionGroup) + results.Add(await precondition.CheckPermissions(context, this, services).ConfigureAwait(false)); + + if (!results.Any(p => p.IsSuccess)) + return PreconditionGroupResult.FromError($"{type} precondition group {preconditionGroup.Key} failed.", results); + } + } + return PreconditionGroupResult.FromSuccess(); } - foreach (PreconditionAttribute precondition in Preconditions) - { - var result = await precondition.CheckPermissions(context, this, services).ConfigureAwait(false); - if (!result.IsSuccess) - return result; - } + var moduleResult = await CheckGroups(Module.Preconditions, "Module"); + if (!moduleResult.IsSuccess) + return moduleResult; + + var commandResult = await CheckGroups(Preconditions, "Command"); + if (!commandResult.IsSuccess) + return commandResult; return PreconditionResult.FromSuccess(); } - public async Task ParseAsync(ICommandContext context, int startIndex, SearchResult searchResult, PreconditionResult? preconditionResult = null) + public async Task ParseAsync(ICommandContext context, int startIndex, SearchResult searchResult, PreconditionResult preconditionResult = null) { if (!searchResult.IsSuccess) return ParseResult.FromError(searchResult); - if (preconditionResult != null && !preconditionResult.Value.IsSuccess) - return ParseResult.FromError(preconditionResult.Value); + if (preconditionResult != null && !preconditionResult.IsSuccess) + return ParseResult.FromError(preconditionResult); string input = searchResult.Text.Substring(startIndex); return await CommandParser.ParseArgs(this, context, input, 0).ConfigureAwait(false); diff --git a/src/Discord.Net.Commands/Results/PreconditionGroupResult.cs b/src/Discord.Net.Commands/Results/PreconditionGroupResult.cs new file mode 100644 index 000000000..1d7f29122 --- /dev/null +++ b/src/Discord.Net.Commands/Results/PreconditionGroupResult.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using System.Diagnostics; + +namespace Discord.Commands +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class PreconditionGroupResult : PreconditionResult + { + public IReadOnlyCollection PreconditionResults { get; } + + protected PreconditionGroupResult(CommandError? error, string errorReason, ICollection preconditions) + : base(error, errorReason) + { + PreconditionResults = (preconditions ?? new List(0)).ToReadOnlyCollection(); + } + + public static new PreconditionGroupResult FromSuccess() + => new PreconditionGroupResult(null, null, null); + public static PreconditionGroupResult FromError(string reason, ICollection preconditions) + => new PreconditionGroupResult(CommandError.UnmetPrecondition, reason, preconditions); + public static new PreconditionGroupResult FromError(IResult result) //needed? + => new PreconditionGroupResult(result.Error, result.ErrorReason, null); + + public override string ToString() => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; + private string DebuggerDisplay => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; + } +} diff --git a/src/Discord.Net.Commands/Results/PreconditionResult.cs b/src/Discord.Net.Commands/Results/PreconditionResult.cs index 77ba1b5b9..ca65a373e 100644 --- a/src/Discord.Net.Commands/Results/PreconditionResult.cs +++ b/src/Discord.Net.Commands/Results/PreconditionResult.cs @@ -3,14 +3,14 @@ namespace Discord.Commands { [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public struct PreconditionResult : IResult + public class PreconditionResult : IResult { public CommandError? Error { get; } public string ErrorReason { get; } public bool IsSuccess => !Error.HasValue; - private PreconditionResult(CommandError? error, string errorReason) + protected PreconditionResult(CommandError? error, string errorReason) { Error = error; ErrorReason = errorReason; From cce572c6000cb2d2572a176d73c402c800913ec4 Mon Sep 17 00:00:00 2001 From: Finite Reality Date: Fri, 23 Jun 2017 15:28:30 +0100 Subject: [PATCH 209/243] Include names in command builder exceptions (#663) --- src/Discord.Net.Commands/Builders/CommandBuilder.cs | 4 ++-- src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs | 2 +- src/Discord.Net.Commands/Builders/ParameterBuilder.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Discord.Net.Commands/Builders/CommandBuilder.cs b/src/Discord.Net.Commands/Builders/CommandBuilder.cs index ff89b7559..8c2207f10 100644 --- a/src/Discord.Net.Commands/Builders/CommandBuilder.cs +++ b/src/Discord.Net.Commands/Builders/CommandBuilder.cs @@ -122,11 +122,11 @@ namespace Discord.Commands.Builders var firstMultipleParam = _parameters.FirstOrDefault(x => x.IsMultiple); if ((firstMultipleParam != null) && (firstMultipleParam != lastParam)) - throw new InvalidOperationException("Only the last parameter in a command may have the Multiple flag."); + throw new InvalidOperationException($"Only the last parameter in a command may have the Multiple flag. Parameter: {firstMultipleParam.Name} in {PrimaryAlias}"); var firstRemainderParam = _parameters.FirstOrDefault(x => x.IsRemainder); if ((firstRemainderParam != null) && (firstRemainderParam != lastParam)) - throw new InvalidOperationException("Only the last parameter in a command may have the Remainder flag."); + throw new InvalidOperationException($"Only the last parameter in a command may have the Remainder flag. Parameter: {firstRemainderParam.Name} in {PrimaryAlias}"); } return new CommandInfo(this, info, service); diff --git a/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs index d8464ea72..fe35e3b2a 100644 --- a/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs +++ b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs @@ -210,7 +210,7 @@ namespace Discord.Commands else if (attribute is RemainderAttribute) { if (position != count-1) - throw new InvalidOperationException("Remainder parameters must be the last parameter in a command."); + throw new InvalidOperationException($"Remainder parameters must be the last parameter in a command. Parameter: {paramInfo.Name} in {paramInfo.Member.DeclaringType.Name}.{paramInfo.Member.Name}"); builder.IsRemainder = true; } diff --git a/src/Discord.Net.Commands/Builders/ParameterBuilder.cs b/src/Discord.Net.Commands/Builders/ParameterBuilder.cs index 6761033b0..d2bebbad0 100644 --- a/src/Discord.Net.Commands/Builders/ParameterBuilder.cs +++ b/src/Discord.Net.Commands/Builders/ParameterBuilder.cs @@ -49,7 +49,7 @@ namespace Discord.Commands.Builders TypeReader = Command.Module.Service.GetDefaultTypeReader(type); if (TypeReader == null) - throw new InvalidOperationException($"{type} does not have a TypeReader registered for it"); + throw new InvalidOperationException($"{type} does not have a TypeReader registered for it. Parameter: {Name} in {Command.PrimaryAlias}"); if (type.GetTypeInfo().IsValueType) DefaultValue = Activator.CreateInstance(type); From fb0a056d76bab60ecf9dfe3ca7d13aa7f859c6cf Mon Sep 17 00:00:00 2001 From: Christopher F Date: Fri, 23 Jun 2017 10:29:39 -0400 Subject: [PATCH 210/243] Add IUser#SendMessageAsync extension (#706) * Add IUser#SendMessageAsync extension * Add ConfigureAwait --- .../Extensions/UserExtensions.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 src/Discord.Net.Core/Extensions/UserExtensions.cs diff --git a/src/Discord.Net.Core/Extensions/UserExtensions.cs b/src/Discord.Net.Core/Extensions/UserExtensions.cs new file mode 100644 index 000000000..0861ed33e --- /dev/null +++ b/src/Discord.Net.Core/Extensions/UserExtensions.cs @@ -0,0 +1,16 @@ +using System.Threading.Tasks; + +namespace Discord +{ + public static class UserExtensions + { + public static async Task SendMessageAsync(this IUser user, + string text, + bool isTTS = false, + Embed embed = null, + RequestOptions options = null) + { + return await (await user.GetOrCreateDMChannelAsync().ConfigureAwait(false)).SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); + } + } +} From 5f04e2beba12904c2a4839eac28e648e8b3669d0 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Fri, 23 Jun 2017 10:29:45 -0400 Subject: [PATCH 211/243] Cache outgoing presence data if disconnected (#705) This resolves #702 --- src/Discord.Net.WebSocket/DiscordSocketClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index d42df7b55..f9458caff 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -360,7 +360,7 @@ namespace Discord.WebSocket private async Task SendStatusAsync() { if (CurrentUser == null) - throw new InvalidOperationException("Presence data cannot be sent before the client has logged in."); + return; var game = Game; var status = Status; var statusSince = _statusSince; From 5601d002851e82ea919cd81923f3423ce7ef0bb1 Mon Sep 17 00:00:00 2001 From: Pat Murphy Date: Fri, 23 Jun 2017 07:29:55 -0700 Subject: [PATCH 212/243] Add various property validation in EmbedBuilder (#711) * Add various property validation in EmbedBuilder * Embed URI changes Changes property types for any URLs in Embeds to System.URI. Adding field name/value null/empty checks. * including property names in argumentexceptions * Adds overall embed length check --- .../Entities/Messages/Embed.cs | 7 +- .../Entities/Messages/EmbedAuthor.cs | 11 +- .../Entities/Messages/EmbedFooter.cs | 9 +- .../Entities/Messages/EmbedImage.cs | 11 +- .../Entities/Messages/EmbedProvider.cs | 7 +- .../Entities/Messages/EmbedThumbnail.cs | 11 +- .../Entities/Messages/EmbedVideo.cs | 9 +- .../Entities/Messages/IEmbed.cs | 2 +- src/Discord.Net.Rest/API/Common/Embed.cs | 2 +- .../API/Common/EmbedAuthor.cs | 9 +- .../API/Common/EmbedFooter.cs | 7 +- src/Discord.Net.Rest/API/Common/EmbedImage.cs | 5 +- .../API/Common/EmbedProvider.cs | 3 +- .../API/Common/EmbedThumbnail.cs | 5 +- src/Discord.Net.Rest/API/Common/EmbedVideo.cs | 3 +- .../Entities/Messages/EmbedBuilder.cs | 131 ++++++++++++++---- 16 files changed, 163 insertions(+), 69 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Messages/Embed.cs b/src/Discord.Net.Core/Entities/Messages/Embed.cs index ebde05d4c..3210c22f5 100644 --- a/src/Discord.Net.Core/Entities/Messages/Embed.cs +++ b/src/Discord.Net.Core/Entities/Messages/Embed.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Immutable; using System.Diagnostics; +using System.Linq; namespace Discord { @@ -10,7 +11,7 @@ namespace Discord public string Type { get; } public string Description { get; internal set; } - public string Url { get; internal set; } + public Uri Url { get; internal set; } public string Title { get; internal set; } public DateTimeOffset? Timestamp { get; internal set; } public Color? Color { get; internal set; } @@ -30,7 +31,7 @@ namespace Discord internal Embed(string type, string title, string description, - string url, + Uri url, DateTimeOffset? timestamp, Color? color, EmbedImage? image, @@ -56,6 +57,8 @@ namespace Discord Fields = fields; } + public int Length => Title?.Length + Author?.Name?.Length + Description?.Length + Footer?.Text?.Length + Fields.Sum(f => f.Name.Length + f.Value.ToString().Length) ?? 0; + public override string ToString() => Title; private string DebuggerDisplay => $"{Title} ({Type})"; } diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedAuthor.cs b/src/Discord.Net.Core/Entities/Messages/EmbedAuthor.cs index 142e36832..d1f2b9618 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedAuthor.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedAuthor.cs @@ -1,4 +1,5 @@ -using System.Diagnostics; +using System; +using System.Diagnostics; namespace Discord { @@ -6,11 +7,11 @@ namespace Discord public struct EmbedAuthor { public string Name { get; internal set; } - public string Url { get; internal set; } - public string IconUrl { get; internal set; } - public string ProxyIconUrl { get; internal set; } + public Uri Url { get; internal set; } + public Uri IconUrl { get; internal set; } + public Uri ProxyIconUrl { get; internal set; } - internal EmbedAuthor(string name, string url, string iconUrl, string proxyIconUrl) + internal EmbedAuthor(string name, Uri url, Uri iconUrl, Uri proxyIconUrl) { Name = name; Url = url; diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedFooter.cs b/src/Discord.Net.Core/Entities/Messages/EmbedFooter.cs index 33582070a..3c9bf35a9 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedFooter.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedFooter.cs @@ -1,4 +1,5 @@ -using System.Diagnostics; +using System; +using System.Diagnostics; namespace Discord { @@ -6,10 +7,10 @@ namespace Discord public struct EmbedFooter { public string Text { get; internal set; } - public string IconUrl { get; internal set; } - public string ProxyUrl { get; internal set; } + public Uri IconUrl { get; internal set; } + public Uri ProxyUrl { get; internal set; } - internal EmbedFooter(string text, string iconUrl, string proxyUrl) + internal EmbedFooter(string text, Uri iconUrl, Uri proxyUrl) { Text = text; IconUrl = iconUrl; diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedImage.cs b/src/Discord.Net.Core/Entities/Messages/EmbedImage.cs index fa4847721..fd87e3db3 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedImage.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedImage.cs @@ -1,16 +1,17 @@ -using System.Diagnostics; +using System; +using System.Diagnostics; namespace Discord { [DebuggerDisplay("{DebuggerDisplay,nq}")] public struct EmbedImage { - public string Url { get; } - public string ProxyUrl { get; } + public Uri Url { get; } + public Uri ProxyUrl { get; } public int? Height { get; } public int? Width { get; } - internal EmbedImage(string url, string proxyUrl, int? height, int? width) + internal EmbedImage(Uri url, Uri proxyUrl, int? height, int? width) { Url = url; ProxyUrl = proxyUrl; @@ -19,6 +20,6 @@ namespace Discord } private string DebuggerDisplay => $"{Url} ({(Width != null && Height != null ? $"{Width}x{Height}" : "0x0")})"; - public override string ToString() => Url; + public override string ToString() => Url.ToString(); } } diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedProvider.cs b/src/Discord.Net.Core/Entities/Messages/EmbedProvider.cs index 943ac5b52..0b816b32b 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedProvider.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedProvider.cs @@ -1,4 +1,5 @@ -using System.Diagnostics; +using System; +using System.Diagnostics; namespace Discord { @@ -6,9 +7,9 @@ namespace Discord public struct EmbedProvider { public string Name { get; } - public string Url { get; } + public Uri Url { get; } - internal EmbedProvider(string name, string url) + internal EmbedProvider(string name, Uri url) { Name = name; Url = url; diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedThumbnail.cs b/src/Discord.Net.Core/Entities/Messages/EmbedThumbnail.cs index 4e125bf2a..b83401e07 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedThumbnail.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedThumbnail.cs @@ -1,16 +1,17 @@ -using System.Diagnostics; +using System; +using System.Diagnostics; namespace Discord { [DebuggerDisplay("{DebuggerDisplay,nq}")] public struct EmbedThumbnail { - public string Url { get; } - public string ProxyUrl { get; } + public Uri Url { get; } + public Uri ProxyUrl { get; } public int? Height { get; } public int? Width { get; } - internal EmbedThumbnail(string url, string proxyUrl, int? height, int? width) + internal EmbedThumbnail(Uri url, Uri proxyUrl, int? height, int? width) { Url = url; ProxyUrl = proxyUrl; @@ -19,6 +20,6 @@ namespace Discord } private string DebuggerDisplay => $"{Url} ({(Width != null && Height != null ? $"{Width}x{Height}" : "0x0")})"; - public override string ToString() => Url; + public override string ToString() => Url.ToString(); } } diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedVideo.cs b/src/Discord.Net.Core/Entities/Messages/EmbedVideo.cs index eaf6f4a4c..9ea4b11d6 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedVideo.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedVideo.cs @@ -1,15 +1,16 @@ -using System.Diagnostics; +using System; +using System.Diagnostics; namespace Discord { [DebuggerDisplay("{DebuggerDisplay,nq}")] public struct EmbedVideo { - public string Url { get; } + public Uri Url { get; } public int? Height { get; } public int? Width { get; } - internal EmbedVideo(string url, int? height, int? width) + internal EmbedVideo(Uri url, int? height, int? width) { Url = url; Height = height; @@ -17,6 +18,6 @@ namespace Discord } private string DebuggerDisplay => $"{Url} ({(Width != null && Height != null ? $"{Width}x{Height}" : "0x0")})"; - public override string ToString() => Url; + public override string ToString() => Url.ToString(); } } diff --git a/src/Discord.Net.Core/Entities/Messages/IEmbed.cs b/src/Discord.Net.Core/Entities/Messages/IEmbed.cs index 5eef5ec9b..145b1fa3c 100644 --- a/src/Discord.Net.Core/Entities/Messages/IEmbed.cs +++ b/src/Discord.Net.Core/Entities/Messages/IEmbed.cs @@ -5,7 +5,7 @@ namespace Discord { public interface IEmbed { - string Url { get; } + Uri Url { get; } string Type { get; } string Title { get; } string Description { get; } diff --git a/src/Discord.Net.Rest/API/Common/Embed.cs b/src/Discord.Net.Rest/API/Common/Embed.cs index f6325efbb..110c5ec8d 100644 --- a/src/Discord.Net.Rest/API/Common/Embed.cs +++ b/src/Discord.Net.Rest/API/Common/Embed.cs @@ -13,7 +13,7 @@ namespace Discord.API [JsonProperty("description")] public string Description { get; set; } [JsonProperty("url")] - public string Url { get; set; } + public Uri Url { get; set; } [JsonProperty("color")] public uint? Color { get; set; } [JsonProperty("timestamp")] diff --git a/src/Discord.Net.Rest/API/Common/EmbedAuthor.cs b/src/Discord.Net.Rest/API/Common/EmbedAuthor.cs index e69fee6eb..9ade58edf 100644 --- a/src/Discord.Net.Rest/API/Common/EmbedAuthor.cs +++ b/src/Discord.Net.Rest/API/Common/EmbedAuthor.cs @@ -1,4 +1,5 @@ -using Newtonsoft.Json; +using System; +using Newtonsoft.Json; namespace Discord.API { @@ -7,10 +8,10 @@ namespace Discord.API [JsonProperty("name")] public string Name { get; set; } [JsonProperty("url")] - public string Url { get; set; } + public Uri Url { get; set; } [JsonProperty("icon_url")] - public string IconUrl { get; set; } + public Uri IconUrl { get; set; } [JsonProperty("proxy_icon_url")] - public string ProxyIconUrl { get; set; } + public Uri ProxyIconUrl { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Common/EmbedFooter.cs b/src/Discord.Net.Rest/API/Common/EmbedFooter.cs index 27048972e..1e079d03e 100644 --- a/src/Discord.Net.Rest/API/Common/EmbedFooter.cs +++ b/src/Discord.Net.Rest/API/Common/EmbedFooter.cs @@ -1,4 +1,5 @@ -using Newtonsoft.Json; +using System; +using Newtonsoft.Json; namespace Discord.API { @@ -7,8 +8,8 @@ namespace Discord.API [JsonProperty("text")] public string Text { get; set; } [JsonProperty("icon_url")] - public string IconUrl { get; set; } + public Uri IconUrl { get; set; } [JsonProperty("proxy_icon_url")] - public string ProxyIconUrl { get; set; } + public Uri ProxyIconUrl { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Common/EmbedImage.cs b/src/Discord.Net.Rest/API/Common/EmbedImage.cs index a5ef748f8..a12299783 100644 --- a/src/Discord.Net.Rest/API/Common/EmbedImage.cs +++ b/src/Discord.Net.Rest/API/Common/EmbedImage.cs @@ -1,4 +1,5 @@ #pragma warning disable CS1591 +using System; using Newtonsoft.Json; namespace Discord.API @@ -6,9 +7,9 @@ namespace Discord.API internal class EmbedImage { [JsonProperty("url")] - public string Url { get; set; } + public Uri Url { get; set; } [JsonProperty("proxy_url")] - public string ProxyUrl { get; set; } + public Uri ProxyUrl { get; set; } [JsonProperty("height")] public Optional Height { get; set; } [JsonProperty("width")] diff --git a/src/Discord.Net.Rest/API/Common/EmbedProvider.cs b/src/Discord.Net.Rest/API/Common/EmbedProvider.cs index 8c46b10dc..7ca87185c 100644 --- a/src/Discord.Net.Rest/API/Common/EmbedProvider.cs +++ b/src/Discord.Net.Rest/API/Common/EmbedProvider.cs @@ -1,4 +1,5 @@ #pragma warning disable CS1591 +using System; using Newtonsoft.Json; namespace Discord.API @@ -8,6 +9,6 @@ namespace Discord.API [JsonProperty("name")] public string Name { get; set; } [JsonProperty("url")] - public string Url { get; set; } + public Uri Url { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Common/EmbedThumbnail.cs b/src/Discord.Net.Rest/API/Common/EmbedThumbnail.cs index f22953a25..b4ccd4b21 100644 --- a/src/Discord.Net.Rest/API/Common/EmbedThumbnail.cs +++ b/src/Discord.Net.Rest/API/Common/EmbedThumbnail.cs @@ -1,4 +1,5 @@ #pragma warning disable CS1591 +using System; using Newtonsoft.Json; namespace Discord.API @@ -6,9 +7,9 @@ namespace Discord.API internal class EmbedThumbnail { [JsonProperty("url")] - public string Url { get; set; } + public Uri Url { get; set; } [JsonProperty("proxy_url")] - public string ProxyUrl { get; set; } + public Uri ProxyUrl { get; set; } [JsonProperty("height")] public Optional Height { get; set; } [JsonProperty("width")] diff --git a/src/Discord.Net.Rest/API/Common/EmbedVideo.cs b/src/Discord.Net.Rest/API/Common/EmbedVideo.cs index 09e933784..2512151ed 100644 --- a/src/Discord.Net.Rest/API/Common/EmbedVideo.cs +++ b/src/Discord.Net.Rest/API/Common/EmbedVideo.cs @@ -1,4 +1,5 @@ #pragma warning disable CS1591 +using System; using Newtonsoft.Json; namespace Discord.API @@ -6,7 +7,7 @@ namespace Discord.API internal class EmbedVideo { [JsonProperty("url")] - public string Url { get; set; } + public Uri Url { get; set; } [JsonProperty("height")] public Optional Height { get; set; } [JsonProperty("width")] diff --git a/src/Discord.Net.Rest/Entities/Messages/EmbedBuilder.cs b/src/Discord.Net.Rest/Entities/Messages/EmbedBuilder.cs index 98a191379..a7c2436b0 100644 --- a/src/Discord.Net.Rest/Entities/Messages/EmbedBuilder.cs +++ b/src/Discord.Net.Rest/Entities/Messages/EmbedBuilder.cs @@ -8,19 +8,42 @@ namespace Discord { private readonly Embed _embed; + public const int MaxFieldCount = 25; + public const int MaxTitleLength = 256; + public const int MaxDescriptionLength = 2048; + public const int MaxEmbedLength = 6000; // user bot limit is 2000, but we don't validate that here. + public EmbedBuilder() { _embed = new Embed("rich"); Fields = new List(); } - public string Title { get { return _embed.Title; } set { _embed.Title = value; } } - public string Description { get { return _embed.Description; } set { _embed.Description = value; } } - public string Url { get { return _embed.Url; } set { _embed.Url = value; } } - public string ThumbnailUrl { get { return _embed.Thumbnail?.Url; } set { _embed.Thumbnail = new EmbedThumbnail(value, null, null, null); } } - public string ImageUrl { get { return _embed.Image?.Url; } set { _embed.Image = new EmbedImage(value, null, null, null); } } - public DateTimeOffset? Timestamp { get { return _embed.Timestamp; } set { _embed.Timestamp = value; } } - public Color? Color { get { return _embed.Color; } set { _embed.Color = value; } } + public string Title + { + get => _embed.Title; + set + { + if (value?.Length > MaxTitleLength) throw new ArgumentException($"Title length must be less than or equal to {MaxTitleLength}.", nameof(Title)); + _embed.Title = value; + } + } + + public string Description + { + get => _embed.Description; + set + { + if (value?.Length > MaxDescriptionLength) throw new ArgumentException($"Description length must be less than or equal to {MaxDescriptionLength}.", nameof(Description)); + _embed.Description = value; + } + } + + public Uri Url { get => _embed.Url; set { _embed.Url = value; } } + public Uri ThumbnailUrl { get => _embed.Thumbnail?.Url; set { _embed.Thumbnail = new EmbedThumbnail(value, null, null, null); } } + public Uri ImageUrl { get => _embed.Image?.Url; set { _embed.Image = new EmbedImage(value, null, null, null); } } + public DateTimeOffset? Timestamp { get => _embed.Timestamp; set { _embed.Timestamp = value; } } + public Color? Color { get => _embed.Color; set { _embed.Color = value; } } public EmbedAuthorBuilder Author { get; set; } public EmbedFooterBuilder Footer { get; set; } @@ -30,8 +53,10 @@ namespace Discord get => _fields; set { - if (value != null) _fields = value; - else throw new ArgumentNullException("Cannot set an embed builder's fields collection to null", nameof(value)); + + if (value == null) throw new ArgumentNullException("Cannot set an embed builder's fields collection to null", nameof(Fields)); + if (value.Count > MaxFieldCount) throw new ArgumentException($"Field count must be less than or equal to {MaxFieldCount}.", nameof(Fields)); + _fields = value; } } @@ -45,17 +70,17 @@ namespace Discord Description = description; return this; } - public EmbedBuilder WithUrl(string url) + public EmbedBuilder WithUrl(Uri url) { Url = url; return this; } - public EmbedBuilder WithThumbnailUrl(string thumbnailUrl) + public EmbedBuilder WithThumbnailUrl(Uri thumbnailUrl) { ThumbnailUrl = thumbnailUrl; return this; } - public EmbedBuilder WithImageUrl(string imageUrl) + public EmbedBuilder WithImageUrl(Uri imageUrl) { ImageUrl = imageUrl; return this; @@ -107,7 +132,7 @@ namespace Discord .WithIsInline(false) .WithName(name) .WithValue(value); - Fields.Add(field); + AddField(field); return this; } public EmbedBuilder AddInlineField(string name, object value) @@ -116,11 +141,16 @@ namespace Discord .WithIsInline(true) .WithName(name) .WithValue(value); - Fields.Add(field); + AddField(field); return this; } public EmbedBuilder AddField(EmbedFieldBuilder field) { + if (Fields.Count >= MaxFieldCount) + { + throw new ArgumentException($"Field count must be less than or equal to {MaxFieldCount}.", nameof(field)); + } + Fields.Add(field); return this; } @@ -128,7 +158,7 @@ namespace Discord { var field = new EmbedFieldBuilder(); action(field); - Fields.Add(field); + this.AddField(field); return this; } @@ -140,6 +170,12 @@ namespace Discord for (int i = 0; i < Fields.Count; i++) fields.Add(Fields[i].Build()); _embed.Fields = fields.ToImmutable(); + + if (_embed.Length > MaxEmbedLength) + { + throw new InvalidOperationException($"Total embed length must be less than or equal to {MaxEmbedLength}"); + } + return _embed; } public static implicit operator Embed(EmbedBuilder builder) => builder?.Build(); @@ -149,9 +185,32 @@ namespace Discord { private EmbedField _field; - public string Name { get { return _field.Name; } set { _field.Name = value; } } - public object Value { get { return _field.Value; } set { _field.Value = value.ToString(); } } - public bool IsInline { get { return _field.Inline; } set { _field.Inline = value; } } + public const int MaxFieldNameLength = 256; + public const int MaxFieldValueLength = 1024; + + public string Name + { + get => _field.Name; + set + { + if (string.IsNullOrEmpty(value)) throw new ArgumentException($"Field name must not be null or empty.", nameof(Name)); + if (value.Length > MaxFieldNameLength) throw new ArgumentException($"Field name length must be less than or equal to {MaxFieldNameLength}.", nameof(Name)); + _field.Name = value; + } + } + + public object Value + { + get => _field.Value; + set + { + var stringValue = value.ToString(); + if (string.IsNullOrEmpty(stringValue)) throw new ArgumentException($"Field value must not be null or empty.", nameof(Value)); + if (stringValue.Length > MaxFieldValueLength) throw new ArgumentException($"Field value length must be less than or equal to {MaxFieldValueLength}.", nameof(Value)); + _field.Value = stringValue; + } + } + public bool IsInline { get => _field.Inline; set { _field.Inline = value; } } public EmbedFieldBuilder() { @@ -182,9 +241,19 @@ namespace Discord { private EmbedAuthor _author; - public string Name { get { return _author.Name; } set { _author.Name = value; } } - public string Url { get { return _author.Url; } set { _author.Url = value; } } - public string IconUrl { get { return _author.IconUrl; } set { _author.IconUrl = value; } } + public const int MaxAuthorNameLength = 256; + + public string Name + { + get => _author.Name; + set + { + if (value?.Length > MaxAuthorNameLength) throw new ArgumentException($"Author name length must be less than or equal to {MaxAuthorNameLength}.", nameof(Name)); + _author.Name = value; + } + } + public Uri Url { get => _author.Url; set { _author.Url = value; } } + public Uri IconUrl { get => _author.IconUrl; set { _author.IconUrl = value; } } public EmbedAuthorBuilder() { @@ -196,12 +265,12 @@ namespace Discord Name = name; return this; } - public EmbedAuthorBuilder WithUrl(string url) + public EmbedAuthorBuilder WithUrl(Uri url) { Url = url; return this; } - public EmbedAuthorBuilder WithIconUrl(string iconUrl) + public EmbedAuthorBuilder WithIconUrl(Uri iconUrl) { IconUrl = iconUrl; return this; @@ -215,8 +284,18 @@ namespace Discord { private EmbedFooter _footer; - public string Text { get { return _footer.Text; } set { _footer.Text = value; } } - public string IconUrl { get { return _footer.IconUrl; } set { _footer.IconUrl = value; } } + public const int MaxFooterTextLength = 2048; + + public string Text + { + get => _footer.Text; + set + { + if (value?.Length > MaxFooterTextLength) throw new ArgumentException($"Footer text length must be less than or equal to {MaxFooterTextLength}.", nameof(Text)); + _footer.Text = value; + } + } + public Uri IconUrl { get => _footer.IconUrl; set { _footer.IconUrl = value; } } public EmbedFooterBuilder() { @@ -228,7 +307,7 @@ namespace Discord Text = text; return this; } - public EmbedFooterBuilder WithIconUrl(string iconUrl) + public EmbedFooterBuilder WithIconUrl(Uri iconUrl) { IconUrl = iconUrl; return this; From d088d7b05c4e398333d928ad5a5940cb6f8870b0 Mon Sep 17 00:00:00 2001 From: Amir Zaidi Date: Fri, 23 Jun 2017 16:48:42 +0200 Subject: [PATCH 213/243] Add packetLoss argument for PCM streams, change FrameBytes to FrameSamplesPerChannel in OpusEncodeStream (#677) --- src/Discord.Net.Core/Audio/IAudioClient.cs | 4 ++-- src/Discord.Net.WebSocket/Audio/AudioClient.cs | 8 ++++---- src/Discord.Net.WebSocket/Audio/Opus/OpusEncoder.cs | 4 ++-- .../Audio/Streams/OpusEncodeStream.cs | 12 ++++++------ 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Discord.Net.Core/Audio/IAudioClient.cs b/src/Discord.Net.Core/Audio/IAudioClient.cs index 7373a8e4d..9be8ceef5 100644 --- a/src/Discord.Net.Core/Audio/IAudioClient.cs +++ b/src/Discord.Net.Core/Audio/IAudioClient.cs @@ -28,8 +28,8 @@ namespace Discord.Audio /// Creates a new outgoing stream accepting Opus-encoded data. This is a direct stream with no internal timer. AudioOutStream CreateDirectOpusStream(); /// Creates a new outgoing stream accepting PCM (raw) data. - AudioOutStream CreatePCMStream(AudioApplication application, int? bitrate = null, int bufferMillis = 1000); + AudioOutStream CreatePCMStream(AudioApplication application, int? bitrate = null, int bufferMillis = 1000, int packetLoss = 30); /// Creates a new direct outgoing stream accepting PCM (raw) data. This is a direct stream with no internal timer. - AudioOutStream CreateDirectPCMStream(AudioApplication application, int? bitrate = null); + AudioOutStream CreateDirectPCMStream(AudioApplication application, int? bitrate = null, int packetLoss = 30); } } diff --git a/src/Discord.Net.WebSocket/Audio/AudioClient.cs b/src/Discord.Net.WebSocket/Audio/AudioClient.cs index 0ca45a557..1f33b3cc5 100644 --- a/src/Discord.Net.WebSocket/Audio/AudioClient.cs +++ b/src/Discord.Net.WebSocket/Audio/AudioClient.cs @@ -153,20 +153,20 @@ namespace Discord.Audio var sodiumEncrypter = new SodiumEncryptStream(outputStream, this); //Passes header return new RTPWriteStream(sodiumEncrypter, _ssrc); //Consumes header (external input), passes } - public AudioOutStream CreatePCMStream(AudioApplication application, int? bitrate, int bufferMillis) + public AudioOutStream CreatePCMStream(AudioApplication application, int? bitrate, int bufferMillis, int packetLoss) { var outputStream = new OutputStream(ApiClient); //Ignores header var sodiumEncrypter = new SodiumEncryptStream(outputStream, this); //Passes header var rtpWriter = new RTPWriteStream(sodiumEncrypter, _ssrc); //Consumes header, passes var bufferedStream = new BufferedWriteStream(rtpWriter, this, bufferMillis, _connection.CancelToken, _audioLogger); //Ignores header, generates header - return new OpusEncodeStream(bufferedStream, bitrate ?? (96 * 1024), application); //Generates header + return new OpusEncodeStream(bufferedStream, bitrate ?? (96 * 1024), application, packetLoss); //Generates header } - public AudioOutStream CreateDirectPCMStream(AudioApplication application, int? bitrate) + public AudioOutStream CreateDirectPCMStream(AudioApplication application, int? bitrate, int packetLoss) { var outputStream = new OutputStream(ApiClient); //Ignores header var sodiumEncrypter = new SodiumEncryptStream(outputStream, this); //Passes header var rtpWriter = new RTPWriteStream(sodiumEncrypter, _ssrc); //Consumes header, passes - return new OpusEncodeStream(rtpWriter, bitrate ?? (96 * 1024), application); //Generates header + return new OpusEncodeStream(rtpWriter, bitrate ?? (96 * 1024), application, packetLoss); //Generates header } internal async Task CreateInputStreamAsync(ulong userId) diff --git a/src/Discord.Net.WebSocket/Audio/Opus/OpusEncoder.cs b/src/Discord.Net.WebSocket/Audio/Opus/OpusEncoder.cs index a12854d69..1ff5a5d9a 100644 --- a/src/Discord.Net.WebSocket/Audio/Opus/OpusEncoder.cs +++ b/src/Discord.Net.WebSocket/Audio/Opus/OpusEncoder.cs @@ -17,7 +17,7 @@ namespace Discord.Audio public AudioApplication Application { get; } public int BitRate { get;} - public OpusEncoder(int bitrate, AudioApplication application) + public OpusEncoder(int bitrate, AudioApplication application, int packetLoss) { if (bitrate < 1 || bitrate > DiscordVoiceAPIClient.MaxBitrate) throw new ArgumentOutOfRangeException(nameof(bitrate)); @@ -48,7 +48,7 @@ namespace Discord.Audio _ptr = CreateEncoder(SamplingRate, Channels, (int)opusApplication, out var error); CheckError(error); CheckError(EncoderCtl(_ptr, OpusCtl.SetSignal, (int)opusSignal)); - CheckError(EncoderCtl(_ptr, OpusCtl.SetPacketLossPercent, 30)); //% + CheckError(EncoderCtl(_ptr, OpusCtl.SetPacketLossPercent, packetLoss)); //% CheckError(EncoderCtl(_ptr, OpusCtl.SetInbandFEC, 1)); //True CheckError(EncoderCtl(_ptr, OpusCtl.SetBitrate, bitrate)); } diff --git a/src/Discord.Net.WebSocket/Audio/Streams/OpusEncodeStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/OpusEncodeStream.cs index a7779a84c..f5883ad4b 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/OpusEncodeStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/OpusEncodeStream.cs @@ -8,18 +8,18 @@ namespace Discord.Audio.Streams public class OpusEncodeStream : AudioOutStream { public const int SampleRate = 48000; - + private readonly AudioStream _next; private readonly OpusEncoder _encoder; private readonly byte[] _buffer; private int _partialFramePos; private ushort _seq; private uint _timestamp; - - public OpusEncodeStream(AudioStream next, int bitrate, AudioApplication application) + + public OpusEncodeStream(AudioStream next, int bitrate, AudioApplication application, int packetLoss) { _next = next; - _encoder = new OpusEncoder(bitrate, application); + _encoder = new OpusEncoder(bitrate, application, packetLoss); _buffer = new byte[OpusConverter.FrameBytes]; } @@ -38,7 +38,7 @@ namespace Discord.Audio.Streams offset += OpusConverter.FrameBytes; count -= OpusConverter.FrameBytes; _seq++; - _timestamp += OpusConverter.FrameBytes; + _timestamp += OpusConverter.FrameSamplesPerChannel; } else if (_partialFramePos + count >= OpusConverter.FrameBytes) { @@ -53,7 +53,7 @@ namespace Discord.Audio.Streams count -= partialSize; _partialFramePos = 0; _seq++; - _timestamp += OpusConverter.FrameBytes; + _timestamp += OpusConverter.FrameSamplesPerChannel; } else { From 707ec9571786db888dc01e804a30265b6f355931 Mon Sep 17 00:00:00 2001 From: Alex Gravely Date: Fri, 23 Jun 2017 11:01:44 -0400 Subject: [PATCH 214/243] Add SocketRole.Members property (#659) * Add SocketRole.Members property * Change Members to IEnumerable. --- src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs b/src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs index 57d913317..7d24d8e1c 100644 --- a/src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs +++ b/src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs @@ -1,6 +1,8 @@ using Discord.Rest; using System; +using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using System.Threading.Tasks; using Model = Discord.API.Role; @@ -22,6 +24,8 @@ namespace Discord.WebSocket public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); public bool IsEveryone => Id == Guild.Id; public string Mention => MentionUtils.MentionRole(Id); + public IEnumerable Members + => Guild.Users.Where(x => x.Roles.Any(r => r.Id == Id)); internal SocketRole(SocketGuild guild, ulong id) : base(guild.Discord, id) From ea685b4f2352b31abcbf769826499aea0e8fb793 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Fri, 23 Jun 2017 14:33:41 -0400 Subject: [PATCH 215/243] Add 'article' EmbedType --- src/Discord.Net.Core/Entities/Messages/EmbedType.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedType.cs b/src/Discord.Net.Core/Entities/Messages/EmbedType.cs index e071e7dc8..ed39317a9 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedType.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedType.cs @@ -6,6 +6,7 @@ Link, Video, Image, - Gifv + Gifv, + Article } } From 36ed2b49f06f5f2736bf6b0937f4d002e2bb52ef Mon Sep 17 00:00:00 2001 From: Joe4evr Date: Fri, 23 Jun 2017 20:46:59 +0200 Subject: [PATCH 216/243] PreconditionGroup quick fix It didn't make much sense --- src/Discord.Net.Commands/Info/CommandInfo.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Discord.Net.Commands/Info/CommandInfo.cs b/src/Discord.Net.Commands/Info/CommandInfo.cs index ae350e592..f187460d5 100644 --- a/src/Discord.Net.Commands/Info/CommandInfo.cs +++ b/src/Discord.Net.Commands/Info/CommandInfo.cs @@ -68,7 +68,7 @@ namespace Discord.Commands { services = services ?? EmptyServiceProvider.Instance; - async Task CheckGroups(IEnumerable preconditions, string type) + async Task CheckGroups(IEnumerable preconditions, string type) { foreach (IGrouping preconditionGroup in preconditions.GroupBy(p => p.Group, StringComparer.Ordinal)) { @@ -78,7 +78,7 @@ namespace Discord.Commands { var result = await precondition.CheckPermissions(context, this, services).ConfigureAwait(false); if (!result.IsSuccess) - return PreconditionGroupResult.FromError($"{type} default precondition group failed.", new[] { result }); + return result; } } else @@ -243,4 +243,4 @@ namespace Discord.Commands return $"\"{Name}\" for {context.User} in {context.Channel}"; } } -} \ No newline at end of file +} From 444868b22d3d7787cad0fbeb01532b7dd1edd7ee Mon Sep 17 00:00:00 2001 From: Joe4evr Date: Sat, 24 Jun 2017 02:39:02 +0200 Subject: [PATCH 217/243] Fix attempting to inject into static properties --- src/Discord.Net.Commands/Utilities/ReflectionUtils.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs b/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs index 4cca0e864..ca3e01ebd 100644 --- a/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs +++ b/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs @@ -58,7 +58,7 @@ namespace Discord.Commands { foreach (var prop in ownerType.DeclaredProperties) { - if (prop.SetMethod?.IsPublic == true && prop.GetCustomAttribute() == null) + if (prop.GetMethod.IsStatic && prop.SetMethod?.IsPublic == true && prop.GetCustomAttribute() == null) result.Add(prop); } ownerType = ownerType.BaseType.GetTypeInfo(); From 34917a35de48aea438aa4dda1a5789591753843a Mon Sep 17 00:00:00 2001 From: Joe4evr Date: Sat, 24 Jun 2017 02:50:30 +0200 Subject: [PATCH 218/243] In my defense, it was 2:40 AM --- src/Discord.Net.Commands/Utilities/ReflectionUtils.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs b/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs index ca3e01ebd..d9956cdc0 100644 --- a/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs +++ b/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs @@ -58,7 +58,7 @@ namespace Discord.Commands { foreach (var prop in ownerType.DeclaredProperties) { - if (prop.GetMethod.IsStatic && prop.SetMethod?.IsPublic == true && prop.GetCustomAttribute() == null) + if (!prop.GetMethod.IsStatic && prop.SetMethod?.IsPublic == true && prop.GetCustomAttribute() == null) result.Add(prop); } ownerType = ownerType.BaseType.GetTypeInfo(); From cc390f03de4e583d9057091bc5e36e23b1eb472c Mon Sep 17 00:00:00 2001 From: Joe4evr Date: Sat, 24 Jun 2017 02:56:57 +0200 Subject: [PATCH 219/243] Fix the off-chance that someone has a property without a getter --- src/Discord.Net.Commands/Utilities/ReflectionUtils.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs b/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs index d9956cdc0..b6ceda426 100644 --- a/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs +++ b/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs @@ -58,7 +58,7 @@ namespace Discord.Commands { foreach (var prop in ownerType.DeclaredProperties) { - if (!prop.GetMethod.IsStatic && prop.SetMethod?.IsPublic == true && prop.GetCustomAttribute() == null) + if (prop.GetMethod?.IsStatic == false && prop.SetMethod?.IsPublic == true && prop.GetCustomAttribute() == null) result.Add(prop); } ownerType = ownerType.BaseType.GetTypeInfo(); From 107f1b580380c8c5906fd61a3bdd86bc30c6ebc0 Mon Sep 17 00:00:00 2001 From: FiniteReality Date: Sat, 24 Jun 2017 22:09:46 +0100 Subject: [PATCH 220/243] Add 'tweet' embed type --- src/Discord.Net.Core/Entities/Messages/EmbedType.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedType.cs b/src/Discord.Net.Core/Entities/Messages/EmbedType.cs index ed39317a9..469e968a5 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedType.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedType.cs @@ -7,6 +7,7 @@ Video, Image, Gifv, - Article + Article, + Tweet } } From 1d612f15c859b0a5d9e0880e6817f99417ff21ac Mon Sep 17 00:00:00 2001 From: Christopher F Date: Tue, 27 Jun 2017 08:49:46 -0400 Subject: [PATCH 221/243] ToString on types of IEmote should return a chat formatted string --- src/Discord.Net.Core/Entities/Emotes/Emoji.cs | 2 ++ src/Discord.Net.Core/Entities/Emotes/Emote.cs | 2 +- src/Discord.Net.Core/Entities/Emotes/GuildEmote.cs | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Emotes/Emoji.cs b/src/Discord.Net.Core/Entities/Emotes/Emoji.cs index 5c1969613..07cee10a9 100644 --- a/src/Discord.Net.Core/Entities/Emotes/Emoji.cs +++ b/src/Discord.Net.Core/Entities/Emotes/Emoji.cs @@ -19,5 +19,7 @@ /// The unicode representation of this emote. /// public string Name { get; } + + public override string ToString() => Name; } } diff --git a/src/Discord.Net.Core/Entities/Emotes/Emote.cs b/src/Discord.Net.Core/Entities/Emotes/Emote.cs index b1ca272eb..76c20a77d 100644 --- a/src/Discord.Net.Core/Entities/Emotes/Emote.cs +++ b/src/Discord.Net.Core/Entities/Emotes/Emote.cs @@ -58,6 +58,6 @@ namespace Discord } private string DebuggerDisplay => $"{Name} ({Id})"; - public override string ToString() => Name; + public override string ToString() => $"<:{Name}:{Id}>"; } } diff --git a/src/Discord.Net.Core/Entities/Emotes/GuildEmote.cs b/src/Discord.Net.Core/Entities/Emotes/GuildEmote.cs index e883c707e..8d776a4cd 100644 --- a/src/Discord.Net.Core/Entities/Emotes/GuildEmote.cs +++ b/src/Discord.Net.Core/Entities/Emotes/GuildEmote.cs @@ -20,7 +20,7 @@ namespace Discord RoleIds = roleIds; } - public override string ToString() => Name; private string DebuggerDisplay => $"{Name} ({Id})"; + public override string ToString() => $"<:{Name}:{Id}>"; } } From 1ce1c019b399b83fb7ed946ec7908d5d37b567fb Mon Sep 17 00:00:00 2001 From: Christopher F Date: Thu, 29 Jun 2017 16:01:59 -0400 Subject: [PATCH 222/243] Add support for audit log reasons (#708) * Add support for audit log reasons * Made changes per discussion --- src/Discord.Net.Core/Entities/Guilds/IGuild.cs | 4 ++-- src/Discord.Net.Core/Entities/Users/IGuildUser.cs | 2 +- src/Discord.Net.Core/Net/Rest/IRestClient.cs | 6 +++--- src/Discord.Net.Core/RequestOptions.cs | 4 ++++ src/Discord.Net.Rest/API/Rest/CreateGuildBanParams.cs | 1 + src/Discord.Net.Rest/DiscordRestApiClient.cs | 8 +++++--- src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs | 4 ++-- src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs | 8 ++++---- src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs | 4 ++-- .../Entities/Users/RestWebhookUser.cs | 2 +- src/Discord.Net.Rest/Entities/Users/UserHelper.cs | 4 ++-- src/Discord.Net.Rest/Net/DefaultRestClient.cs | 11 ++++++++--- .../Net/Queue/Requests/JsonRestRequest.cs | 2 +- .../Net/Queue/Requests/MultipartRestRequest.cs | 2 +- .../Net/Queue/Requests/RestRequest.cs | 2 +- .../Entities/Guilds/SocketGuild.cs | 8 ++++---- .../Entities/Users/SocketGuildUser.cs | 4 ++-- .../Entities/Users/SocketWebhookUser.cs | 2 +- test/Discord.Net.Tests/Net/CachedRestClient.cs | 6 +++--- 19 files changed, 48 insertions(+), 36 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs index 506cbd3e4..7874f5fd1 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs @@ -66,10 +66,10 @@ namespace Discord Task> GetBansAsync(RequestOptions options = null); /// Bans the provided user from this guild and optionally prunes their recent messages. /// The number of days to remove messages from this user for - must be between [0, 7] - Task AddBanAsync(IUser user, int pruneDays = 0, RequestOptions options = null); + Task AddBanAsync(IUser user, int pruneDays = 0, string reason = null, RequestOptions options = null); /// Bans the provided user id from this guild and optionally prunes their recent messages. /// The number of days to remove messages from this user for - must be between [0, 7] - Task AddBanAsync(ulong userId, int pruneDays = 0, RequestOptions options = null); + Task AddBanAsync(ulong userId, int pruneDays = 0, string reason = null, RequestOptions options = null); /// Unbans the provided user if it is currently banned. Task RemoveBanAsync(IUser user, RequestOptions options = null); /// Unbans the provided user id if it is currently banned. diff --git a/src/Discord.Net.Core/Entities/Users/IGuildUser.cs b/src/Discord.Net.Core/Entities/Users/IGuildUser.cs index cd9516395..57cad1333 100644 --- a/src/Discord.Net.Core/Entities/Users/IGuildUser.cs +++ b/src/Discord.Net.Core/Entities/Users/IGuildUser.cs @@ -25,7 +25,7 @@ namespace Discord ChannelPermissions GetPermissions(IGuildChannel channel); /// Kicks this user from this guild. - Task KickAsync(RequestOptions options = null); + Task KickAsync(string reason = null, RequestOptions options = null); /// Modifies this user's properties in this guild. Task ModifyAsync(Action func, RequestOptions options = null); diff --git a/src/Discord.Net.Core/Net/Rest/IRestClient.cs b/src/Discord.Net.Core/Net/Rest/IRestClient.cs index b5f136cb0..addfa9061 100644 --- a/src/Discord.Net.Core/Net/Rest/IRestClient.cs +++ b/src/Discord.Net.Core/Net/Rest/IRestClient.cs @@ -9,8 +9,8 @@ namespace Discord.Net.Rest void SetHeader(string key, string value); void SetCancelToken(CancellationToken cancelToken); - Task SendAsync(string method, string endpoint, CancellationToken cancelToken, bool headerOnly = false); - Task SendAsync(string method, string endpoint, string json, CancellationToken cancelToken, bool headerOnly = false); - Task SendAsync(string method, string endpoint, IReadOnlyDictionary multipartParams, CancellationToken cancelToken, bool headerOnly = false); + Task SendAsync(string method, string endpoint, CancellationToken cancelToken, bool headerOnly = false, string reason = null); + Task SendAsync(string method, string endpoint, string json, CancellationToken cancelToken, bool headerOnly = false, string reason = null); + Task SendAsync(string method, string endpoint, IReadOnlyDictionary multipartParams, CancellationToken cancelToken, bool headerOnly = false, string reason = null); } } diff --git a/src/Discord.Net.Core/RequestOptions.cs b/src/Discord.Net.Core/RequestOptions.cs index 4f5910c53..5f3a8814b 100644 --- a/src/Discord.Net.Core/RequestOptions.cs +++ b/src/Discord.Net.Core/RequestOptions.cs @@ -14,6 +14,10 @@ namespace Discord public CancellationToken CancelToken { get; set; } = CancellationToken.None; public RetryMode? RetryMode { get; set; } public bool HeaderOnly { get; internal set; } + /// + /// The reason for this action in the guild's audit log + /// + public string AuditLogReason { get; set; } internal bool IgnoreState { get; set; } internal string BucketId { get; set; } diff --git a/src/Discord.Net.Rest/API/Rest/CreateGuildBanParams.cs b/src/Discord.Net.Rest/API/Rest/CreateGuildBanParams.cs index 0c148fe70..f0432e517 100644 --- a/src/Discord.Net.Rest/API/Rest/CreateGuildBanParams.cs +++ b/src/Discord.Net.Rest/API/Rest/CreateGuildBanParams.cs @@ -4,5 +4,6 @@ namespace Discord.API.Rest internal class CreateGuildBanParams { public Optional DeleteMessageDays { get; set; } + public string Reason { get; set; } } } diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index a632e5d42..621c2d0e2 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -803,7 +803,8 @@ namespace Discord.API options = RequestOptions.CreateOrClone(options); var ids = new BucketIds(guildId: guildId); - await SendAsync("PUT", () => $"guilds/{guildId}/bans/{userId}?delete-message-days={args.DeleteMessageDays}", ids, options: options).ConfigureAwait(false); + string reason = string.IsNullOrWhiteSpace(args.Reason) ? "" : $"&reason={args.Reason}"; + await SendAsync("PUT", () => $"guilds/{guildId}/bans/{userId}?delete-message-days={args.DeleteMessageDays}{reason}", ids, options: options).ConfigureAwait(false); } public async Task RemoveGuildBanAsync(ulong guildId, ulong userId, RequestOptions options = null) { @@ -980,14 +981,15 @@ namespace Discord.API Expression> endpoint = () => $"guilds/{guildId}/members?limit={limit}&after={afterUserId}"; return await SendAsync>("GET", endpoint, ids, options: options).ConfigureAwait(false); } - public async Task RemoveGuildMemberAsync(ulong guildId, ulong userId, RequestOptions options = null) + public async Task RemoveGuildMemberAsync(ulong guildId, ulong userId, string reason, RequestOptions options = null) { Preconditions.NotEqual(guildId, 0, nameof(guildId)); Preconditions.NotEqual(userId, 0, nameof(userId)); options = RequestOptions.CreateOrClone(options); var ids = new BucketIds(guildId: guildId); - await SendAsync("DELETE", () => $"guilds/{guildId}/members/{userId}", ids, options: options).ConfigureAwait(false); + reason = string.IsNullOrWhiteSpace(reason) ? "" : $"?reason={reason}"; + await SendAsync("DELETE", () => $"guilds/{guildId}/members/{userId}{reason}", ids, options: options).ConfigureAwait(false); } public async Task ModifyGuildMemberAsync(ulong guildId, ulong userId, Rest.ModifyGuildMemberParams args, RequestOptions options = null) { diff --git a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs index 98303cea6..5cfb1e566 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs @@ -107,9 +107,9 @@ namespace Discord.Rest } public static async Task AddBanAsync(IGuild guild, BaseDiscordClient client, - ulong userId, int pruneDays, RequestOptions options) + ulong userId, int pruneDays, string reason, RequestOptions options) { - var args = new CreateGuildBanParams { DeleteMessageDays = pruneDays }; + var args = new CreateGuildBanParams { DeleteMessageDays = pruneDays, Reason = reason }; await client.ApiClient.CreateGuildBanAsync(guild.Id, userId, args, options).ConfigureAwait(false); } public static async Task RemoveBanAsync(IGuild guild, BaseDiscordClient client, diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs index 8b5598ffe..11971a5c1 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -137,10 +137,10 @@ namespace Discord.Rest public Task> GetBansAsync(RequestOptions options = null) => GuildHelper.GetBansAsync(this, Discord, options); - public Task AddBanAsync(IUser user, int pruneDays = 0, RequestOptions options = null) - => GuildHelper.AddBanAsync(this, Discord, user.Id, pruneDays, options); - public Task AddBanAsync(ulong userId, int pruneDays = 0, RequestOptions options = null) - => GuildHelper.AddBanAsync(this, Discord, userId, pruneDays, options); + public Task AddBanAsync(IUser user, int pruneDays = 0, string reason = null, RequestOptions options = null) + => GuildHelper.AddBanAsync(this, Discord, user.Id, pruneDays, reason, options); + public Task AddBanAsync(ulong userId, int pruneDays = 0, string reason = null, RequestOptions options = null) + => GuildHelper.AddBanAsync(this, Discord, userId, pruneDays, reason, options); public Task RemoveBanAsync(IUser user, RequestOptions options = null) => GuildHelper.RemoveBanAsync(this, Discord, user.Id, options); diff --git a/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs b/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs index f6db057f2..2fce5f619 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs @@ -85,8 +85,8 @@ namespace Discord.Rest else if (args.RoleIds.IsSpecified) UpdateRoles(args.RoleIds.Value.ToArray()); } - public Task KickAsync(RequestOptions options = null) - => UserHelper.KickAsync(this, Discord, options); + public Task KickAsync(string reason = null, RequestOptions options = null) + => UserHelper.KickAsync(this, Discord, reason, options); /// public Task AddRoleAsync(IRole role, RequestOptions options = null) => AddRolesAsync(new[] { role }, options); diff --git a/src/Discord.Net.Rest/Entities/Users/RestWebhookUser.cs b/src/Discord.Net.Rest/Entities/Users/RestWebhookUser.cs index ae794becc..bb44f2777 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestWebhookUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestWebhookUser.cs @@ -45,7 +45,7 @@ namespace Discord.Rest GuildPermissions IGuildUser.GuildPermissions => GuildPermissions.Webhook; ChannelPermissions IGuildUser.GetPermissions(IGuildChannel channel) => Permissions.ToChannelPerms(channel, GuildPermissions.Webhook.RawValue); - Task IGuildUser.KickAsync(RequestOptions options) + Task IGuildUser.KickAsync(string reason, RequestOptions options) { throw new NotSupportedException("Webhook users cannot be kicked."); } diff --git a/src/Discord.Net.Rest/Entities/Users/UserHelper.cs b/src/Discord.Net.Rest/Entities/Users/UserHelper.cs index 82e59227d..562cfaae8 100644 --- a/src/Discord.Net.Rest/Entities/Users/UserHelper.cs +++ b/src/Discord.Net.Rest/Entities/Users/UserHelper.cs @@ -53,9 +53,9 @@ namespace Discord.Rest } public static async Task KickAsync(IGuildUser user, BaseDiscordClient client, - RequestOptions options) + string reason, RequestOptions options) { - await client.ApiClient.RemoveGuildMemberAsync(user.GuildId, user.Id, options).ConfigureAwait(false); + await client.ApiClient.RemoveGuildMemberAsync(user.GuildId, user.Id, reason, options).ConfigureAwait(false); } public static async Task CreateDMChannelAsync(IUser user, BaseDiscordClient client, diff --git a/src/Discord.Net.Rest/Net/DefaultRestClient.cs b/src/Discord.Net.Rest/Net/DefaultRestClient.cs index 20fbe2278..a54107829 100644 --- a/src/Discord.Net.Rest/Net/DefaultRestClient.cs +++ b/src/Discord.Net.Rest/Net/DefaultRestClient.cs @@ -62,26 +62,31 @@ namespace Discord.Net.Rest _cancelToken = cancelToken; } - public async Task SendAsync(string method, string endpoint, CancellationToken cancelToken, bool headerOnly) + public async Task SendAsync(string method, string endpoint, CancellationToken cancelToken, bool headerOnly, string reason = null) { string uri = Path.Combine(_baseUrl, endpoint); using (var restRequest = new HttpRequestMessage(GetMethod(method), uri)) + { + if (reason != null) restRequest.Headers.Add("X-Audit-Log-Reason", Uri.EscapeDataString(reason)); return await SendInternalAsync(restRequest, cancelToken, headerOnly).ConfigureAwait(false); + } } - public async Task SendAsync(string method, string endpoint, string json, CancellationToken cancelToken, bool headerOnly) + public async Task SendAsync(string method, string endpoint, string json, CancellationToken cancelToken, bool headerOnly, string reason = null) { string uri = Path.Combine(_baseUrl, endpoint); using (var restRequest = new HttpRequestMessage(GetMethod(method), uri)) { + if (reason != null) restRequest.Headers.Add("X-Audit-Log-Reason", Uri.EscapeDataString(reason)); restRequest.Content = new StringContent(json, Encoding.UTF8, "application/json"); return await SendInternalAsync(restRequest, cancelToken, headerOnly).ConfigureAwait(false); } } - public async Task SendAsync(string method, string endpoint, IReadOnlyDictionary multipartParams, CancellationToken cancelToken, bool headerOnly) + public async Task SendAsync(string method, string endpoint, IReadOnlyDictionary multipartParams, CancellationToken cancelToken, bool headerOnly, string reason = null) { string uri = Path.Combine(_baseUrl, endpoint); using (var restRequest = new HttpRequestMessage(GetMethod(method), uri)) { + if (reason != null) restRequest.Headers.Add("X-Audit-Log-Reason", Uri.EscapeDataString(reason)); var content = new MultipartFormDataContent("Upload----" + DateTime.Now.ToString(CultureInfo.InvariantCulture)); if (multipartParams != null) { diff --git a/src/Discord.Net.Rest/Net/Queue/Requests/JsonRestRequest.cs b/src/Discord.Net.Rest/Net/Queue/Requests/JsonRestRequest.cs index 83c5e0eb5..2949bab3c 100644 --- a/src/Discord.Net.Rest/Net/Queue/Requests/JsonRestRequest.cs +++ b/src/Discord.Net.Rest/Net/Queue/Requests/JsonRestRequest.cs @@ -15,7 +15,7 @@ namespace Discord.Net.Queue public override async Task SendAsync() { - return await Client.SendAsync(Method, Endpoint, Json, Options.CancelToken, Options.HeaderOnly).ConfigureAwait(false); + return await Client.SendAsync(Method, Endpoint, Json, Options.CancelToken, Options.HeaderOnly, Options.AuditLogReason).ConfigureAwait(false); } } } diff --git a/src/Discord.Net.Rest/Net/Queue/Requests/MultipartRestRequest.cs b/src/Discord.Net.Rest/Net/Queue/Requests/MultipartRestRequest.cs index 424a5325e..c8d97bbdf 100644 --- a/src/Discord.Net.Rest/Net/Queue/Requests/MultipartRestRequest.cs +++ b/src/Discord.Net.Rest/Net/Queue/Requests/MultipartRestRequest.cs @@ -16,7 +16,7 @@ namespace Discord.Net.Queue public override async Task SendAsync() { - return await Client.SendAsync(Method, Endpoint, MultipartParams, Options.CancelToken, Options.HeaderOnly).ConfigureAwait(false); + return await Client.SendAsync(Method, Endpoint, MultipartParams, Options.CancelToken, Options.HeaderOnly, Options.AuditLogReason).ConfigureAwait(false); } } } diff --git a/src/Discord.Net.Rest/Net/Queue/Requests/RestRequest.cs b/src/Discord.Net.Rest/Net/Queue/Requests/RestRequest.cs index 7f358e786..8f160273a 100644 --- a/src/Discord.Net.Rest/Net/Queue/Requests/RestRequest.cs +++ b/src/Discord.Net.Rest/Net/Queue/Requests/RestRequest.cs @@ -28,7 +28,7 @@ namespace Discord.Net.Queue public virtual async Task SendAsync() { - return await Client.SendAsync(Method, Endpoint, Options.CancelToken, Options.HeaderOnly).ConfigureAwait(false); + return await Client.SendAsync(Method, Endpoint, Options.CancelToken, Options.HeaderOnly, Options.AuditLogReason).ConfigureAwait(false); } } } diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index 5358605c8..aae18be36 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -281,10 +281,10 @@ namespace Discord.WebSocket public Task> GetBansAsync(RequestOptions options = null) => GuildHelper.GetBansAsync(this, Discord, options); - public Task AddBanAsync(IUser user, int pruneDays = 0, RequestOptions options = null) - => GuildHelper.AddBanAsync(this, Discord, user.Id, pruneDays, options); - public Task AddBanAsync(ulong userId, int pruneDays = 0, RequestOptions options = null) - => GuildHelper.AddBanAsync(this, Discord, userId, pruneDays, options); + public Task AddBanAsync(IUser user, int pruneDays = 0, string reason = null, RequestOptions options = null) + => GuildHelper.AddBanAsync(this, Discord, user.Id, pruneDays, reason, options); + public Task AddBanAsync(ulong userId, int pruneDays = 0, string reason = null, RequestOptions options = null) + => GuildHelper.AddBanAsync(this, Discord, userId, pruneDays, reason, options); public Task RemoveBanAsync(IUser user, RequestOptions options = null) => GuildHelper.RemoveBanAsync(this, Discord, user.Id, options); diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs index 05aa132a5..844b0c7f4 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs @@ -122,8 +122,8 @@ namespace Discord.WebSocket public Task ModifyAsync(Action func, RequestOptions options = null) => UserHelper.ModifyAsync(this, Discord, func, options); - public Task KickAsync(RequestOptions options = null) - => UserHelper.KickAsync(this, Discord, options); + public Task KickAsync(string reason = null, RequestOptions options = null) + => UserHelper.KickAsync(this, Discord, reason, options); /// public Task AddRoleAsync(IRole role, RequestOptions options = null) => AddRolesAsync(new[] { role }, options); diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs index c34f866cb..78a29639b 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs @@ -47,7 +47,7 @@ namespace Discord.WebSocket GuildPermissions IGuildUser.GuildPermissions => GuildPermissions.Webhook; ChannelPermissions IGuildUser.GetPermissions(IGuildChannel channel) => Permissions.ToChannelPerms(channel, GuildPermissions.Webhook.RawValue); - Task IGuildUser.KickAsync(RequestOptions options) + Task IGuildUser.KickAsync(string reason, RequestOptions options) { throw new NotSupportedException("Webhook users cannot be kicked."); } diff --git a/test/Discord.Net.Tests/Net/CachedRestClient.cs b/test/Discord.Net.Tests/Net/CachedRestClient.cs index f4b3bb279..4bc8a386a 100644 --- a/test/Discord.Net.Tests/Net/CachedRestClient.cs +++ b/test/Discord.Net.Tests/Net/CachedRestClient.cs @@ -66,7 +66,7 @@ namespace Discord.Net _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _cancelTokenSource.Token).Token; } - public async Task SendAsync(string method, string endpoint, CancellationToken cancelToken, bool headerOnly) + public async Task SendAsync(string method, string endpoint, CancellationToken cancelToken, bool headerOnly, string reason = null) { if (method != "GET") throw new InvalidOperationException("This RestClient only supports GET requests."); @@ -75,11 +75,11 @@ namespace Discord.Net var bytes = await _blobCache.DownloadUrl(uri, _headers); return new RestResponse(HttpStatusCode.OK, _headers, new MemoryStream(bytes)); } - public Task SendAsync(string method, string endpoint, string json, CancellationToken cancelToken, bool headerOnly) + public Task SendAsync(string method, string endpoint, string json, CancellationToken cancelToken, bool headerOnly, string reason = null) { throw new InvalidOperationException("This RestClient does not support payloads."); } - public Task SendAsync(string method, string endpoint, IReadOnlyDictionary multipartParams, CancellationToken cancelToken, bool headerOnly) + public Task SendAsync(string method, string endpoint, IReadOnlyDictionary multipartParams, CancellationToken cancelToken, bool headerOnly, string reason = null) { throw new InvalidOperationException("This RestClient does not support multipart requests."); } From 7837c4862cab32ecc432b3c6794277d92d89647d Mon Sep 17 00:00:00 2001 From: Christopher F Date: Thu, 29 Jun 2017 16:38:05 -0400 Subject: [PATCH 223/243] Revert change of all Url types on IEmbed to string (#724) --- .../Entities/Messages/Embed.cs | 4 +- .../Entities/Messages/EmbedAuthor.cs | 8 +-- .../Entities/Messages/EmbedFooter.cs | 6 +- .../Entities/Messages/EmbedImage.cs | 6 +- .../Entities/Messages/EmbedProvider.cs | 4 +- .../Entities/Messages/EmbedThumbnail.cs | 6 +- .../Entities/Messages/EmbedVideo.cs | 4 +- .../Entities/Messages/IEmbed.cs | 2 +- src/Discord.Net.Rest/API/Common/Embed.cs | 2 +- .../API/Common/EmbedAuthor.cs | 6 +- .../API/Common/EmbedFooter.cs | 4 +- src/Discord.Net.Rest/API/Common/EmbedImage.cs | 4 +- .../API/Common/EmbedProvider.cs | 2 +- .../API/Common/EmbedThumbnail.cs | 4 +- src/Discord.Net.Rest/API/Common/EmbedVideo.cs | 2 +- .../Entities/Messages/EmbedBuilder.cs | 72 +++++++++++++++---- 16 files changed, 92 insertions(+), 44 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Messages/Embed.cs b/src/Discord.Net.Core/Entities/Messages/Embed.cs index 9b72b9194..5fae7acde 100644 --- a/src/Discord.Net.Core/Entities/Messages/Embed.cs +++ b/src/Discord.Net.Core/Entities/Messages/Embed.cs @@ -11,7 +11,7 @@ namespace Discord public EmbedType Type { get; } public string Description { get; internal set; } - public Uri Url { get; internal set; } + public string Url { get; internal set; } public string Title { get; internal set; } public DateTimeOffset? Timestamp { get; internal set; } public Color? Color { get; internal set; } @@ -31,7 +31,7 @@ namespace Discord internal Embed(EmbedType type, string title, string description, - Uri url, + string url, DateTimeOffset? timestamp, Color? color, EmbedImage? image, diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedAuthor.cs b/src/Discord.Net.Core/Entities/Messages/EmbedAuthor.cs index d1f2b9618..c59473704 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedAuthor.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedAuthor.cs @@ -7,11 +7,11 @@ namespace Discord public struct EmbedAuthor { public string Name { get; internal set; } - public Uri Url { get; internal set; } - public Uri IconUrl { get; internal set; } - public Uri ProxyIconUrl { get; internal set; } + public string Url { get; internal set; } + public string IconUrl { get; internal set; } + public string ProxyIconUrl { get; internal set; } - internal EmbedAuthor(string name, Uri url, Uri iconUrl, Uri proxyIconUrl) + internal EmbedAuthor(string name, string url, string iconUrl, string proxyIconUrl) { Name = name; Url = url; diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedFooter.cs b/src/Discord.Net.Core/Entities/Messages/EmbedFooter.cs index 3c9bf35a9..29d85cd90 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedFooter.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedFooter.cs @@ -7,10 +7,10 @@ namespace Discord public struct EmbedFooter { public string Text { get; internal set; } - public Uri IconUrl { get; internal set; } - public Uri ProxyUrl { get; internal set; } + public string IconUrl { get; internal set; } + public string ProxyUrl { get; internal set; } - internal EmbedFooter(string text, Uri iconUrl, Uri proxyUrl) + internal EmbedFooter(string text, string iconUrl, string proxyUrl) { Text = text; IconUrl = iconUrl; diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedImage.cs b/src/Discord.Net.Core/Entities/Messages/EmbedImage.cs index fd87e3db3..f21d42c0c 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedImage.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedImage.cs @@ -6,12 +6,12 @@ namespace Discord [DebuggerDisplay("{DebuggerDisplay,nq}")] public struct EmbedImage { - public Uri Url { get; } - public Uri ProxyUrl { get; } + public string Url { get; } + public string ProxyUrl { get; } public int? Height { get; } public int? Width { get; } - internal EmbedImage(Uri url, Uri proxyUrl, int? height, int? width) + internal EmbedImage(string url, string proxyUrl, int? height, int? width) { Url = url; ProxyUrl = proxyUrl; diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedProvider.cs b/src/Discord.Net.Core/Entities/Messages/EmbedProvider.cs index 0b816b32b..24722b158 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedProvider.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedProvider.cs @@ -7,9 +7,9 @@ namespace Discord public struct EmbedProvider { public string Name { get; } - public Uri Url { get; } + public string Url { get; } - internal EmbedProvider(string name, Uri url) + internal EmbedProvider(string name, string url) { Name = name; Url = url; diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedThumbnail.cs b/src/Discord.Net.Core/Entities/Messages/EmbedThumbnail.cs index b83401e07..209a93e37 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedThumbnail.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedThumbnail.cs @@ -6,12 +6,12 @@ namespace Discord [DebuggerDisplay("{DebuggerDisplay,nq}")] public struct EmbedThumbnail { - public Uri Url { get; } - public Uri ProxyUrl { get; } + public string Url { get; } + public string ProxyUrl { get; } public int? Height { get; } public int? Width { get; } - internal EmbedThumbnail(Uri url, Uri proxyUrl, int? height, int? width) + internal EmbedThumbnail(string url, string proxyUrl, int? height, int? width) { Url = url; ProxyUrl = proxyUrl; diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedVideo.cs b/src/Discord.Net.Core/Entities/Messages/EmbedVideo.cs index 9ea4b11d6..f00681d89 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedVideo.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedVideo.cs @@ -6,11 +6,11 @@ namespace Discord [DebuggerDisplay("{DebuggerDisplay,nq}")] public struct EmbedVideo { - public Uri Url { get; } + public string Url { get; } public int? Height { get; } public int? Width { get; } - internal EmbedVideo(Uri url, int? height, int? width) + internal EmbedVideo(string url, int? height, int? width) { Url = url; Height = height; diff --git a/src/Discord.Net.Core/Entities/Messages/IEmbed.cs b/src/Discord.Net.Core/Entities/Messages/IEmbed.cs index 01ea2b248..f390c4c28 100644 --- a/src/Discord.Net.Core/Entities/Messages/IEmbed.cs +++ b/src/Discord.Net.Core/Entities/Messages/IEmbed.cs @@ -5,7 +5,7 @@ namespace Discord { public interface IEmbed { - Uri Url { get; } + string Url { get; } string Title { get; } string Description { get; } EmbedType Type { get; } diff --git a/src/Discord.Net.Rest/API/Common/Embed.cs b/src/Discord.Net.Rest/API/Common/Embed.cs index ebce9757b..1c9fa34e2 100644 --- a/src/Discord.Net.Rest/API/Common/Embed.cs +++ b/src/Discord.Net.Rest/API/Common/Embed.cs @@ -12,7 +12,7 @@ namespace Discord.API [JsonProperty("description")] public string Description { get; set; } [JsonProperty("url")] - public Uri Url { get; set; } + public string Url { get; set; } [JsonProperty("color")] public uint? Color { get; set; } [JsonProperty("type"), JsonConverter(typeof(StringEnumConverter))] diff --git a/src/Discord.Net.Rest/API/Common/EmbedAuthor.cs b/src/Discord.Net.Rest/API/Common/EmbedAuthor.cs index 9ade58edf..4381a9da3 100644 --- a/src/Discord.Net.Rest/API/Common/EmbedAuthor.cs +++ b/src/Discord.Net.Rest/API/Common/EmbedAuthor.cs @@ -8,10 +8,10 @@ namespace Discord.API [JsonProperty("name")] public string Name { get; set; } [JsonProperty("url")] - public Uri Url { get; set; } + public string Url { get; set; } [JsonProperty("icon_url")] - public Uri IconUrl { get; set; } + public string IconUrl { get; set; } [JsonProperty("proxy_icon_url")] - public Uri ProxyIconUrl { get; set; } + public string ProxyIconUrl { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Common/EmbedFooter.cs b/src/Discord.Net.Rest/API/Common/EmbedFooter.cs index 1e079d03e..3dd7020d9 100644 --- a/src/Discord.Net.Rest/API/Common/EmbedFooter.cs +++ b/src/Discord.Net.Rest/API/Common/EmbedFooter.cs @@ -8,8 +8,8 @@ namespace Discord.API [JsonProperty("text")] public string Text { get; set; } [JsonProperty("icon_url")] - public Uri IconUrl { get; set; } + public string IconUrl { get; set; } [JsonProperty("proxy_icon_url")] - public Uri ProxyIconUrl { get; set; } + public string ProxyIconUrl { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Common/EmbedImage.cs b/src/Discord.Net.Rest/API/Common/EmbedImage.cs index a12299783..c6b3562a3 100644 --- a/src/Discord.Net.Rest/API/Common/EmbedImage.cs +++ b/src/Discord.Net.Rest/API/Common/EmbedImage.cs @@ -7,9 +7,9 @@ namespace Discord.API internal class EmbedImage { [JsonProperty("url")] - public Uri Url { get; set; } + public string Url { get; set; } [JsonProperty("proxy_url")] - public Uri ProxyUrl { get; set; } + public string ProxyUrl { get; set; } [JsonProperty("height")] public Optional Height { get; set; } [JsonProperty("width")] diff --git a/src/Discord.Net.Rest/API/Common/EmbedProvider.cs b/src/Discord.Net.Rest/API/Common/EmbedProvider.cs index 7ca87185c..1658eda1a 100644 --- a/src/Discord.Net.Rest/API/Common/EmbedProvider.cs +++ b/src/Discord.Net.Rest/API/Common/EmbedProvider.cs @@ -9,6 +9,6 @@ namespace Discord.API [JsonProperty("name")] public string Name { get; set; } [JsonProperty("url")] - public Uri Url { get; set; } + public string Url { get; set; } } } diff --git a/src/Discord.Net.Rest/API/Common/EmbedThumbnail.cs b/src/Discord.Net.Rest/API/Common/EmbedThumbnail.cs index b4ccd4b21..993beb72b 100644 --- a/src/Discord.Net.Rest/API/Common/EmbedThumbnail.cs +++ b/src/Discord.Net.Rest/API/Common/EmbedThumbnail.cs @@ -7,9 +7,9 @@ namespace Discord.API internal class EmbedThumbnail { [JsonProperty("url")] - public Uri Url { get; set; } + public string Url { get; set; } [JsonProperty("proxy_url")] - public Uri ProxyUrl { get; set; } + public string ProxyUrl { get; set; } [JsonProperty("height")] public Optional Height { get; set; } [JsonProperty("width")] diff --git a/src/Discord.Net.Rest/API/Common/EmbedVideo.cs b/src/Discord.Net.Rest/API/Common/EmbedVideo.cs index 2512151ed..610cf58a8 100644 --- a/src/Discord.Net.Rest/API/Common/EmbedVideo.cs +++ b/src/Discord.Net.Rest/API/Common/EmbedVideo.cs @@ -7,7 +7,7 @@ namespace Discord.API internal class EmbedVideo { [JsonProperty("url")] - public Uri Url { get; set; } + public string Url { get; set; } [JsonProperty("height")] public Optional Height { get; set; } [JsonProperty("width")] diff --git a/src/Discord.Net.Rest/Entities/Messages/EmbedBuilder.cs b/src/Discord.Net.Rest/Entities/Messages/EmbedBuilder.cs index be5ef7f32..c299bd1a1 100644 --- a/src/Discord.Net.Rest/Entities/Messages/EmbedBuilder.cs +++ b/src/Discord.Net.Rest/Entities/Messages/EmbedBuilder.cs @@ -39,9 +39,33 @@ namespace Discord } } - public Uri Url { get => _embed.Url; set { _embed.Url = value; } } - public Uri ThumbnailUrl { get => _embed.Thumbnail?.Url; set { _embed.Thumbnail = new EmbedThumbnail(value, null, null, null); } } - public Uri ImageUrl { get => _embed.Image?.Url; set { _embed.Image = new EmbedImage(value, null, null, null); } } + public string Url + { + get => _embed.Url; + set + { + if (!Uri.IsWellFormedUriString(value, UriKind.Absolute)) throw new ArgumentException("Url must be a well-formed URI", nameof(Url)); + _embed.Url = value; + } + } + public string ThumbnailUrl + { + get => _embed.Thumbnail?.Url; + set + { + if (!Uri.IsWellFormedUriString(value, UriKind.Absolute)) throw new ArgumentException("Url must be a well-formed URI", nameof(ThumbnailUrl)); + _embed.Thumbnail = new EmbedThumbnail(value, null, null, null); + } + } + public string ImageUrl + { + get => _embed.Image?.Url; + set + { + if (!Uri.IsWellFormedUriString(value, UriKind.Absolute)) throw new ArgumentException("Url must be a well-formed URI", nameof(ImageUrl)); + _embed.Image = new EmbedImage(value, null, null, null); + } + } public DateTimeOffset? Timestamp { get => _embed.Timestamp; set { _embed.Timestamp = value; } } public Color? Color { get => _embed.Color; set { _embed.Color = value; } } @@ -70,17 +94,17 @@ namespace Discord Description = description; return this; } - public EmbedBuilder WithUrl(Uri url) + public EmbedBuilder WithUrl(string url) { Url = url; return this; } - public EmbedBuilder WithThumbnailUrl(Uri thumbnailUrl) + public EmbedBuilder WithThumbnailUrl(string thumbnailUrl) { ThumbnailUrl = thumbnailUrl; return this; } - public EmbedBuilder WithImageUrl(Uri imageUrl) + public EmbedBuilder WithImageUrl(string imageUrl) { ImageUrl = imageUrl; return this; @@ -252,8 +276,24 @@ namespace Discord _author.Name = value; } } - public Uri Url { get => _author.Url; set { _author.Url = value; } } - public Uri IconUrl { get => _author.IconUrl; set { _author.IconUrl = value; } } + public string Url + { + get => _author.Url; + set + { + if (!Uri.IsWellFormedUriString(value, UriKind.Absolute)) throw new ArgumentException("Url must be a well-formed URI", nameof(Url)); + _author.Url = value; + } + } + public string IconUrl + { + get => _author.IconUrl; + set + { + if (!Uri.IsWellFormedUriString(value, UriKind.Absolute)) throw new ArgumentException("Url must be a well-formed URI", nameof(IconUrl)); + _author.IconUrl = value; + } + } public EmbedAuthorBuilder() { @@ -265,12 +305,12 @@ namespace Discord Name = name; return this; } - public EmbedAuthorBuilder WithUrl(Uri url) + public EmbedAuthorBuilder WithUrl(string url) { Url = url; return this; } - public EmbedAuthorBuilder WithIconUrl(Uri iconUrl) + public EmbedAuthorBuilder WithIconUrl(string iconUrl) { IconUrl = iconUrl; return this; @@ -295,7 +335,15 @@ namespace Discord _footer.Text = value; } } - public Uri IconUrl { get => _footer.IconUrl; set { _footer.IconUrl = value; } } + public string IconUrl + { + get => _footer.IconUrl; + set + { + if (!Uri.IsWellFormedUriString(value, UriKind.Absolute)) throw new ArgumentException("Url must be a well-formed URI", nameof(IconUrl)); + _footer.IconUrl = value; + } + } public EmbedFooterBuilder() { @@ -307,7 +355,7 @@ namespace Discord Text = text; return this; } - public EmbedFooterBuilder WithIconUrl(Uri iconUrl) + public EmbedFooterBuilder WithIconUrl(string iconUrl) { IconUrl = iconUrl; return this; From 41222eafeb57822d42015060a807d482187cc047 Mon Sep 17 00:00:00 2001 From: Alex Gravely Date: Thu, 29 Jun 2017 16:40:40 -0400 Subject: [PATCH 224/243] Add color presets. (#725) * Add DiscordColors struct * Moved presets to Discord.Color --- src/Discord.Net.Core/Entities/Roles/Color.cs | 40 ++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/Discord.Net.Core/Entities/Roles/Color.cs b/src/Discord.Net.Core/Entities/Roles/Color.cs index 3250acb2d..89e76df6d 100644 --- a/src/Discord.Net.Core/Entities/Roles/Color.cs +++ b/src/Discord.Net.Core/Entities/Roles/Color.cs @@ -8,6 +8,46 @@ namespace Discord { /// Gets the default user color value. public static readonly Color Default = new Color(0); + /// Gets the teal color value + public static readonly Color Teal = new Color(0x1ABC9C); + /// Gets the dark teal color value + public static readonly Color DarkTeal = new Color(0x11806A); + /// Gets the green color value + public static readonly Color Green = new Color(0x2ECC71); + /// Gets the dark green color value + public static readonly Color DarkGreen = new Color(0x1F8B4C); + /// Gets the blue color value + public static readonly Color Blue = new Color(0x3498DB); + /// Gets the dark blue color value + public static readonly Color DarkBlue = new Color(0x206694); + /// Gets the purple color value + public static readonly Color Purple = new Color(0x9B59B6); + /// Gets the dark purple color value + public static readonly Color DarkPurple = new Color(0x71368A); + /// Gets the magenta color value + public static readonly Color Magenta = new Color(0xE91E63); + /// Gets the dark magenta color value + public static readonly Color DarkMagenta = new Color(0xAD1457); + /// Gets the gold color value + public static readonly Color Gold = new Color(0xF1C40F); + /// Gets the light orange color value + public static readonly Color LightOrange = new Color(0xC27C0E); + /// Gets the orange color value + public static readonly Color Orange = new Color(0xE67E22); + /// Gets the dark orange color value + public static readonly Color DarkOrange = new Color(0xA84300); + /// Gets the red color value + public static readonly Color Red = new Color(0xE74C3C); + /// Gets the dark red color value + public static readonly Color DarkRed = new Color(0x992D22); + /// Gets the light grey color value + public static readonly Color LightGrey = new Color(0x979C9F); + /// Gets the lighter grey color value + public static readonly Color LighterGrey = new Color(0x95A5A6); + /// Gets the dark grey color value + public static readonly Color DarkGrey = new Color(0x607D8B); + /// Gets the darker grey color value + public static readonly Color DarkerGrey = new Color(0x546E7A); /// Gets the encoded value for this color. public uint RawValue { get; } From 032aba91291e1423af8f040733cc82e706ac6e7d Mon Sep 17 00:00:00 2001 From: Finite Reality Date: Thu, 29 Jun 2017 21:43:55 +0100 Subject: [PATCH 225/243] Update commands with C#7 features (#689) * C#7 features in commands, CommandInfo in ModuleBase * Update TypeReaders with C#7 features and IServiceProvider * Add best-choice command selection to CommandService * Normalize type reader scores correctly * Fix logic error and rebase onto dev * Change GetMethod for SetMethod in ReflectionUtils Should be checking against setters, not getters * Ensure args/params scores do not overwhelm Priority * Remove possibility of NaNs --- .../Builders/CommandBuilder.cs | 4 +- .../Builders/ModuleBuilder.cs | 2 +- .../Builders/ModuleClassBuilder.cs | 120 ++++++++++------- src/Discord.Net.Commands/CommandMatch.cs | 4 +- src/Discord.Net.Commands/CommandParser.cs | 9 +- src/Discord.Net.Commands/CommandService.cs | 123 ++++++++++++------ src/Discord.Net.Commands/IModuleBase.cs | 4 +- src/Discord.Net.Commands/Info/CommandInfo.cs | 10 +- .../Info/ParameterInfo.cs | 5 +- src/Discord.Net.Commands/ModuleBase.cs | 12 +- src/Discord.Net.Commands/PrimitiveParsers.cs | 5 - .../Readers/ChannelTypeReader.cs | 2 +- .../Readers/EnumTypeReader.cs | 5 +- .../Readers/MessageTypeReader.cs | 8 +- .../Readers/PrimitiveTypeReader.cs | 18 ++- .../Readers/RoleTypeReader.cs | 2 +- .../Readers/TypeReader.cs | 5 +- .../Readers/UserTypeReader.cs | 5 +- .../Utilities/ReflectionUtils.cs | 2 +- 19 files changed, 206 insertions(+), 139 deletions(-) diff --git a/src/Discord.Net.Commands/Builders/CommandBuilder.cs b/src/Discord.Net.Commands/Builders/CommandBuilder.cs index 8c2207f10..30db1fa62 100644 --- a/src/Discord.Net.Commands/Builders/CommandBuilder.cs +++ b/src/Discord.Net.Commands/Builders/CommandBuilder.cs @@ -13,7 +13,7 @@ namespace Discord.Commands.Builders private readonly List _aliases; public ModuleBuilder Module { get; } - internal Func Callback { get; set; } + internal Func Callback { get; set; } public string Name { get; set; } public string Summary { get; set; } @@ -36,7 +36,7 @@ namespace Discord.Commands.Builders _aliases = new List(); } //User-defined - internal CommandBuilder(ModuleBuilder module, string primaryAlias, Func callback) + internal CommandBuilder(ModuleBuilder module, string primaryAlias, Func callback) : this(module) { Discord.Preconditions.NotNull(primaryAlias, nameof(primaryAlias)); diff --git a/src/Discord.Net.Commands/Builders/ModuleBuilder.cs b/src/Discord.Net.Commands/Builders/ModuleBuilder.cs index d79239057..525907b8b 100644 --- a/src/Discord.Net.Commands/Builders/ModuleBuilder.cs +++ b/src/Discord.Net.Commands/Builders/ModuleBuilder.cs @@ -74,7 +74,7 @@ namespace Discord.Commands.Builders _preconditions.Add(precondition); return this; } - public ModuleBuilder AddCommand(string primaryAlias, Func callback, Action createFunc) + public ModuleBuilder AddCommand(string primaryAlias, Func callback, Action createFunc) { var builder = new CommandBuilder(this, primaryAlias, callback); createFunc(builder); diff --git a/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs index fe35e3b2a..401396900 100644 --- a/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs +++ b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs @@ -12,25 +12,42 @@ namespace Discord.Commands { private static readonly TypeInfo _moduleTypeInfo = typeof(IModuleBase).GetTypeInfo(); - public static IEnumerable Search(Assembly assembly) + public static async Task> SearchAsync(Assembly assembly, CommandService service) { - foreach (var type in assembly.ExportedTypes) + bool IsLoadableModule(TypeInfo info) { - var typeInfo = type.GetTypeInfo(); - if (IsValidModuleDefinition(typeInfo) && - !typeInfo.IsDefined(typeof(DontAutoLoadAttribute))) + return info.DeclaredMethods.Any(x => x.GetCustomAttribute() != null) && + info.GetCustomAttribute() == null; + } + + List result = new List(); + + foreach (var typeInfo in assembly.DefinedTypes) + { + if (typeInfo.IsPublic) + { + if (IsValidModuleDefinition(typeInfo) && + !typeInfo.IsDefined(typeof(DontAutoLoadAttribute))) + { + result.Add(typeInfo); + } + } + else if (IsLoadableModule(typeInfo)) { - yield return typeInfo; + await service._cmdLogger.WarningAsync($"Class {typeInfo.FullName} is not public and cannot be loaded. To suppress this message, mark the class with {nameof(DontAutoLoadAttribute)}."); } } + + return result; } - public static Dictionary Build(CommandService service, params TypeInfo[] validTypes) => Build(validTypes, service); - public static Dictionary Build(IEnumerable validTypes, CommandService service) + + public static Task> BuildAsync(CommandService service, params TypeInfo[] validTypes) => BuildAsync(validTypes, service); + public static async Task> BuildAsync(IEnumerable validTypes, CommandService service) { /*if (!validTypes.Any()) throw new InvalidOperationException("Could not find any valid modules from the given selection");*/ - + var topLevelGroups = validTypes.Where(x => x.DeclaringType == null); var subGroups = validTypes.Intersect(topLevelGroups); @@ -48,10 +65,13 @@ namespace Discord.Commands BuildModule(module, typeInfo, service); BuildSubTypes(module, typeInfo.DeclaredNestedTypes, builtTypes, service); + builtTypes.Add(typeInfo); result[typeInfo.AsType()] = module.Build(service); } + await service._cmdLogger.DebugAsync($"Successfully built and loaded {builtTypes.Count} modules.").ConfigureAwait(false); + return result; } @@ -128,26 +148,32 @@ namespace Discord.Commands foreach (var attribute in attributes) { - // TODO: C#7 type switch - if (attribute is CommandAttribute) + switch (attribute) { - var cmdAttr = attribute as CommandAttribute; - builder.AddAliases(cmdAttr.Text); - builder.RunMode = cmdAttr.RunMode; - builder.Name = builder.Name ?? cmdAttr.Text; + case CommandAttribute command: + builder.AddAliases(command.Text); + builder.RunMode = command.RunMode; + builder.Name = builder.Name ?? command.Text; + break; + case NameAttribute name: + builder.Name = name.Text; + break; + case PriorityAttribute priority: + builder.Priority = priority.Priority; + break; + case SummaryAttribute summary: + builder.Summary = summary.Text; + break; + case RemarksAttribute remarks: + builder.Remarks = remarks.Text; + break; + case AliasAttribute alias: + builder.AddAliases(alias.Aliases); + break; + case PreconditionAttribute precondition: + builder.AddPrecondition(precondition); + break; } - else if (attribute is NameAttribute) - builder.Name = (attribute as NameAttribute).Text; - else if (attribute is PriorityAttribute) - builder.Priority = (attribute as PriorityAttribute).Priority; - else if (attribute is SummaryAttribute) - builder.Summary = (attribute as SummaryAttribute).Text; - else if (attribute is RemarksAttribute) - builder.Remarks = (attribute as RemarksAttribute).Text; - else if (attribute is AliasAttribute) - builder.AddAliases((attribute as AliasAttribute).Aliases); - else if (attribute is PreconditionAttribute) - builder.AddPrecondition(attribute as PreconditionAttribute); } if (builder.Name == null) @@ -165,19 +191,19 @@ namespace Discord.Commands var createInstance = ReflectionUtils.CreateBuilder(typeInfo, service); - builder.Callback = async (ctx, args, map) => + builder.Callback = async (ctx, args, map, cmd) => { var instance = createInstance(map); instance.SetContext(ctx); try { - instance.BeforeExecute(); + instance.BeforeExecute(cmd); var task = method.Invoke(instance, args) as Task ?? Task.Delay(0); await task.ConfigureAwait(false); } finally { - instance.AfterExecute(); + instance.AfterExecute(cmd); (instance as IDisposable)?.Dispose(); } }; @@ -195,24 +221,24 @@ namespace Discord.Commands foreach (var attribute in attributes) { - // TODO: C#7 type switch - if (attribute is SummaryAttribute) - builder.Summary = (attribute as SummaryAttribute).Text; - else if (attribute is OverrideTypeReaderAttribute) - builder.TypeReader = GetTypeReader(service, paramType, (attribute as OverrideTypeReaderAttribute).TypeReader); - else if (attribute is ParameterPreconditionAttribute) - builder.AddPrecondition(attribute as ParameterPreconditionAttribute); - else if (attribute is ParamArrayAttribute) - { - builder.IsMultiple = true; - paramType = paramType.GetElementType(); - } - else if (attribute is RemainderAttribute) + switch (attribute) { - if (position != count-1) - throw new InvalidOperationException($"Remainder parameters must be the last parameter in a command. Parameter: {paramInfo.Name} in {paramInfo.Member.DeclaringType.Name}.{paramInfo.Member.Name}"); - - builder.IsRemainder = true; + case SummaryAttribute summary: + builder.Summary = summary.Text; + break; + case OverrideTypeReaderAttribute typeReader: + builder.TypeReader = GetTypeReader(service, paramType, typeReader.TypeReader); + break; + case ParamArrayAttribute _: + builder.IsMultiple = true; + paramType = paramType.GetElementType(); + break; + case RemainderAttribute _: + if (position != count - 1) + throw new InvalidOperationException($"Remainder parameters must be the last parameter in a command. Parameter: {paramInfo.Name} in {paramInfo.Member.DeclaringType.Name}.{paramInfo.Member.Name}"); + + builder.IsRemainder = true; + break; } } diff --git a/src/Discord.Net.Commands/CommandMatch.cs b/src/Discord.Net.Commands/CommandMatch.cs index 74c0de73e..d2bd9ef03 100644 --- a/src/Discord.Net.Commands/CommandMatch.cs +++ b/src/Discord.Net.Commands/CommandMatch.cs @@ -18,8 +18,8 @@ namespace Discord.Commands public Task CheckPreconditionsAsync(ICommandContext context, IServiceProvider services = null) => Command.CheckPreconditionsAsync(context, services); - public Task ParseAsync(ICommandContext context, SearchResult searchResult, PreconditionResult preconditionResult = null) - => Command.ParseAsync(context, Alias.Length, searchResult, preconditionResult); + public Task ParseAsync(ICommandContext context, SearchResult searchResult, PreconditionResult preconditionResult = null, IServiceProvider services = null) + => Command.ParseAsync(context, Alias.Length, searchResult, preconditionResult, services); public Task ExecuteAsync(ICommandContext context, IEnumerable argList, IEnumerable paramList, IServiceProvider services) => Command.ExecuteAsync(context, argList, paramList, services); public Task ExecuteAsync(ICommandContext context, ParseResult parseResult, IServiceProvider services) diff --git a/src/Discord.Net.Commands/CommandParser.cs b/src/Discord.Net.Commands/CommandParser.cs index 5b4ba2480..394f8589d 100644 --- a/src/Discord.Net.Commands/CommandParser.cs +++ b/src/Discord.Net.Commands/CommandParser.cs @@ -1,4 +1,5 @@ -using System.Collections.Immutable; +using System; +using System.Collections.Immutable; using System.Text; using System.Threading.Tasks; @@ -13,7 +14,7 @@ namespace Discord.Commands QuotedParameter } - public static async Task ParseArgs(CommandInfo command, ICommandContext context, string input, int startPos) + public static async Task ParseArgs(CommandInfo command, ICommandContext context, IServiceProvider services, string input, int startPos) { ParameterInfo curParam = null; StringBuilder argBuilder = new StringBuilder(input.Length); @@ -110,7 +111,7 @@ namespace Discord.Commands if (curParam == null) return ParseResult.FromError(CommandError.BadArgCount, "The input text has too many parameters."); - var typeReaderResult = await curParam.Parse(context, argString).ConfigureAwait(false); + var typeReaderResult = await curParam.Parse(context, argString, services).ConfigureAwait(false); if (!typeReaderResult.IsSuccess && typeReaderResult.Error != CommandError.MultipleMatches) return ParseResult.FromError(typeReaderResult); @@ -133,7 +134,7 @@ namespace Discord.Commands if (curParam != null && curParam.IsRemainder) { - var typeReaderResult = await curParam.Parse(context, argBuilder.ToString()).ConfigureAwait(false); + var typeReaderResult = await curParam.Parse(context, argBuilder.ToString(), services).ConfigureAwait(false); if (!typeReaderResult.IsSuccess) return ParseResult.FromError(typeReaderResult); argList.Add(typeReaderResult); diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index f526e8f3b..90e7c8097 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -33,7 +33,7 @@ namespace Discord.Commands public IEnumerable Modules => _moduleDefs.Select(x => x); public IEnumerable Commands => _moduleDefs.SelectMany(x => x.Commands); - public ILookup TypeReaders => _typeReaders.SelectMany(x => x.Value.Select(y => new {y.Key, y.Value})).ToLookup(x => x.Key, x => x.Value); + public ILookup TypeReaders => _typeReaders.SelectMany(x => x.Value.Select(y => new { y.Key, y.Value })).ToLookup(x => x.Key, x => x.Value); public CommandService() : this(new CommandServiceConfig()) { } public CommandService(CommandServiceConfig config) @@ -59,6 +59,9 @@ namespace Discord.Commands foreach (var type in PrimitiveParsers.SupportedTypes) _defaultTypeReaders[type] = PrimitiveTypeReader.Create(type); + _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<>))); @@ -95,7 +98,7 @@ namespace Discord.Commands if (_typedModuleDefs.ContainsKey(type)) throw new ArgumentException($"This module has already been added."); - var module = ModuleClassBuilder.Build(this, typeInfo).FirstOrDefault(); + var module = (await ModuleClassBuilder.BuildAsync(this, typeInfo).ConfigureAwait(false)).FirstOrDefault(); if (module.Value == default(ModuleInfo)) throw new InvalidOperationException($"Could not build the module {type.FullName}, did you pass an invalid type?"); @@ -114,8 +117,8 @@ namespace Discord.Commands await _moduleLock.WaitAsync().ConfigureAwait(false); try { - var types = ModuleClassBuilder.Search(assembly).ToArray(); - var moduleDefs = ModuleClassBuilder.Build(types, this); + var types = await ModuleClassBuilder.SearchAsync(assembly, this).ConfigureAwait(false); + var moduleDefs = await ModuleClassBuilder.BuildAsync(types, this).ConfigureAwait(false); foreach (var info in moduleDefs) { @@ -161,8 +164,7 @@ namespace Discord.Commands await _moduleLock.WaitAsync().ConfigureAwait(false); try { - ModuleInfo module; - if (!_typedModuleDefs.TryRemove(type, out module)) + if (!_typedModuleDefs.TryRemove(type, out var module)) return false; return RemoveModuleInternal(module); @@ -196,20 +198,18 @@ namespace Discord.Commands } public void AddTypeReader(Type type, TypeReader reader) { - var readers = _typeReaders.GetOrAdd(type, x=> new ConcurrentDictionary()); + var readers = _typeReaders.GetOrAdd(type, x => new ConcurrentDictionary()); readers[reader.GetType()] = reader; } internal IDictionary GetTypeReaders(Type type) { - ConcurrentDictionary definedTypeReaders; - if (_typeReaders.TryGetValue(type, out definedTypeReaders)) + if (_typeReaders.TryGetValue(type, out var definedTypeReaders)) return definedTypeReaders; return null; } internal TypeReader GetDefaultTypeReader(Type type) { - TypeReader reader; - if (_defaultTypeReaders.TryGetValue(type, out reader)) + if (_defaultTypeReaders.TryGetValue(type, out var reader)) return reader; var typeInfo = type.GetTypeInfo(); @@ -235,13 +235,13 @@ namespace Discord.Commands } //Execution - public SearchResult Search(ICommandContext context, int argPos) + public SearchResult Search(ICommandContext context, int argPos) => Search(context, context.Message.Content.Substring(argPos)); public SearchResult Search(ICommandContext context, string input) { string searchInput = _caseSensitive ? input : input.ToLowerInvariant(); var matches = _map.GetCommands(searchInput).OrderByDescending(x => x.Command.Priority).ToImmutableArray(); - + if (matches.Length > 0) return SearchResult.FromSuccess(input, matches); else @@ -259,46 +259,83 @@ namespace Discord.Commands return searchResult; var commands = searchResult.Commands; - for (int i = 0; i < commands.Count; i++) + var preconditionResults = new Dictionary(); + + foreach (var match in commands) { - var preconditionResult = await commands[i].CheckPreconditionsAsync(context, services).ConfigureAwait(false); - if (!preconditionResult.IsSuccess) - { - if (commands.Count == 1) - return preconditionResult; - else - continue; - } + preconditionResults[match] = await match.Command.CheckPreconditionsAsync(context, services).ConfigureAwait(false); + } + + var successfulPreconditions = preconditionResults + .Where(x => x.Value.IsSuccess) + .ToArray(); + + if (successfulPreconditions.Length == 0) + { + //All preconditions failed, return the one from the highest priority command + var bestCandidate = preconditionResults + .OrderByDescending(x => x.Key.Command.Priority) + .FirstOrDefault(x => !x.Value.IsSuccess); + return bestCandidate.Value; + } + + //If we get this far, at least one precondition was successful. + + var parseResultsDict = new Dictionary(); + foreach (var pair in successfulPreconditions) + { + var parseResult = await pair.Key.ParseAsync(context, searchResult, pair.Value, services).ConfigureAwait(false); - var parseResult = await commands[i].ParseAsync(context, searchResult, preconditionResult).ConfigureAwait(false); - if (!parseResult.IsSuccess) + if (parseResult.Error == CommandError.MultipleMatches) { - if (parseResult.Error == CommandError.MultipleMatches) + IReadOnlyList argList, paramList; + switch (multiMatchHandling) { - IReadOnlyList argList, paramList; - switch (multiMatchHandling) - { - case MultiMatchHandling.Best: - argList = parseResult.ArgValues.Select(x => x.Values.OrderByDescending(y => y.Score).First()).ToImmutableArray(); - paramList = parseResult.ParamValues.Select(x => x.Values.OrderByDescending(y => y.Score).First()).ToImmutableArray(); - parseResult = ParseResult.FromSuccess(argList, paramList); - break; - } + case MultiMatchHandling.Best: + argList = parseResult.ArgValues.Select(x => x.Values.OrderByDescending(y => y.Score).First()).ToImmutableArray(); + paramList = parseResult.ParamValues.Select(x => x.Values.OrderByDescending(y => y.Score).First()).ToImmutableArray(); + parseResult = ParseResult.FromSuccess(argList, paramList); + break; } + } - if (!parseResult.IsSuccess) - { - if (commands.Count == 1) - return parseResult; - else - continue; - } + parseResultsDict[pair.Key] = parseResult; + } + + // Calculates the 'score' of a command given a parse result + float CalculateScore(CommandMatch match, ParseResult parseResult) + { + float argValuesScore = 0, paramValuesScore = 0; + + if (match.Command.Parameters.Count > 0) + { + argValuesScore = parseResult.ArgValues.Sum(x => x.Values.OrderByDescending(y => y.Score).FirstOrDefault().Score) / match.Command.Parameters.Count; + paramValuesScore = parseResult.ParamValues.Sum(x => x.Values.OrderByDescending(y => y.Score).FirstOrDefault().Score) / match.Command.Parameters.Count; } - return await commands[i].ExecuteAsync(context, parseResult, services).ConfigureAwait(false); + var totalArgsScore = (argValuesScore + paramValuesScore) / 2; + return match.Command.Priority + totalArgsScore * 0.99f; + } + + //Order the parse results by their score so that we choose the most likely result to execute + var parseResults = parseResultsDict + .OrderByDescending(x => CalculateScore(x.Key, x.Value)); + + var successfulParses = parseResults + .Where(x => x.Value.IsSuccess) + .ToArray(); + + if (successfulParses.Length == 0) + { + //All parses failed, return the one from the highest priority command, using score as a tie breaker + var bestMatch = parseResults + .FirstOrDefault(x => !x.Value.IsSuccess); + return bestMatch.Value; } - return SearchResult.FromError(CommandError.UnknownCommand, "This input does not match any overload."); + //If we get this far, at least one parse was successful. Execute the most likely overload. + var chosenOverload = successfulParses[0]; + return await chosenOverload.Key.ExecuteAsync(context, chosenOverload.Value, services).ConfigureAwait(false); } } } diff --git a/src/Discord.Net.Commands/IModuleBase.cs b/src/Discord.Net.Commands/IModuleBase.cs index fda768b53..479724ae3 100644 --- a/src/Discord.Net.Commands/IModuleBase.cs +++ b/src/Discord.Net.Commands/IModuleBase.cs @@ -4,8 +4,8 @@ { void SetContext(ICommandContext context); - void BeforeExecute(); + void BeforeExecute(CommandInfo command); - void AfterExecute(); + void AfterExecute(CommandInfo command); } } diff --git a/src/Discord.Net.Commands/Info/CommandInfo.cs b/src/Discord.Net.Commands/Info/CommandInfo.cs index f187460d5..a97bd4fa5 100644 --- a/src/Discord.Net.Commands/Info/CommandInfo.cs +++ b/src/Discord.Net.Commands/Info/CommandInfo.cs @@ -18,7 +18,7 @@ namespace Discord.Commands private static readonly System.Reflection.MethodInfo _convertParamsMethod = typeof(CommandInfo).GetTypeInfo().GetDeclaredMethod(nameof(ConvertParamsList)); private static readonly ConcurrentDictionary, object>> _arrayConverters = new ConcurrentDictionary, object>>(); - private readonly Func _action; + private readonly Func _action; public ModuleInfo Module { get; } public string Name { get; } @@ -105,15 +105,17 @@ namespace Discord.Commands return PreconditionResult.FromSuccess(); } - public async Task ParseAsync(ICommandContext context, int startIndex, SearchResult searchResult, PreconditionResult preconditionResult = null) + public async Task ParseAsync(ICommandContext context, int startIndex, SearchResult searchResult, PreconditionResult preconditionResult = null, IServiceProvider services = null) { + services = services ?? EmptyServiceProvider.Instance; + if (!searchResult.IsSuccess) return ParseResult.FromError(searchResult); if (preconditionResult != null && !preconditionResult.IsSuccess) return ParseResult.FromError(preconditionResult); string input = searchResult.Text.Substring(startIndex); - return await CommandParser.ParseArgs(this, context, input, 0).ConfigureAwait(false); + return await CommandParser.ParseArgs(this, context, services, input, 0).ConfigureAwait(false); } public Task ExecuteAsync(ICommandContext context, ParseResult parseResult, IServiceProvider services) @@ -181,7 +183,7 @@ namespace Discord.Commands await Module.Service._cmdLogger.DebugAsync($"Executing {GetLogText(context)}").ConfigureAwait(false); try { - await _action(context, args, services).ConfigureAwait(false); + await _action(context, args, services, this).ConfigureAwait(false); } catch (Exception ex) { diff --git a/src/Discord.Net.Commands/Info/ParameterInfo.cs b/src/Discord.Net.Commands/Info/ParameterInfo.cs index 2ecf26a9f..2b71bb90b 100644 --- a/src/Discord.Net.Commands/Info/ParameterInfo.cs +++ b/src/Discord.Net.Commands/Info/ParameterInfo.cs @@ -54,9 +54,10 @@ namespace Discord.Commands return PreconditionResult.FromSuccess(); } - public async Task Parse(ICommandContext context, string input) + public async Task Parse(ICommandContext context, string input, IServiceProvider services = null) { - return await _reader.Read(context, input).ConfigureAwait(false); + services = services ?? EmptyServiceProvider.Instance; + return await _reader.Read(context, input, services).ConfigureAwait(false); } public override string ToString() => Name; diff --git a/src/Discord.Net.Commands/ModuleBase.cs b/src/Discord.Net.Commands/ModuleBase.cs index ed0b49006..f51656e40 100644 --- a/src/Discord.Net.Commands/ModuleBase.cs +++ b/src/Discord.Net.Commands/ModuleBase.cs @@ -15,11 +15,11 @@ namespace Discord.Commands return await Context.Channel.SendMessageAsync(message, isTTS, embed, options).ConfigureAwait(false); } - protected virtual void BeforeExecute() + protected virtual void BeforeExecute(CommandInfo command) { } - protected virtual void AfterExecute() + protected virtual void AfterExecute(CommandInfo command) { } @@ -27,13 +27,11 @@ namespace Discord.Commands void IModuleBase.SetContext(ICommandContext context) { var newValue = context as T; - if (newValue == null) - throw new InvalidOperationException($"Invalid context type. Expected {typeof(T).Name}, got {context.GetType().Name}"); - Context = newValue; + Context = newValue ?? throw new InvalidOperationException($"Invalid context type. Expected {typeof(T).Name}, got {context.GetType().Name}"); } - void IModuleBase.BeforeExecute() => BeforeExecute(); + void IModuleBase.BeforeExecute(CommandInfo command) => BeforeExecute(command); - void IModuleBase.AfterExecute() => AfterExecute(); + void IModuleBase.AfterExecute(CommandInfo command) => AfterExecute(command); } } diff --git a/src/Discord.Net.Commands/PrimitiveParsers.cs b/src/Discord.Net.Commands/PrimitiveParsers.cs index 623ddafa7..6a54ba402 100644 --- a/src/Discord.Net.Commands/PrimitiveParsers.cs +++ b/src/Discord.Net.Commands/PrimitiveParsers.cs @@ -31,11 +31,6 @@ namespace Discord.Commands parserBuilder[typeof(DateTimeOffset)] = (TryParseDelegate)DateTimeOffset.TryParse; parserBuilder[typeof(TimeSpan)] = (TryParseDelegate)TimeSpan.TryParse; parserBuilder[typeof(char)] = (TryParseDelegate)char.TryParse; - parserBuilder[typeof(string)] = (TryParseDelegate)delegate (string str, out string value) - { - value = str; - return true; - }; return parserBuilder.ToImmutable(); } diff --git a/src/Discord.Net.Commands/Readers/ChannelTypeReader.cs b/src/Discord.Net.Commands/Readers/ChannelTypeReader.cs index d2e34b436..72c62282e 100644 --- a/src/Discord.Net.Commands/Readers/ChannelTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/ChannelTypeReader.cs @@ -9,7 +9,7 @@ namespace Discord.Commands internal class ChannelTypeReader : TypeReader where T : class, IChannel { - public override async Task Read(ICommandContext context, string input) + public override async Task Read(ICommandContext context, string input, IServiceProvider services) { if (context.Guild != null) { diff --git a/src/Discord.Net.Commands/Readers/EnumTypeReader.cs b/src/Discord.Net.Commands/Readers/EnumTypeReader.cs index 7b2ff505a..383b8e63c 100644 --- a/src/Discord.Net.Commands/Readers/EnumTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/EnumTypeReader.cs @@ -44,12 +44,11 @@ namespace Discord.Commands _enumsByValue = byValueBuilder.ToImmutable(); } - public override Task Read(ICommandContext context, string input) + public override Task Read(ICommandContext context, string input, IServiceProvider services) { - T baseValue; object enumValue; - if (_tryParse(input, out baseValue)) + if (_tryParse(input, out T baseValue)) { if (_enumsByValue.TryGetValue(baseValue, out enumValue)) return Task.FromResult(TypeReaderResult.FromSuccess(enumValue)); diff --git a/src/Discord.Net.Commands/Readers/MessageTypeReader.cs b/src/Discord.Net.Commands/Readers/MessageTypeReader.cs index 9baa1901a..895713e4f 100644 --- a/src/Discord.Net.Commands/Readers/MessageTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/MessageTypeReader.cs @@ -1,4 +1,5 @@ -using System.Globalization; +using System; +using System.Globalization; using System.Threading.Tasks; namespace Discord.Commands @@ -6,15 +7,14 @@ namespace Discord.Commands internal class MessageTypeReader : TypeReader where T : class, IMessage { - public override async Task Read(ICommandContext context, string input) + public override async Task Read(ICommandContext context, string input, IServiceProvider services) { ulong id; //By Id (1.0) if (ulong.TryParse(input, NumberStyles.None, CultureInfo.InvariantCulture, out id)) { - var msg = await context.Channel.GetMessageAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T; - if (msg != null) + if (await context.Channel.GetMessageAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) is T msg) return TypeReaderResult.FromSuccess(msg); } diff --git a/src/Discord.Net.Commands/Readers/PrimitiveTypeReader.cs b/src/Discord.Net.Commands/Readers/PrimitiveTypeReader.cs index aa4c7c7a4..2656741f0 100644 --- a/src/Discord.Net.Commands/Readers/PrimitiveTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/PrimitiveTypeReader.cs @@ -15,17 +15,25 @@ namespace Discord.Commands internal class PrimitiveTypeReader : TypeReader { private readonly TryParseDelegate _tryParse; + private readonly float _score; public PrimitiveTypeReader() + : this(PrimitiveParsers.Get(), 1) + { } + + public PrimitiveTypeReader(TryParseDelegate tryParse, float score) { - _tryParse = PrimitiveParsers.Get(); + if (score < 0 || score > 1) + throw new ArgumentOutOfRangeException(nameof(score), score, "Scores must be within the range [0, 1]"); + + _tryParse = tryParse; + _score = score; } - public override Task Read(ICommandContext context, string input) + public override Task Read(ICommandContext context, string input, IServiceProvider services) { - T value; - if (_tryParse(input, out value)) - return Task.FromResult(TypeReaderResult.FromSuccess(value)); + if (_tryParse(input, out T value)) + return Task.FromResult(TypeReaderResult.FromSuccess(new TypeReaderValue(value, _score))); return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, $"Failed to parse {typeof(T).Name}")); } } diff --git a/src/Discord.Net.Commands/Readers/RoleTypeReader.cs b/src/Discord.Net.Commands/Readers/RoleTypeReader.cs index a90432782..17786e6f0 100644 --- a/src/Discord.Net.Commands/Readers/RoleTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/RoleTypeReader.cs @@ -9,7 +9,7 @@ namespace Discord.Commands internal class RoleTypeReader : TypeReader where T : class, IRole { - public override Task Read(ICommandContext context, string input) + public override Task Read(ICommandContext context, string input, IServiceProvider services) { ulong id; diff --git a/src/Discord.Net.Commands/Readers/TypeReader.cs b/src/Discord.Net.Commands/Readers/TypeReader.cs index d53491e92..2c4644376 100644 --- a/src/Discord.Net.Commands/Readers/TypeReader.cs +++ b/src/Discord.Net.Commands/Readers/TypeReader.cs @@ -1,9 +1,10 @@ -using System.Threading.Tasks; +using System; +using System.Threading.Tasks; namespace Discord.Commands { public abstract class TypeReader { - public abstract Task Read(ICommandContext context, string input); + public abstract Task Read(ICommandContext context, string input, IServiceProvider services); } } diff --git a/src/Discord.Net.Commands/Readers/UserTypeReader.cs b/src/Discord.Net.Commands/Readers/UserTypeReader.cs index d7fc6cfdc..c71dac2d2 100644 --- a/src/Discord.Net.Commands/Readers/UserTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/UserTypeReader.cs @@ -10,7 +10,7 @@ namespace Discord.Commands internal class UserTypeReader : TypeReader where T : class, IUser { - public override async Task Read(ICommandContext context, string input) + public override async Task Read(ICommandContext context, string input, IServiceProvider services) { var results = new Dictionary(); IReadOnlyCollection channelUsers = (await context.Channel.GetUsersAsync(CacheMode.CacheOnly).Flatten().ConfigureAwait(false)).ToArray(); //TODO: must be a better way? @@ -43,8 +43,7 @@ namespace Discord.Commands if (index >= 0) { string username = input.Substring(0, index); - ushort discriminator; - if (ushort.TryParse(input.Substring(index + 1), out discriminator)) + if (ushort.TryParse(input.Substring(index + 1), out ushort discriminator)) { var channelUser = channelUsers.FirstOrDefault(x => x.DiscriminatorValue == discriminator && string.Equals(username, x.Username, StringComparison.OrdinalIgnoreCase)); diff --git a/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs b/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs index b6ceda426..ab88f66ae 100644 --- a/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs +++ b/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs @@ -58,7 +58,7 @@ namespace Discord.Commands { foreach (var prop in ownerType.DeclaredProperties) { - if (prop.GetMethod?.IsStatic == false && prop.SetMethod?.IsPublic == true && prop.GetCustomAttribute() == null) + if (prop.SetMethod?.IsStatic == false && prop.SetMethod?.IsPublic == true && prop.GetCustomAttribute() == null) result.Add(prop); } ownerType = ownerType.BaseType.GetTypeInfo(); From 224d0403dbd5bc8352de53a09129847cef45fe07 Mon Sep 17 00:00:00 2001 From: Pat Murphy Date: Thu, 29 Jun 2017 14:05:16 -0700 Subject: [PATCH 226/243] Adding Equals() overloads for reactions/emotes (#723) --- src/Discord.Net.Core/Entities/Emotes/Emoji.cs | 13 +++++++++++ src/Discord.Net.Core/Entities/Emotes/Emote.cs | 19 ++++++++++++++++ .../Entities/Messages/SocketReaction.cs | 22 +++++++++++++++++++ 3 files changed, 54 insertions(+) diff --git a/src/Discord.Net.Core/Entities/Emotes/Emoji.cs b/src/Discord.Net.Core/Entities/Emotes/Emoji.cs index 07cee10a9..76eef58c8 100644 --- a/src/Discord.Net.Core/Entities/Emotes/Emoji.cs +++ b/src/Discord.Net.Core/Entities/Emotes/Emoji.cs @@ -21,5 +21,18 @@ public string Name { get; } public override string ToString() => Name; + + public override bool Equals(object other) + { + if (other == null) return false; + if (other == this) return true; + + var otherEmoji = other as Emoji; + if (otherEmoji == null) return false; + + return string.Equals(Name, otherEmoji.Name); + } + + public override int GetHashCode() => Name.GetHashCode(); } } diff --git a/src/Discord.Net.Core/Entities/Emotes/Emote.cs b/src/Discord.Net.Core/Entities/Emotes/Emote.cs index 76c20a77d..f498c818e 100644 --- a/src/Discord.Net.Core/Entities/Emotes/Emote.cs +++ b/src/Discord.Net.Core/Entities/Emotes/Emote.cs @@ -25,6 +25,25 @@ namespace Discord Name = name; } + public override bool Equals(object other) + { + if (other == null) return false; + if (other == this) return true; + + var otherEmote = other as Emote; + if (otherEmote == null) return false; + + return string.Equals(Name, otherEmote.Name) && Id == otherEmote.Id; + } + + public override int GetHashCode() + { + unchecked + { + return (Name.GetHashCode() * 397) ^ Id.GetHashCode(); + } + } + /// /// Parse an Emote from its raw format /// diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketReaction.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketReaction.cs index 9f58f1cf6..35bee9e68 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketReaction.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketReaction.cs @@ -29,5 +29,27 @@ namespace Discord.WebSocket emote = new Emoji(model.Emoji.Name); return new SocketReaction(channel, model.MessageId, message, model.UserId, user, emote); } + + public override bool Equals(object other) + { + if (other == null) return false; + if (other == this) return true; + + var otherReaction = other as SocketReaction; + if (otherReaction == null) return false; + + return UserId == otherReaction.UserId && MessageId == otherReaction.MessageId && Emote.Equals(otherReaction.Emote); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = UserId.GetHashCode(); + hashCode = (hashCode * 397) ^ MessageId.GetHashCode(); + hashCode = (hashCode * 397) ^ Emote.GetHashCode(); + return hashCode; + } + } } } From 394e0aa4d19fe5bc30d47cfcc30423e18186770c Mon Sep 17 00:00:00 2001 From: RogueException Date: Thu, 29 Jun 2017 18:06:12 -0300 Subject: [PATCH 227/243] Reorganized properties in Emoji.cs --- src/Discord.Net.Core/Entities/Emotes/Emoji.cs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Emotes/Emoji.cs b/src/Discord.Net.Core/Entities/Emotes/Emoji.cs index 76eef58c8..c2dfc31ad 100644 --- a/src/Discord.Net.Core/Entities/Emotes/Emoji.cs +++ b/src/Discord.Net.Core/Entities/Emotes/Emoji.cs @@ -6,6 +6,14 @@ public class Emoji : IEmote { // TODO: need to constrain this to unicode-only emojis somehow + + /// + /// The unicode representation of this emote. + /// + public string Name { get; } + + public override string ToString() => Name; + /// /// Creates a unicode emoji. /// @@ -15,13 +23,6 @@ Name = unicode; } - /// - /// The unicode representation of this emote. - /// - public string Name { get; } - - public override string ToString() => Name; - public override bool Equals(object other) { if (other == null) return false; From b96748f9c3d4851191f0e4893fd19a32690df6fc Mon Sep 17 00:00:00 2001 From: Finite Reality Date: Thu, 29 Jun 2017 22:30:26 +0100 Subject: [PATCH 228/243] Allow arbitrary attributes to be added to commands (#458) * Allow arbitrary attributes to be added to commands I still don't approve adding type info back into commands, so I decided to use this solution for allowing arbitrary attributes to be added to commands. Add attributes property to ParameterBuilder Add Attributes properties to info types * Why on earth git * Add using for system so that Attribute can be used --- .../Builders/CommandBuilder.cs | 8 ++++++++ .../Builders/ModuleBuilder.cs | 8 ++++++++ .../Builders/ModuleClassBuilder.cs | 11 ++++++++++- .../Builders/ParameterBuilder.cs | 10 +++++++++- src/Discord.Net.Commands/Info/CommandInfo.cs | 2 ++ src/Discord.Net.Commands/Info/ModuleInfo.cs | 17 +++++++++++++++++ src/Discord.Net.Commands/Info/ParameterInfo.cs | 2 ++ 7 files changed, 56 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net.Commands/Builders/CommandBuilder.cs b/src/Discord.Net.Commands/Builders/CommandBuilder.cs index 30db1fa62..0a79f13f2 100644 --- a/src/Discord.Net.Commands/Builders/CommandBuilder.cs +++ b/src/Discord.Net.Commands/Builders/CommandBuilder.cs @@ -10,6 +10,7 @@ namespace Discord.Commands.Builders { private readonly List _preconditions; private readonly List _parameters; + private readonly List _attributes; private readonly List _aliases; public ModuleBuilder Module { get; } @@ -24,6 +25,7 @@ namespace Discord.Commands.Builders public IReadOnlyList Preconditions => _preconditions; public IReadOnlyList Parameters => _parameters; + public IReadOnlyList Attributes => _attributes; public IReadOnlyList Aliases => _aliases; //Automatic @@ -33,6 +35,7 @@ namespace Discord.Commands.Builders _preconditions = new List(); _parameters = new List(); + _attributes = new List(); _aliases = new List(); } //User-defined @@ -83,6 +86,11 @@ namespace Discord.Commands.Builders } return this; } + public CommandBuilder AddAttributes(params Attribute[] attributes) + { + _attributes.AddRange(attributes); + return this; + } public CommandBuilder AddPrecondition(PreconditionAttribute precondition) { _preconditions.Add(precondition); diff --git a/src/Discord.Net.Commands/Builders/ModuleBuilder.cs b/src/Discord.Net.Commands/Builders/ModuleBuilder.cs index 525907b8b..e5e688fe9 100644 --- a/src/Discord.Net.Commands/Builders/ModuleBuilder.cs +++ b/src/Discord.Net.Commands/Builders/ModuleBuilder.cs @@ -10,6 +10,7 @@ namespace Discord.Commands.Builders private readonly List _commands; private readonly List _submodules; private readonly List _preconditions; + private readonly List _attributes; private readonly List _aliases; public CommandService Service { get; } @@ -21,6 +22,7 @@ namespace Discord.Commands.Builders public IReadOnlyList Commands => _commands; public IReadOnlyList Modules => _submodules; public IReadOnlyList Preconditions => _preconditions; + public IReadOnlyList Attributes => _attributes; public IReadOnlyList Aliases => _aliases; //Automatic @@ -32,6 +34,7 @@ namespace Discord.Commands.Builders _commands = new List(); _submodules = new List(); _preconditions = new List(); + _attributes = new List(); _aliases = new List(); } //User-defined @@ -69,6 +72,11 @@ namespace Discord.Commands.Builders } return this; } + public ModuleBuilder AddAttributes(params Attribute[] attributes) + { + _attributes.AddRange(attributes); + return this; + } public ModuleBuilder AddPrecondition(PreconditionAttribute precondition) { _preconditions.Add(precondition); diff --git a/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs index 401396900..2df1d805f 100644 --- a/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs +++ b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs @@ -122,6 +122,9 @@ namespace Discord.Commands case PreconditionAttribute precondition: builder.AddPrecondition(precondition); break; + default: + builder.AddAttributes(attribute); + break; } } @@ -173,6 +176,9 @@ namespace Discord.Commands case PreconditionAttribute precondition: builder.AddPrecondition(precondition); break; + default: + builder.AddAttributes(attribute); + break; } } @@ -239,6 +245,9 @@ namespace Discord.Commands builder.IsRemainder = true; break; + default: + builder.AddAttributes(attribute); + break; } } @@ -289,4 +298,4 @@ namespace Discord.Commands !methodInfo.IsGenericMethod; } } -} \ No newline at end of file +} diff --git a/src/Discord.Net.Commands/Builders/ParameterBuilder.cs b/src/Discord.Net.Commands/Builders/ParameterBuilder.cs index d2bebbad0..d1782d7ea 100644 --- a/src/Discord.Net.Commands/Builders/ParameterBuilder.cs +++ b/src/Discord.Net.Commands/Builders/ParameterBuilder.cs @@ -8,7 +8,8 @@ namespace Discord.Commands.Builders { public class ParameterBuilder { - private readonly List _preconditions; + private readonly List _preconditions; + private readonly List _attributes; public CommandBuilder Command { get; } public string Name { get; internal set; } @@ -22,11 +23,13 @@ namespace Discord.Commands.Builders public string Summary { get; set; } public IReadOnlyList Preconditions => _preconditions; + public IReadOnlyList Attributes => _attributes; //Automatic internal ParameterBuilder(CommandBuilder command) { _preconditions = new List(); + _attributes = new List(); Command = command; } @@ -84,6 +87,11 @@ namespace Discord.Commands.Builders return this; } + public ParameterBuilder AddAttributes(params Attribute[] attributes) + { + _attributes.AddRange(attributes); + return this; + } public ParameterBuilder AddPrecondition(ParameterPreconditionAttribute precondition) { _preconditions.Add(precondition); diff --git a/src/Discord.Net.Commands/Info/CommandInfo.cs b/src/Discord.Net.Commands/Info/CommandInfo.cs index a97bd4fa5..00041f22d 100644 --- a/src/Discord.Net.Commands/Info/CommandInfo.cs +++ b/src/Discord.Net.Commands/Info/CommandInfo.cs @@ -31,6 +31,7 @@ namespace Discord.Commands public IReadOnlyList Aliases { get; } public IReadOnlyList Parameters { get; } public IReadOnlyList Preconditions { get; } + public IReadOnlyList Attributes { get; } internal CommandInfo(CommandBuilder builder, ModuleInfo module, CommandService service) { @@ -57,6 +58,7 @@ namespace Discord.Commands .ToImmutableArray(); Preconditions = builder.Preconditions.ToImmutableArray(); + Attributes = builder.Attributes.ToImmutableArray(); Parameters = builder.Parameters.Select(x => x.Build(this)).ToImmutableArray(); HasVarArgs = builder.Parameters.Count > 0 ? builder.Parameters[builder.Parameters.Count - 1].IsMultiple : false; diff --git a/src/Discord.Net.Commands/Info/ModuleInfo.cs b/src/Discord.Net.Commands/Info/ModuleInfo.cs index a2094df65..97b90bf4e 100644 --- a/src/Discord.Net.Commands/Info/ModuleInfo.cs +++ b/src/Discord.Net.Commands/Info/ModuleInfo.cs @@ -1,3 +1,4 @@ +using System; using System.Linq; using System.Collections.Generic; using System.Collections.Immutable; @@ -16,6 +17,7 @@ namespace Discord.Commands public IReadOnlyList Aliases { get; } public IReadOnlyList Commands { get; } public IReadOnlyList Preconditions { get; } + public IReadOnlyList Attributes { get; } public IReadOnlyList Submodules { get; } public ModuleInfo Parent { get; } public bool IsSubmodule => Parent != null; @@ -32,6 +34,7 @@ namespace Discord.Commands Aliases = BuildAliases(builder, service).ToImmutableArray(); Commands = builder.Commands.Select(x => x.Build(this, service)).ToImmutableArray(); Preconditions = BuildPreconditions(builder).ToImmutableArray(); + Attributes = BuildAttributes(builder).ToImmutableArray(); Submodules = BuildSubmodules(builder, service).ToImmutableArray(); } @@ -86,5 +89,19 @@ namespace Discord.Commands return result; } + + private static List BuildAttributes(ModuleBuilder builder) + { + var result = new List(); + + ModuleBuilder parent = builder; + while (parent != null) + { + result.AddRange(parent.Attributes); + parent = parent.Parent; + } + + return result; + } } } diff --git a/src/Discord.Net.Commands/Info/ParameterInfo.cs b/src/Discord.Net.Commands/Info/ParameterInfo.cs index 2b71bb90b..e417b1ab6 100644 --- a/src/Discord.Net.Commands/Info/ParameterInfo.cs +++ b/src/Discord.Net.Commands/Info/ParameterInfo.cs @@ -21,6 +21,7 @@ namespace Discord.Commands public object DefaultValue { get; } public IReadOnlyList Preconditions { get; } + public IReadOnlyList Attributes { get; } internal ParameterInfo(ParameterBuilder builder, CommandInfo command, CommandService service) { @@ -36,6 +37,7 @@ namespace Discord.Commands DefaultValue = builder.DefaultValue; Preconditions = builder.Preconditions.ToImmutableArray(); + Attributes = builder.Attributes.ToImmutableArray(); _reader = builder.TypeReader; } From 74f6a4b392fb9ff6dfed77f6a1195f93a9c16efe Mon Sep 17 00:00:00 2001 From: Finite Reality Date: Thu, 29 Jun 2017 23:21:05 +0100 Subject: [PATCH 229/243] Allow commands to return a Task (#466) * Allow commands to return a Task This allows bot developers to centralize command result logic by using result data whether the command as successful or not. Example usage: ```csharp var _result = await Commands.ExecuteAsync(context, argPos); if (_result is RuntimeResult result) { await message.Channel.SendMessageAsync(result.Reason); } else if (!_result.IsSuccess) { // Previous error handling } ``` The RuntimeResult class can be subclassed too, for example: ```csharp var _result = await Commands.ExecuteAsync(context, argPos); if (_result is MySubclassedResult result) { var builder = new EmbedBuilder(); for (var pair in result.Data) { builder.AddField(pair.Key, pair.Value, true); } await message.Channel.SendMessageAsync("", embed: builder); } else if (_result is RuntimeResult result) { await message.Channel.SendMessageAsync(result.Reason); } else if (!_result.IsSuccess) { // Previous error handling } ``` * Make RuntimeResult's ctor protected * Make RuntimeResult abstract It never really made sense to have it instantiable in the first place, frankly. --- .../Builders/ModuleClassBuilder.cs | 24 ++++++--- src/Discord.Net.Commands/CommandError.cs | 5 +- src/Discord.Net.Commands/CommandMatch.cs | 4 +- src/Discord.Net.Commands/Info/CommandInfo.cs | 52 +++++++++++++------ .../Results/RuntimeResult.cs | 27 ++++++++++ 5 files changed, 86 insertions(+), 26 deletions(-) create mode 100644 src/Discord.Net.Commands/Results/RuntimeResult.cs diff --git a/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs index 2df1d805f..be3449b6f 100644 --- a/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs +++ b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs @@ -197,22 +197,34 @@ namespace Discord.Commands var createInstance = ReflectionUtils.CreateBuilder(typeInfo, service); - builder.Callback = async (ctx, args, map, cmd) => + async Task ExecuteCallback(ICommandContext context, object[] args, IServiceProvider services, CommandInfo cmd) { - var instance = createInstance(map); - instance.SetContext(ctx); + var instance = createInstance(services); + instance.SetContext(context); + try { instance.BeforeExecute(cmd); + var task = method.Invoke(instance, args) as Task ?? Task.Delay(0); - await task.ConfigureAwait(false); + if (task is Task resultTask) + { + return await resultTask.ConfigureAwait(false); + } + else + { + await task.ConfigureAwait(false); + return ExecuteResult.FromSuccess(); + } } finally { instance.AfterExecute(cmd); (instance as IDisposable)?.Dispose(); } - }; + } + + builder.Callback = ExecuteCallback; } private static void BuildParameter(ParameterBuilder builder, System.Reflection.ParameterInfo paramInfo, int position, int count, CommandService service) @@ -293,7 +305,7 @@ namespace Discord.Commands private static bool IsValidCommandDefinition(MethodInfo methodInfo) { return methodInfo.IsDefined(typeof(CommandAttribute)) && - (methodInfo.ReturnType == typeof(Task) || methodInfo.ReturnType == typeof(void)) && + (methodInfo.ReturnType == typeof(Task) || methodInfo.ReturnType == typeof(Task)) && !methodInfo.IsStatic && !methodInfo.IsGenericMethod; } diff --git a/src/Discord.Net.Commands/CommandError.cs b/src/Discord.Net.Commands/CommandError.cs index 41b4822ad..abfc14e1d 100644 --- a/src/Discord.Net.Commands/CommandError.cs +++ b/src/Discord.Net.Commands/CommandError.cs @@ -18,6 +18,9 @@ UnmetPrecondition, //Execute - Exception + Exception, + + //Runtime + Unsuccessful } } diff --git a/src/Discord.Net.Commands/CommandMatch.cs b/src/Discord.Net.Commands/CommandMatch.cs index d2bd9ef03..d922a2229 100644 --- a/src/Discord.Net.Commands/CommandMatch.cs +++ b/src/Discord.Net.Commands/CommandMatch.cs @@ -20,9 +20,9 @@ namespace Discord.Commands => Command.CheckPreconditionsAsync(context, services); public Task ParseAsync(ICommandContext context, SearchResult searchResult, PreconditionResult preconditionResult = null, IServiceProvider services = null) => Command.ParseAsync(context, Alias.Length, searchResult, preconditionResult, services); - public Task ExecuteAsync(ICommandContext context, IEnumerable argList, IEnumerable paramList, IServiceProvider services) + public Task ExecuteAsync(ICommandContext context, IEnumerable argList, IEnumerable paramList, IServiceProvider services) => Command.ExecuteAsync(context, argList, paramList, services); - public Task ExecuteAsync(ICommandContext context, ParseResult parseResult, IServiceProvider services) + public Task ExecuteAsync(ICommandContext context, ParseResult parseResult, IServiceProvider services) => Command.ExecuteAsync(context, parseResult, services); } } diff --git a/src/Discord.Net.Commands/Info/CommandInfo.cs b/src/Discord.Net.Commands/Info/CommandInfo.cs index 00041f22d..60df2a6a9 100644 --- a/src/Discord.Net.Commands/Info/CommandInfo.cs +++ b/src/Discord.Net.Commands/Info/CommandInfo.cs @@ -36,14 +36,14 @@ namespace Discord.Commands internal CommandInfo(CommandBuilder builder, ModuleInfo module, CommandService service) { Module = module; - + Name = builder.Name; Summary = builder.Summary; Remarks = builder.Remarks; RunMode = (builder.RunMode == RunMode.Default ? service._defaultRunMode : builder.RunMode); Priority = builder.Priority; - + Aliases = module.Aliases .Permutate(builder.Aliases, (first, second) => { @@ -106,7 +106,7 @@ namespace Discord.Commands return PreconditionResult.FromSuccess(); } - + public async Task ParseAsync(ICommandContext context, int startIndex, SearchResult searchResult, PreconditionResult preconditionResult = null, IServiceProvider services = null) { services = services ?? EmptyServiceProvider.Instance; @@ -115,35 +115,35 @@ namespace Discord.Commands return ParseResult.FromError(searchResult); if (preconditionResult != null && !preconditionResult.IsSuccess) return ParseResult.FromError(preconditionResult); - + string input = searchResult.Text.Substring(startIndex); return await CommandParser.ParseArgs(this, context, services, input, 0).ConfigureAwait(false); } - public Task ExecuteAsync(ICommandContext context, ParseResult parseResult, IServiceProvider services) + public Task ExecuteAsync(ICommandContext context, ParseResult parseResult, IServiceProvider services) { if (!parseResult.IsSuccess) - return Task.FromResult(ExecuteResult.FromError(parseResult)); + return Task.FromResult((IResult)ExecuteResult.FromError(parseResult)); var argList = new object[parseResult.ArgValues.Count]; for (int i = 0; i < parseResult.ArgValues.Count; i++) { if (!parseResult.ArgValues[i].IsSuccess) - return Task.FromResult(ExecuteResult.FromError(parseResult.ArgValues[i])); + return Task.FromResult((IResult)ExecuteResult.FromError(parseResult.ArgValues[i])); argList[i] = parseResult.ArgValues[i].Values.First().Value; } - + var paramList = new object[parseResult.ParamValues.Count]; for (int i = 0; i < parseResult.ParamValues.Count; i++) { if (!parseResult.ParamValues[i].IsSuccess) - return Task.FromResult(ExecuteResult.FromError(parseResult.ParamValues[i])); + return Task.FromResult((IResult)ExecuteResult.FromError(parseResult.ParamValues[i])); paramList[i] = parseResult.ParamValues[i].Values.First().Value; } return ExecuteAsync(context, argList, paramList, services); } - public async Task ExecuteAsync(ICommandContext context, IEnumerable argList, IEnumerable paramList, IServiceProvider services) + public async Task ExecuteAsync(ICommandContext context, IEnumerable argList, IEnumerable paramList, IServiceProvider services) { services = services ?? EmptyServiceProvider.Instance; @@ -163,10 +163,9 @@ namespace Discord.Commands switch (RunMode) { case RunMode.Sync: //Always sync - await ExecuteAsyncInternal(context, args, services).ConfigureAwait(false); - break; + return await ExecuteAsyncInternal(context, args, services).ConfigureAwait(false); case RunMode.Async: //Always async - var t2 = Task.Run(async () => + var t2 = Task.Run(async () => { await ExecuteAsyncInternal(context, args, services).ConfigureAwait(false); }); @@ -180,12 +179,26 @@ namespace Discord.Commands } } - private async Task ExecuteAsyncInternal(ICommandContext context, object[] args, IServiceProvider services) + private async Task ExecuteAsyncInternal(ICommandContext context, object[] args, IServiceProvider services) { await Module.Service._cmdLogger.DebugAsync($"Executing {GetLogText(context)}").ConfigureAwait(false); try { - await _action(context, args, services, this).ConfigureAwait(false); + var task = _action(context, args, services, this); + if (task is Task resultTask) + { + var result = await resultTask.ConfigureAwait(false); + if (result is RuntimeResult execResult) + return execResult; + } + else if (task is Task execTask) + { + return await execTask.ConfigureAwait(false); + } + else + await task.ConfigureAwait(false); + + return ExecuteResult.FromSuccess(); } catch (Exception ex) { @@ -202,8 +215,13 @@ namespace Discord.Commands else ExceptionDispatchInfo.Capture(ex).Throw(); } + + return ExecuteResult.FromError(CommandError.Exception, ex.Message); + } + finally + { + await Module.Service._cmdLogger.VerboseAsync($"Executed {GetLogText(context)}").ConfigureAwait(false); } - await Module.Service._cmdLogger.VerboseAsync($"Executed {GetLogText(context)}").ConfigureAwait(false); } private object[] GenerateArgs(IEnumerable argList, IEnumerable paramsList) @@ -240,7 +258,7 @@ namespace Discord.Commands => paramsList.Cast().ToArray(); internal string GetLogText(ICommandContext context) - { + { if (context.Guild != null) return $"\"{Name}\" for {context.User} in {context.Guild}/{context.Channel}"; else diff --git a/src/Discord.Net.Commands/Results/RuntimeResult.cs b/src/Discord.Net.Commands/Results/RuntimeResult.cs new file mode 100644 index 000000000..2a326a7a3 --- /dev/null +++ b/src/Discord.Net.Commands/Results/RuntimeResult.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; + +namespace Discord.Commands +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public abstract class RuntimeResult : IResult + { + protected RuntimeResult(CommandError? error, string reason) + { + Error = error; + Reason = reason; + } + + public CommandError? Error { get; } + public string Reason { get; } + + public bool IsSuccess => !Error.HasValue; + + string IResult.ErrorReason => Reason; + + public override string ToString() => Reason ?? (IsSuccess ? "Successful" : "Unsuccessful"); + private string DebuggerDisplay => IsSuccess ? $"Success: {Reason ?? "No Reason"}" : $"{Error}: {Reason}"; + } +} From fdd38c8d7f9292b6ab5e25ac9596b973e07fcca6 Mon Sep 17 00:00:00 2001 From: Finite Reality Date: Thu, 29 Jun 2017 23:44:08 +0100 Subject: [PATCH 230/243] Add embed builder extensions (#460) * Add embed builder extensions People in #dotnet_discord-net suggested that this should be part of the lib after I demonstrated it * Move some extensions into EmbedBuilder [2] Apparently git didn't like that previous commit * Fix error with EmbedBuilderExtensions A summary of issues which happened: - Git decided to add an amend commit (I told it to quit?) - VS Code thinks everything is an error so it wasn't helpful - dotnet decided to think there was no error until I deleted all build outputs and rebuild Sometimes I question my ability to use version control properly. --- .../Entities/Messages/EmbedBuilder.cs | 32 +++++++++++++++++++ .../Extensions/EmbedBuilderExtensions.cs | 20 ++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 src/Discord.Net.Rest/Extensions/EmbedBuilderExtensions.cs diff --git a/src/Discord.Net.Rest/Entities/Messages/EmbedBuilder.cs b/src/Discord.Net.Rest/Entities/Messages/EmbedBuilder.cs index c299bd1a1..2331f6749 100644 --- a/src/Discord.Net.Rest/Entities/Messages/EmbedBuilder.cs +++ b/src/Discord.Net.Rest/Entities/Messages/EmbedBuilder.cs @@ -137,6 +137,17 @@ namespace Discord Author = author; return this; } + public EmbedBuilder WithAuthor(string name, string iconUrl = null, string url = null) + { + var author = new EmbedAuthorBuilder + { + Name = name, + IconUrl = iconUrl, + Url = url + }; + Author = author; + return this; + } public EmbedBuilder WithFooter(EmbedFooterBuilder footer) { Footer = footer; @@ -149,6 +160,16 @@ namespace Discord Footer = footer; return this; } + public EmbedBuilder WithFooter(string text, string iconUrl = null) + { + var footer = new EmbedFooterBuilder + { + Text = text, + IconUrl = iconUrl + }; + Footer = footer; + return this; + } public EmbedBuilder AddField(string name, object value) { @@ -185,6 +206,17 @@ namespace Discord this.AddField(field); return this; } + public EmbedBuilder AddField(string title, string text, bool inline = false) + { + var field = new EmbedFieldBuilder + { + Name = title, + Value = text, + IsInline = inline + }; + _fields.Add(field); + return this; + } public Embed Build() { diff --git a/src/Discord.Net.Rest/Extensions/EmbedBuilderExtensions.cs b/src/Discord.Net.Rest/Extensions/EmbedBuilderExtensions.cs new file mode 100644 index 000000000..64f96c93f --- /dev/null +++ b/src/Discord.Net.Rest/Extensions/EmbedBuilderExtensions.cs @@ -0,0 +1,20 @@ +namespace Discord +{ + public static class EmbedBuilderExtensions + { + public static EmbedBuilder WithColor(this EmbedBuilder builder, uint rawValue) => + builder.WithColor(new Color(rawValue)); + + public static EmbedBuilder WithColor(this EmbedBuilder builder, byte r, byte g, byte b) => + builder.WithColor(new Color(r, g, b)); + + public static EmbedBuilder WithColor(this EmbedBuilder builder, float r, float g, float b) => + builder.WithColor(new Color(r, g, b)); + + public static EmbedBuilder WithAuthor(this EmbedBuilder builder, IUser user) => + builder.WithAuthor($"{user.Username}#{user.Discriminator}", user.AvatarUrl); + + public static EmbedBuilder WithAuthor(this EmbedBuilder builder, IGuildUser user) => + builder.WithAuthor($"{user.Nickname ?? user.Username}#{user.Discriminator}", user.AvatarUrl); + } +} From 14dfc48df3dcce3842731a08e73f76ccb5dee675 Mon Sep 17 00:00:00 2001 From: RogueException Date: Thu, 29 Jun 2017 19:25:21 -0300 Subject: [PATCH 231/243] Style cleanup --- .../Builders/CommandBuilder.cs | 2 +- .../Builders/ModuleBuilder.cs | 2 +- .../Builders/ModuleClassBuilder.cs | 2 +- src/Discord.Net.Commands/Info/CommandInfo.cs | 4 +- src/Discord.Net.Rest/DiscordRestApiClient.cs | 10 ++-- .../DiscordSocketClient.cs | 51 +++++++++---------- 6 files changed, 35 insertions(+), 36 deletions(-) diff --git a/src/Discord.Net.Commands/Builders/CommandBuilder.cs b/src/Discord.Net.Commands/Builders/CommandBuilder.cs index 0a79f13f2..b6d002c70 100644 --- a/src/Discord.Net.Commands/Builders/CommandBuilder.cs +++ b/src/Discord.Net.Commands/Builders/CommandBuilder.cs @@ -80,7 +80,7 @@ namespace Discord.Commands.Builders { for (int i = 0; i < aliases.Length; i++) { - var alias = aliases[i] ?? ""; + string alias = aliases[i] ?? ""; if (!_aliases.Contains(alias)) _aliases.Add(alias); } diff --git a/src/Discord.Net.Commands/Builders/ModuleBuilder.cs b/src/Discord.Net.Commands/Builders/ModuleBuilder.cs index e5e688fe9..0a33c9e26 100644 --- a/src/Discord.Net.Commands/Builders/ModuleBuilder.cs +++ b/src/Discord.Net.Commands/Builders/ModuleBuilder.cs @@ -66,7 +66,7 @@ namespace Discord.Commands.Builders { for (int i = 0; i < aliases.Length; i++) { - var alias = aliases[i] ?? ""; + string alias = aliases[i] ?? ""; if (!_aliases.Contains(alias)) _aliases.Add(alias); } diff --git a/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs index be3449b6f..b8fbbf462 100644 --- a/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs +++ b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs @@ -20,7 +20,7 @@ namespace Discord.Commands info.GetCustomAttribute() == null; } - List result = new List(); + var result = new List(); foreach (var typeInfo in assembly.DefinedTypes) { diff --git a/src/Discord.Net.Commands/Info/CommandInfo.cs b/src/Discord.Net.Commands/Info/CommandInfo.cs index 60df2a6a9..ebef80baf 100644 --- a/src/Discord.Net.Commands/Info/CommandInfo.cs +++ b/src/Discord.Net.Commands/Info/CommandInfo.cs @@ -154,7 +154,7 @@ namespace Discord.Commands for (int position = 0; position < Parameters.Count; position++) { var parameter = Parameters[position]; - var argument = args[position]; + object argument = args[position]; var result = await parameter.CheckPreconditionsAsync(context, argument, services).ConfigureAwait(false); if (!result.IsSuccess) return ExecuteResult.FromError(result); @@ -232,7 +232,7 @@ namespace Discord.Commands argCount--; int i = 0; - foreach (var arg in argList) + foreach (object arg in argList) { if (i == argCount) throw new InvalidOperationException("Command was invoked with too many parameters"); diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index 621c2d0e2..1fac66ec5 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -30,7 +30,7 @@ namespace Discord.API protected readonly JsonSerializer _serializer; protected readonly SemaphoreSlim _stateLock; - private readonly RestClientProvider RestClientProvider; + private readonly RestClientProvider _restClientProvider; protected bool _isDisposed; private CancellationTokenSource _loginCancelToken; @@ -48,7 +48,7 @@ namespace Discord.API public DiscordRestApiClient(RestClientProvider restClientProvider, string userAgent, RetryMode defaultRetryMode = RetryMode.AlwaysRetry, JsonSerializer serializer = null) { - RestClientProvider = restClientProvider; + _restClientProvider = restClientProvider; UserAgent = userAgent; DefaultRetryMode = defaultRetryMode; _serializer = serializer ?? new JsonSerializer { DateFormatString = "yyyy-MM-ddTHH:mm:ssZ", ContractResolver = new DiscordContractResolver() }; @@ -60,7 +60,7 @@ namespace Discord.API } internal void SetBaseUrl(string baseUrl) { - RestClient = RestClientProvider(baseUrl); + RestClient = _restClientProvider(baseUrl); RestClient.SetHeader("accept", "*/*"); RestClient.SetHeader("user-agent", UserAgent); RestClient.SetHeader("authorization", GetPrefixedToken(AuthTokenType, AuthToken)); @@ -189,7 +189,7 @@ namespace Discord.API options.BucketId = AuthTokenType == TokenType.User ? ClientBucket.Get(clientBucket).Id : bucketId; options.IsClientBucket = AuthTokenType == TokenType.User; - var json = payload != null ? SerializeJson(payload) : null; + string json = payload != null ? SerializeJson(payload) : null; var request = new JsonRestRequest(RestClient, method, endpoint, json, options); await SendInternalAsync(method, endpoint, request).ConfigureAwait(false); } @@ -233,7 +233,7 @@ namespace Discord.API options.BucketId = AuthTokenType == TokenType.User ? ClientBucket.Get(clientBucket).Id : bucketId; options.IsClientBucket = AuthTokenType == TokenType.User; - var json = payload != null ? SerializeJson(payload) : null; + string json = payload != null ? SerializeJson(payload) : null; var request = new JsonRestRequest(RestClient, method, endpoint, json, options); return DeserializeJson(await SendInternalAsync(method, endpoint, request).ConfigureAwait(false)); } diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index f9458caff..b13ceca1d 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -34,7 +34,7 @@ namespace Discord.WebSocket private int _lastSeq; private ImmutableDictionary _voiceRegions; private Task _heartbeatTask, _guildDownloadTask; - private int _unavailableGuilds; + private int _unavailableGuildCount; private long _lastGuildAvailableTime, _lastMessageTime; private int _nextAudioId; private DateTimeOffset? _statusSince; @@ -60,7 +60,7 @@ namespace Discord.WebSocket internal int? HandlerTimeout { get; private set; } internal new DiscordSocketApiClient ApiClient => base.ApiClient as DiscordSocketApiClient; - public new SocketSelfUser CurrentUser { get { return base.CurrentUser as SocketSelfUser; } private set { base.CurrentUser = value; } } + public new SocketSelfUser CurrentUser { get => base.CurrentUser as SocketSelfUser; private set => base.CurrentUser = value; } public IReadOnlyCollection Guilds => State.Guilds; public IReadOnlyCollection PrivateChannels => State.PrivateChannels; public IReadOnlyCollection DMChannels @@ -474,7 +474,7 @@ namespace Discord.WebSocket AddPrivateChannel(data.PrivateChannels[i], state); _sessionId = data.SessionId; - _unavailableGuilds = unavailableGuilds; + _unavailableGuildCount = unavailableGuilds; CurrentUser = currentUser; State = state; } @@ -537,10 +537,9 @@ namespace Discord.WebSocket if (guild != null) { guild.Update(State, data); - - var unavailableGuilds = _unavailableGuilds; - if (unavailableGuilds != 0) - _unavailableGuilds = unavailableGuilds - 1; + + if (_unavailableGuildCount != 0) + _unavailableGuildCount--; await GuildAvailableAsync(guild).ConfigureAwait(false); if (guild.DownloadedMemberCount >= guild.MemberCount && !guild.DownloaderPromise.IsCompleted) @@ -622,7 +621,7 @@ namespace Discord.WebSocket var before = guild.Clone(); guild.Update(State, data); //This is treated as an extension of GUILD_AVAILABLE - _unavailableGuilds--; + _unavailableGuildCount--; _lastGuildAvailableTime = Environment.TickCount; await GuildAvailableAsync(guild).ConfigureAwait(false); await TimedInvokeAsync(_guildUpdatedEvent, nameof(GuildUpdated), before, guild).ConfigureAwait(false); @@ -646,7 +645,7 @@ namespace Discord.WebSocket if (guild != null) { await GuildUnavailableAsync(guild).ConfigureAwait(false); - _unavailableGuilds++; + _unavailableGuildCount++; } else { @@ -1212,10 +1211,10 @@ namespace Discord.WebSocket var data = (payload as JToken).ToObject(_serializer); if (State.GetChannel(data.ChannelId) is ISocketMessageChannel channel) { - SocketUserMessage cachedMsg = channel.GetCachedMessage(data.MessageId) as SocketUserMessage; + var cachedMsg = channel.GetCachedMessage(data.MessageId) as SocketUserMessage; bool isCached = cachedMsg != null; var user = await channel.GetUserAsync(data.UserId, CacheMode.CacheOnly); - SocketReaction reaction = SocketReaction.Create(data, channel, cachedMsg, Optional.Create(user)); + var reaction = SocketReaction.Create(data, channel, cachedMsg, Optional.Create(user)); var cacheable = new Cacheable(cachedMsg, data.MessageId, isCached, async () => await channel.GetMessageAsync(data.MessageId) as IUserMessage); cachedMsg?.AddReaction(reaction); @@ -1236,10 +1235,10 @@ namespace Discord.WebSocket var data = (payload as JToken).ToObject(_serializer); if (State.GetChannel(data.ChannelId) is ISocketMessageChannel channel) { - SocketUserMessage cachedMsg = channel.GetCachedMessage(data.MessageId) as SocketUserMessage; + var cachedMsg = channel.GetCachedMessage(data.MessageId) as SocketUserMessage; bool isCached = cachedMsg != null; var user = await channel.GetUserAsync(data.UserId, CacheMode.CacheOnly); - SocketReaction reaction = SocketReaction.Create(data, channel, cachedMsg, Optional.Create(user)); + var reaction = SocketReaction.Create(data, channel, cachedMsg, Optional.Create(user)); var cacheable = new Cacheable(cachedMsg, data.MessageId, isCached, async () => await channel.GetMessageAsync(data.MessageId) as IUserMessage); cachedMsg?.RemoveReaction(reaction); @@ -1260,7 +1259,7 @@ namespace Discord.WebSocket var data = (payload as JToken).ToObject(_serializer); if (State.GetChannel(data.ChannelId) is ISocketMessageChannel channel) { - SocketUserMessage cachedMsg = channel.GetCachedMessage(data.MessageId) as SocketUserMessage; + var cachedMsg = channel.GetCachedMessage(data.MessageId) as SocketUserMessage; bool isCached = cachedMsg != null; var cacheable = new Cacheable(cachedMsg, data.MessageId, isCached, async () => await channel.GetMessageAsync(data.MessageId) as IUserMessage); @@ -1289,7 +1288,7 @@ namespace Discord.WebSocket return; } - foreach (var id in data.Ids) + foreach (ulong id in data.Ids) { var msg = SocketChannelHelper.RemoveMessage(channel, this, id); bool isCached = msg != null; @@ -1542,7 +1541,7 @@ namespace Discord.WebSocket await _gatewayLogger.DebugAsync("Heartbeat Started").ConfigureAwait(false); while (!cancelToken.IsCancellationRequested) { - var now = Environment.TickCount; + int now = Environment.TickCount; //Did server respond to our last heartbeat, or are we still receiving messages (long load?) if (_heartbeatTimes.Count != 0 && (now - _lastMessageTime) > intervalMillis) @@ -1589,7 +1588,7 @@ namespace Discord.WebSocket try { await logger.DebugAsync("GuildDownloader Started").ConfigureAwait(false); - while ((_unavailableGuilds != 0) && (Environment.TickCount - _lastGuildAvailableTime < 2000)) + while ((_unavailableGuildCount != 0) && (Environment.TickCount - _lastGuildAvailableTime < 2000)) await Task.Delay(500, cancelToken).ConfigureAwait(false); await logger.DebugAsync("GuildDownloader Stopped").ConfigureAwait(false); } @@ -1750,27 +1749,27 @@ namespace Discord.WebSocket private async Task UnknownGlobalUserAsync(string evnt, ulong userId) { - var details = $"{evnt} User={userId}"; + string details = $"{evnt} User={userId}"; await _gatewayLogger.WarningAsync($"Unknown User ({details}).").ConfigureAwait(false); } private async Task UnknownChannelUserAsync(string evnt, ulong userId, ulong channelId) { - var details = $"{evnt} User={userId} Channel={channelId}"; + string details = $"{evnt} User={userId} Channel={channelId}"; await _gatewayLogger.WarningAsync($"Unknown User ({details}).").ConfigureAwait(false); } private async Task UnknownGuildUserAsync(string evnt, ulong userId, ulong guildId) { - var details = $"{evnt} User={userId} Guild={guildId}"; + string details = $"{evnt} User={userId} Guild={guildId}"; await _gatewayLogger.WarningAsync($"Unknown User ({details}).").ConfigureAwait(false); } private async Task IncompleteGuildUserAsync(string evnt, ulong userId, ulong guildId) { - var details = $"{evnt} User={userId} Guild={guildId}"; + string details = $"{evnt} User={userId} Guild={guildId}"; await _gatewayLogger.DebugAsync($"User has not been downloaded ({details}).").ConfigureAwait(false); } private async Task UnknownChannelAsync(string evnt, ulong channelId) { - var details = $"{evnt} Channel={channelId}"; + string details = $"{evnt} Channel={channelId}"; await _gatewayLogger.WarningAsync($"Unknown Channel ({details}).").ConfigureAwait(false); } private async Task UnknownChannelAsync(string evnt, ulong channelId, ulong guildId) @@ -1780,22 +1779,22 @@ namespace Discord.WebSocket await UnknownChannelAsync(evnt, channelId).ConfigureAwait(false); return; } - var details = $"{evnt} Channel={channelId} Guild={guildId}"; + string details = $"{evnt} Channel={channelId} Guild={guildId}"; await _gatewayLogger.WarningAsync($"Unknown Channel ({details}).").ConfigureAwait(false); } private async Task UnknownRoleAsync(string evnt, ulong roleId, ulong guildId) { - var details = $"{evnt} Role={roleId} Guild={guildId}"; + string details = $"{evnt} Role={roleId} Guild={guildId}"; await _gatewayLogger.WarningAsync($"Unknown Role ({details}).").ConfigureAwait(false); } private async Task UnknownGuildAsync(string evnt, ulong guildId) { - var details = $"{evnt} Guild={guildId}"; + string details = $"{evnt} Guild={guildId}"; await _gatewayLogger.WarningAsync($"Unknown Guild ({details}).").ConfigureAwait(false); } private async Task UnsyncedGuildAsync(string evnt, ulong guildId) { - var details = $"{evnt} Guild={guildId}"; + string details = $"{evnt} Guild={guildId}"; await _gatewayLogger.DebugAsync($"Unsynced Guild ({details}).").ConfigureAwait(false); } From 3b78817c544d5a35fb0c0e7b9f395b9c353da6f6 Mon Sep 17 00:00:00 2001 From: RogueException Date: Thu, 29 Jun 2017 19:45:02 -0300 Subject: [PATCH 232/243] Added int overload to EmbedBuilderExtensions --- src/Discord.Net.Rest/Extensions/EmbedBuilderExtensions.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Discord.Net.Rest/Extensions/EmbedBuilderExtensions.cs b/src/Discord.Net.Rest/Extensions/EmbedBuilderExtensions.cs index 64f96c93f..67acf7c8d 100644 --- a/src/Discord.Net.Rest/Extensions/EmbedBuilderExtensions.cs +++ b/src/Discord.Net.Rest/Extensions/EmbedBuilderExtensions.cs @@ -8,6 +8,9 @@ namespace Discord public static EmbedBuilder WithColor(this EmbedBuilder builder, byte r, byte g, byte b) => builder.WithColor(new Color(r, g, b)); + public static EmbedBuilder WithColor(this EmbedBuilder builder, int r, int g, int b) => + builder.WithColor(new Color(r, g, b)); + public static EmbedBuilder WithColor(this EmbedBuilder builder, float r, float g, float b) => builder.WithColor(new Color(r, g, b)); From ba18179eb845e21cda7f2772e22e7a14f4bf2732 Mon Sep 17 00:00:00 2001 From: RogueException Date: Thu, 29 Jun 2017 19:50:07 -0300 Subject: [PATCH 233/243] Fixed compile error --- src/Discord.Net.Rest/Extensions/EmbedBuilderExtensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net.Rest/Extensions/EmbedBuilderExtensions.cs b/src/Discord.Net.Rest/Extensions/EmbedBuilderExtensions.cs index 67acf7c8d..cee9a136e 100644 --- a/src/Discord.Net.Rest/Extensions/EmbedBuilderExtensions.cs +++ b/src/Discord.Net.Rest/Extensions/EmbedBuilderExtensions.cs @@ -15,9 +15,9 @@ namespace Discord builder.WithColor(new Color(r, g, b)); public static EmbedBuilder WithAuthor(this EmbedBuilder builder, IUser user) => - builder.WithAuthor($"{user.Username}#{user.Discriminator}", user.AvatarUrl); + builder.WithAuthor($"{user.Username}#{user.Discriminator}", user.GetAvatarUrl()); public static EmbedBuilder WithAuthor(this EmbedBuilder builder, IGuildUser user) => - builder.WithAuthor($"{user.Nickname ?? user.Username}#{user.Discriminator}", user.AvatarUrl); + builder.WithAuthor($"{user.Nickname ?? user.Username}#{user.Discriminator}", user.GetAvatarUrl()); } } From 26bc0b300da7198e2a3f62060316214664c69af5 Mon Sep 17 00:00:00 2001 From: RogueException Date: Thu, 29 Jun 2017 20:00:26 -0300 Subject: [PATCH 234/243] Updated version to 1.0 --- Discord.Net.targets | 2 +- src/Discord.Net/Discord.Net.nuspec | 40 +++++++++++++++--------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/Discord.Net.targets b/Discord.Net.targets index 947819898..500d18fd8 100644 --- a/Discord.Net.targets +++ b/Discord.Net.targets @@ -1,7 +1,7 @@ 1.0.0 - rc3 + 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 2a637dbfb..667cc14b9 100644 --- a/src/Discord.Net/Discord.Net.nuspec +++ b/src/Discord.Net/Discord.Net.nuspec @@ -2,39 +2,39 @@ Discord.Net - 1.0.0-rc3$suffix$ + 1.0.0$suffix$ Discord.Net RogueException RogueException - An aynchronous API wrapper for Discord. This metapackage includes all of the optional Discord.Net components. + An asynchronous API wrapper for Discord. This metapackage includes all of the optional Discord.Net components. discord;discordapp https://github.com/RogueException/Discord.Net http://opensource.org/licenses/MIT false - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + From c316b29286f71f9ee11fada3becbf692e90b4084 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Sun, 2 Jul 2017 12:33:03 -0400 Subject: [PATCH 235/243] Bump version to 1.0.1 --- Discord.Net.targets | 2 +- src/Discord.Net/Discord.Net.nuspec | 38 +++++++++++++++--------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/Discord.Net.targets b/Discord.Net.targets index 500d18fd8..6dc4bb140 100644 --- a/Discord.Net.targets +++ b/Discord.Net.targets @@ -1,6 +1,6 @@ - 1.0.0 + 1.0.1 RogueException discord;discordapp diff --git a/src/Discord.Net/Discord.Net.nuspec b/src/Discord.Net/Discord.Net.nuspec index 667cc14b9..19f173c3d 100644 --- a/src/Discord.Net/Discord.Net.nuspec +++ b/src/Discord.Net/Discord.Net.nuspec @@ -2,7 +2,7 @@ Discord.Net - 1.0.0$suffix$ + 1.0.1-build$suffix$ Discord.Net RogueException RogueException @@ -13,28 +13,28 @@ false - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + From 7597cf5baab8f8818b62f0a67ff649b00676430f Mon Sep 17 00:00:00 2001 From: Finite Reality Date: Thu, 6 Jul 2017 00:09:38 +0100 Subject: [PATCH 236/243] Fix CalculateScore throwing on missing parameters (#727) * Fix CalculateScore throwing on missing parameters * Bump to version 1.0.1 --- src/Discord.Net.Commands/CommandService.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index 90e7c8097..6ea2abcf3 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -309,8 +309,11 @@ namespace Discord.Commands if (match.Command.Parameters.Count > 0) { - argValuesScore = parseResult.ArgValues.Sum(x => x.Values.OrderByDescending(y => y.Score).FirstOrDefault().Score) / match.Command.Parameters.Count; - paramValuesScore = parseResult.ParamValues.Sum(x => x.Values.OrderByDescending(y => y.Score).FirstOrDefault().Score) / match.Command.Parameters.Count; + var argValuesSum = parseResult.ArgValues?.Sum(x => x.Values.OrderByDescending(y => y.Score).FirstOrDefault().Score) ?? 0; + var paramValuesSum = parseResult.ParamValues?.Sum(x => x.Values.OrderByDescending(y => y.Score).FirstOrDefault().Score) ?? 0; + + argValuesScore = argValuesSum / match.Command.Parameters.Count; + paramValuesScore = paramValuesSum / match.Command.Parameters.Count; } var totalArgsScore = (argValuesScore + paramValuesScore) / 2; From b6dcc9e8d8b9bb72e66c9446bc6579560d9fbd23 Mon Sep 17 00:00:00 2001 From: Joe4evr Date: Thu, 6 Jul 2017 01:13:49 +0200 Subject: [PATCH 237/243] Add back the case for ParameterPreconditions (#735) --- src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs index b8fbbf462..f284c34f4 100644 --- a/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs +++ b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs @@ -251,6 +251,9 @@ namespace Discord.Commands builder.IsMultiple = true; paramType = paramType.GetElementType(); break; + case ParameterPreconditionAttribute precon: + builder.AddPrecondition(precon); + break; case RemainderAttribute _: if (position != count - 1) throw new InvalidOperationException($"Remainder parameters must be the last parameter in a command. Parameter: {paramInfo.Name} in {paramInfo.Member.DeclaringType.Name}.{paramInfo.Member.Name}"); From d2afb06942868521b1ea5cc193abc2a2a08d69e3 Mon Sep 17 00:00:00 2001 From: Finite Reality Date: Thu, 6 Jul 2017 00:19:09 +0100 Subject: [PATCH 238/243] Make the "cannot be loaded" warning fire correctly (#729) Why am I such a bad programmer? Maybe I'm just bad with git. Maybe I'm just bad in general. Maybe I should resign from programming. --- src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs index f284c34f4..6fae719ee 100644 --- a/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs +++ b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs @@ -24,7 +24,7 @@ namespace Discord.Commands foreach (var typeInfo in assembly.DefinedTypes) { - if (typeInfo.IsPublic) + if (typeInfo.IsPublic || typeInfo.IsNestedPublic) { if (IsValidModuleDefinition(typeInfo) && !typeInfo.IsDefined(typeof(DontAutoLoadAttribute))) @@ -70,7 +70,7 @@ namespace Discord.Commands result[typeInfo.AsType()] = module.Build(service); } - await service._cmdLogger.DebugAsync($"Successfully built and loaded {builtTypes.Count} modules.").ConfigureAwait(false); + await service._cmdLogger.DebugAsync($"Successfully built {builtTypes.Count} modules.").ConfigureAwait(false); return result; } From 8cd99beb622532290ce34ef39ae3000a3992831e Mon Sep 17 00:00:00 2001 From: Joe4evr Date: Thu, 6 Jul 2017 01:23:46 +0200 Subject: [PATCH 239/243] Unify ShardedCommandContext with SocketCommandContext (#739) * Make ShardedCommandContext derive from SocketCommandContext * Explicitly re-implement ICommandContext.Client --- .../Commands/ShardedCommandContext.cs | 22 +++++-------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/src/Discord.Net.WebSocket/Commands/ShardedCommandContext.cs b/src/Discord.Net.WebSocket/Commands/ShardedCommandContext.cs index 627b9b390..a29c9bb70 100644 --- a/src/Discord.Net.WebSocket/Commands/ShardedCommandContext.cs +++ b/src/Discord.Net.WebSocket/Commands/ShardedCommandContext.cs @@ -2,30 +2,20 @@ namespace Discord.Commands { - public class ShardedCommandContext : ICommandContext + public class ShardedCommandContext : SocketCommandContext, ICommandContext { - public DiscordShardedClient Client { get; } - public SocketGuild Guild { get; } - public ISocketMessageChannel Channel { get; } - public SocketUser User { get; } - public SocketUserMessage Message { get; } - - public bool IsPrivate => Channel is IPrivateChannel; + public new DiscordShardedClient Client { get; } public ShardedCommandContext(DiscordShardedClient client, SocketUserMessage msg) + : base(client.GetShard(GetShardId(client, (msg.Channel as SocketGuildChannel)?.Guild)), msg) { Client = client; - Guild = (msg.Channel as SocketGuildChannel)?.Guild; - Channel = msg.Channel; - User = msg.Author; - Message = msg; } + private static int GetShardId(DiscordShardedClient client, IGuild guild) + => guild == null ? 0 : client.GetShardIdFor(guild); + //ICommandContext IDiscordClient ICommandContext.Client => Client; - IGuild ICommandContext.Guild => Guild; - IMessageChannel ICommandContext.Channel => Channel; - IUser ICommandContext.User => User; - IUserMessage ICommandContext.Message => Message; } } From d89804d7c7b4923bc9132fa622de3b843bcf3904 Mon Sep 17 00:00:00 2001 From: Pat Murphy Date: Wed, 5 Jul 2017 16:56:43 -0700 Subject: [PATCH 240/243] Fix potential nullref in embedBuilder value setter (#734) * Fix potential nullref in embedBuilder value setter * Null check on footer iconUrl * Adding checks for the other URL properties * Adding IsNullOrUri extension * Setting StringExtensions as internal --- .../Extensions/StringExtensions.cs | 10 ++++++++++ .../Entities/Messages/EmbedBuilder.cs | 14 +++++++------- 2 files changed, 17 insertions(+), 7 deletions(-) create mode 100644 src/Discord.Net.Core/Extensions/StringExtensions.cs diff --git a/src/Discord.Net.Core/Extensions/StringExtensions.cs b/src/Discord.Net.Core/Extensions/StringExtensions.cs new file mode 100644 index 000000000..c0ebb2626 --- /dev/null +++ b/src/Discord.Net.Core/Extensions/StringExtensions.cs @@ -0,0 +1,10 @@ +using System; + +namespace Discord +{ + internal static class StringExtensions + { + public static bool IsNullOrUri(this string url) => + string.IsNullOrEmpty(url) || Uri.IsWellFormedUriString(url, UriKind.Absolute); + } +} diff --git a/src/Discord.Net.Rest/Entities/Messages/EmbedBuilder.cs b/src/Discord.Net.Rest/Entities/Messages/EmbedBuilder.cs index 2331f6749..7b0285891 100644 --- a/src/Discord.Net.Rest/Entities/Messages/EmbedBuilder.cs +++ b/src/Discord.Net.Rest/Entities/Messages/EmbedBuilder.cs @@ -44,7 +44,7 @@ namespace Discord get => _embed.Url; set { - if (!Uri.IsWellFormedUriString(value, UriKind.Absolute)) throw new ArgumentException("Url must be a well-formed URI", nameof(Url)); + if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(Url)); _embed.Url = value; } } @@ -53,7 +53,7 @@ namespace Discord get => _embed.Thumbnail?.Url; set { - if (!Uri.IsWellFormedUriString(value, UriKind.Absolute)) throw new ArgumentException("Url must be a well-formed URI", nameof(ThumbnailUrl)); + if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(ThumbnailUrl)); _embed.Thumbnail = new EmbedThumbnail(value, null, null, null); } } @@ -62,7 +62,7 @@ namespace Discord get => _embed.Image?.Url; set { - if (!Uri.IsWellFormedUriString(value, UriKind.Absolute)) throw new ArgumentException("Url must be a well-formed URI", nameof(ImageUrl)); + if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(ImageUrl)); _embed.Image = new EmbedImage(value, null, null, null); } } @@ -260,7 +260,7 @@ namespace Discord get => _field.Value; set { - var stringValue = value.ToString(); + var stringValue = value?.ToString(); if (string.IsNullOrEmpty(stringValue)) throw new ArgumentException($"Field value must not be null or empty.", nameof(Value)); if (stringValue.Length > MaxFieldValueLength) throw new ArgumentException($"Field value length must be less than or equal to {MaxFieldValueLength}.", nameof(Value)); _field.Value = stringValue; @@ -313,7 +313,7 @@ namespace Discord get => _author.Url; set { - if (!Uri.IsWellFormedUriString(value, UriKind.Absolute)) throw new ArgumentException("Url must be a well-formed URI", nameof(Url)); + if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(Url)); _author.Url = value; } } @@ -322,7 +322,7 @@ namespace Discord get => _author.IconUrl; set { - if (!Uri.IsWellFormedUriString(value, UriKind.Absolute)) throw new ArgumentException("Url must be a well-formed URI", nameof(IconUrl)); + if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(IconUrl)); _author.IconUrl = value; } } @@ -372,7 +372,7 @@ namespace Discord get => _footer.IconUrl; set { - if (!Uri.IsWellFormedUriString(value, UriKind.Absolute)) throw new ArgumentException("Url must be a well-formed URI", nameof(IconUrl)); + if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(IconUrl)); _footer.IconUrl = value; } } From b35fbac017e496c885854eaa54f73a4c4092b561 Mon Sep 17 00:00:00 2001 From: RogueException Date: Wed, 5 Jul 2017 21:30:25 -0300 Subject: [PATCH 241/243] Removed version from README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c5ed907ee..2b58d4579 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Discord.Net v1.0.0-rc +# Discord.Net [![NuGet](https://img.shields.io/nuget/vpre/Discord.Net.svg?maxAge=2592000?style=plastic)](https://www.nuget.org/packages/Discord.Net) [![MyGet](https://img.shields.io/myget/discord-net/vpre/Discord.Net.svg)](https://www.myget.org/feed/Packages/discord-net) [![Build status](https://ci.appveyor.com/api/projects/status/5sb7n8a09w9clute/branch/dev?svg=true)](https://ci.appveyor.com/project/RogueException/discord-net/branch/dev) From d27657d193fdffe5c3b7f687ba2372fb57620334 Mon Sep 17 00:00:00 2001 From: RogueException Date: Wed, 5 Jul 2017 21:31:59 -0300 Subject: [PATCH 242/243] Removed hardcoded suffix from nuspec --- appveyor.yml | 2 +- src/Discord.Net/Discord.Net.nuspec | 38 +++++++++++++++--------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 3bf70c09c..25b4dc9bd 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -34,7 +34,7 @@ after_build: if ($Env:APPVEYOR_REPO_TAG -eq "true") { nuget pack src\Discord.Net\Discord.Net.nuspec -OutputDirectory "artifacts" -properties suffix="" } else { - nuget pack src\Discord.Net\Discord.Net.nuspec -OutputDirectory "artifacts" -properties suffix="-$Env:BUILD" + nuget pack src\Discord.Net\Discord.Net.nuspec -OutputDirectory "artifacts" -properties suffix="build-$Env:BUILD" } - ps: Get-ChildItem artifacts\*.nupkg | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name } diff --git a/src/Discord.Net/Discord.Net.nuspec b/src/Discord.Net/Discord.Net.nuspec index 19f173c3d..864083599 100644 --- a/src/Discord.Net/Discord.Net.nuspec +++ b/src/Discord.Net/Discord.Net.nuspec @@ -2,7 +2,7 @@ Discord.Net - 1.0.1-build$suffix$ + 1.0.1$suffix$ Discord.Net RogueException RogueException @@ -13,28 +13,28 @@ false - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + From ff10f17cba6b25642ac3543a47c32a3d513baaac Mon Sep 17 00:00:00 2001 From: RogueException Date: Wed, 5 Jul 2017 21:35:38 -0300 Subject: [PATCH 243/243] Proper versioning is hard --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 25b4dc9bd..d94e2ad68 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -34,7 +34,7 @@ after_build: if ($Env:APPVEYOR_REPO_TAG -eq "true") { nuget pack src\Discord.Net\Discord.Net.nuspec -OutputDirectory "artifacts" -properties suffix="" } else { - nuget pack src\Discord.Net\Discord.Net.nuspec -OutputDirectory "artifacts" -properties suffix="build-$Env:BUILD" + nuget pack src\Discord.Net\Discord.Net.nuspec -OutputDirectory "artifacts" -properties suffix="-build-$Env:BUILD" } - ps: Get-ChildItem artifacts\*.nupkg | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name }