From 0f2af47ec842da517b05b2fb9aefbc62a5674466 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Sun, 7 Jan 2018 15:55:01 -0500 Subject: [PATCH 01/48] Fix typo in API model for GameAssets This would cause the large asset's Image ID (and in turn, Image URL) and hover text to be reversed. --- src/Discord.Net.Rest/API/Common/GameAssets.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Discord.Net.Rest/API/Common/GameAssets.cs b/src/Discord.Net.Rest/API/Common/GameAssets.cs index b5928a8abd..94a5407692 100644 --- a/src/Discord.Net.Rest/API/Common/GameAssets.cs +++ b/src/Discord.Net.Rest/API/Common/GameAssets.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; namespace Discord.API { @@ -8,9 +8,9 @@ internal class GameAssets public Optional SmallText { get; set; } [JsonProperty("small_image")] public Optional SmallImage { get; set; } - [JsonProperty("large_image")] - public Optional LargeText { get; set; } [JsonProperty("large_text")] + public Optional LargeText { get; set; } + [JsonProperty("large_image")] public Optional LargeImage { get; set; } } -} \ No newline at end of file +} From 42c879c37cdac8ec8358f80f08105f2a21255b31 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Mon, 8 Jan 2018 02:29:24 -0500 Subject: [PATCH 02/48] Add 'html' to EmbedType enum This resolves #762. This change adds an 'html' variant to the EmbedType enum. This change also adds an 'Unknown' variant to the EmbedType enum (at position -1); this will be used in a later commit to future-proof the EmbedType enum from any further variants Discord may add. --- src/Discord.Net.Core/Entities/Messages/EmbedType.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedType.cs b/src/Discord.Net.Core/Entities/Messages/EmbedType.cs index 469e968a5f..5bb2653e21 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedType.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedType.cs @@ -1,13 +1,15 @@ -namespace Discord +namespace Discord { public enum EmbedType { + Unknown = -1, Rich, Link, Video, Image, Gifv, Article, - Tweet + Tweet, + Html, } } From 97397f36177ab03835f31f3e051160411ee6df7c Mon Sep 17 00:00:00 2001 From: Christopher F Date: Mon, 8 Jan 2018 12:56:31 -0500 Subject: [PATCH 03/48] Apply consistency to attributes in the commands extension (#928) * Apply consistency to attributes in the commands extension This resolves #527. Not sure if I missed any, putting this up for review. * Allow preconditions to be used multiple times (for use with groups) --- src/Discord.Net.Commands/Attributes/AliasAttribute.cs | 2 +- src/Discord.Net.Commands/Attributes/CommandAttribute.cs | 4 ++-- src/Discord.Net.Commands/Attributes/DontAutoLoadAttribute.cs | 4 ++-- src/Discord.Net.Commands/Attributes/DontInjectAttribute.cs | 2 +- src/Discord.Net.Commands/Attributes/GroupAttribute.cs | 4 ++-- src/Discord.Net.Commands/Attributes/NameAttribute.cs | 2 +- .../Attributes/OverrideTypeReaderAttribute.cs | 4 ++-- .../Attributes/ParameterPreconditionAttribute.cs | 3 +-- .../Attributes/Preconditions/RequireContextAttribute.cs | 4 ++-- .../Attributes/Preconditions/RequireNsfwAttribute.cs | 4 ++-- .../Attributes/Preconditions/RequireOwnerAttribute.cs | 5 ++--- src/Discord.Net.Commands/Attributes/PriorityAttribute.cs | 2 +- src/Discord.Net.Commands/Attributes/RemainderAttribute.cs | 4 ++-- src/Discord.Net.Commands/Attributes/RemarksAttribute.cs | 4 ++-- src/Discord.Net.Commands/Attributes/SummaryAttribute.cs | 4 ++-- 15 files changed, 25 insertions(+), 27 deletions(-) diff --git a/src/Discord.Net.Commands/Attributes/AliasAttribute.cs b/src/Discord.Net.Commands/Attributes/AliasAttribute.cs index 6e115bd605..6cd0abbb71 100644 --- a/src/Discord.Net.Commands/Attributes/AliasAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/AliasAttribute.cs @@ -3,7 +3,7 @@ namespace Discord.Commands { /// Provides aliases for a command. - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] public class AliasAttribute : Attribute { /// The aliases which have been defined for the command. diff --git a/src/Discord.Net.Commands/Attributes/CommandAttribute.cs b/src/Discord.Net.Commands/Attributes/CommandAttribute.cs index 5ae6092ebb..5f8e9ceafa 100644 --- a/src/Discord.Net.Commands/Attributes/CommandAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/CommandAttribute.cs @@ -1,8 +1,8 @@ -using System; +using System; namespace Discord.Commands { - [AttributeUsage(AttributeTargets.Method)] + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] public class CommandAttribute : Attribute { public string Text { get; } diff --git a/src/Discord.Net.Commands/Attributes/DontAutoLoadAttribute.cs b/src/Discord.Net.Commands/Attributes/DontAutoLoadAttribute.cs index d6a1c646e1..cc23a6d151 100644 --- a/src/Discord.Net.Commands/Attributes/DontAutoLoadAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/DontAutoLoadAttribute.cs @@ -1,8 +1,8 @@ -using System; +using System; namespace Discord.Commands { - [AttributeUsage(AttributeTargets.Class)] + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] public class DontAutoLoadAttribute : Attribute { } diff --git a/src/Discord.Net.Commands/Attributes/DontInjectAttribute.cs b/src/Discord.Net.Commands/Attributes/DontInjectAttribute.cs index bd966e1292..c982d93a1e 100644 --- a/src/Discord.Net.Commands/Attributes/DontInjectAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/DontInjectAttribute.cs @@ -2,7 +2,7 @@ namespace Discord.Commands { - [AttributeUsage(AttributeTargets.Property)] + [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] public class DontInjectAttribute : Attribute { } diff --git a/src/Discord.Net.Commands/Attributes/GroupAttribute.cs b/src/Discord.Net.Commands/Attributes/GroupAttribute.cs index 105d256ec1..b1760d1495 100644 --- a/src/Discord.Net.Commands/Attributes/GroupAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/GroupAttribute.cs @@ -1,8 +1,8 @@ -using System; +using System; namespace Discord.Commands { - [AttributeUsage(AttributeTargets.Class)] + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] public class GroupAttribute : Attribute { public string Prefix { get; } diff --git a/src/Discord.Net.Commands/Attributes/NameAttribute.cs b/src/Discord.Net.Commands/Attributes/NameAttribute.cs index 0a5156fee7..4a4b2bfedb 100644 --- a/src/Discord.Net.Commands/Attributes/NameAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/NameAttribute.cs @@ -3,7 +3,7 @@ namespace Discord.Commands { // Override public name of command/module - [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Parameter)] + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)] public class NameAttribute : Attribute { public string Text { get; } diff --git a/src/Discord.Net.Commands/Attributes/OverrideTypeReaderAttribute.cs b/src/Discord.Net.Commands/Attributes/OverrideTypeReaderAttribute.cs index 37f685c95b..44ab6d214b 100644 --- a/src/Discord.Net.Commands/Attributes/OverrideTypeReaderAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/OverrideTypeReaderAttribute.cs @@ -4,7 +4,7 @@ namespace Discord.Commands { - [AttributeUsage(AttributeTargets.Parameter)] + [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)] public class OverrideTypeReaderAttribute : Attribute { private static readonly TypeInfo _typeReaderTypeInfo = typeof(TypeReader).GetTypeInfo(); @@ -19,4 +19,4 @@ public OverrideTypeReaderAttribute(Type overridenTypeReader) TypeReader = overridenTypeReader; } } -} \ No newline at end of file +} diff --git a/src/Discord.Net.Commands/Attributes/ParameterPreconditionAttribute.cs b/src/Discord.Net.Commands/Attributes/ParameterPreconditionAttribute.cs index 2098225837..3c5e8cf92a 100644 --- a/src/Discord.Net.Commands/Attributes/ParameterPreconditionAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/ParameterPreconditionAttribute.cs @@ -1,6 +1,5 @@ using System; using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; namespace Discord.Commands { @@ -9,4 +8,4 @@ public abstract class ParameterPreconditionAttribute : Attribute { public abstract Task CheckPermissionsAsync(ICommandContext context, ParameterInfo parameter, object value, IServiceProvider services); } -} \ No newline at end of file +} diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs index 5fa0fb1b9f..90af035e49 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; @@ -15,7 +15,7 @@ public enum ContextType /// /// Require that the command be invoked in a specified context. /// - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] public class RequireContextAttribute : PreconditionAttribute { public ContextType Contexts { get; } diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireNsfwAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireNsfwAttribute.cs index c8e3bfa82f..273c764bd2 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireNsfwAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireNsfwAttribute.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading.Tasks; namespace Discord.Commands @@ -6,7 +6,7 @@ namespace Discord.Commands /// /// Require that the command is invoked in a channel marked NSFW /// - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] public class RequireNsfwAttribute : PreconditionAttribute { public override Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs index e370aeec49..7a8a009be2 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs @@ -1,7 +1,6 @@ -#pragma warning disable CS0618 +#pragma warning disable CS0618 using System; using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; namespace Discord.Commands { @@ -9,7 +8,7 @@ namespace Discord.Commands /// Require that the command is invoked by the owner of the bot. /// /// This precondition will only work if the bot is a bot account. - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] public class RequireOwnerAttribute : PreconditionAttribute { public override async Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) diff --git a/src/Discord.Net.Commands/Attributes/PriorityAttribute.cs b/src/Discord.Net.Commands/Attributes/PriorityAttribute.cs index 5120bb7d16..353e96e418 100644 --- a/src/Discord.Net.Commands/Attributes/PriorityAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/PriorityAttribute.cs @@ -3,7 +3,7 @@ namespace Discord.Commands { /// Sets priority of commands - [AttributeUsage(AttributeTargets.Method)] + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] public class PriorityAttribute : Attribute { /// The priority which has been set for the command diff --git a/src/Discord.Net.Commands/Attributes/RemainderAttribute.cs b/src/Discord.Net.Commands/Attributes/RemainderAttribute.cs index 4aa16bebbd..56938f1676 100644 --- a/src/Discord.Net.Commands/Attributes/RemainderAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/RemainderAttribute.cs @@ -1,8 +1,8 @@ -using System; +using System; namespace Discord.Commands { - [AttributeUsage(AttributeTargets.Parameter)] + [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)] public class RemainderAttribute : Attribute { } diff --git a/src/Discord.Net.Commands/Attributes/RemarksAttribute.cs b/src/Discord.Net.Commands/Attributes/RemarksAttribute.cs index 44db18a796..c11f790a79 100644 --- a/src/Discord.Net.Commands/Attributes/RemarksAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/RemarksAttribute.cs @@ -1,9 +1,9 @@ -using System; +using System; namespace Discord.Commands { // Extension of the Cosmetic Summary, for Groups, Commands, and Parameters - [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)] + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = true)] public class RemarksAttribute : Attribute { public string Text { get; } diff --git a/src/Discord.Net.Commands/Attributes/SummaryAttribute.cs b/src/Discord.Net.Commands/Attributes/SummaryAttribute.cs index 46d52f3d96..641163408d 100644 --- a/src/Discord.Net.Commands/Attributes/SummaryAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/SummaryAttribute.cs @@ -1,9 +1,9 @@ -using System; +using System; namespace Discord.Commands { // Cosmetic Summary, for Groups and Commands - [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Parameter)] + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)] public class SummaryAttribute : Attribute { public string Text { get; } From b5e75486512491f5adcc10709c5417d3410c0113 Mon Sep 17 00:00:00 2001 From: Joe4evr Date: Thu, 11 Jan 2018 03:31:43 +0100 Subject: [PATCH 04/48] Comparers (#929) * Add entity equality comparers * Fix namespace #whoops * Add Message comparer. * Add comment explaining the specialized implementation * Remove specialized implementation, as per feedback --- src/Discord.Net.Core/Utils/Comparers.cs | 45 +++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 src/Discord.Net.Core/Utils/Comparers.cs diff --git a/src/Discord.Net.Core/Utils/Comparers.cs b/src/Discord.Net.Core/Utils/Comparers.cs new file mode 100644 index 0000000000..d7641e8972 --- /dev/null +++ b/src/Discord.Net.Core/Utils/Comparers.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; + +namespace Discord +{ + public static class DiscordComparers + { + // TODO: simplify with '??=' slated for C# 8.0 + public static IEqualityComparer UserComparer => _userComparer ?? (_userComparer = new EntityEqualityComparer()); + public static IEqualityComparer GuildComparer => _guildComparer ?? (_guildComparer = new EntityEqualityComparer()); + public static IEqualityComparer ChannelComparer => _channelComparer ?? (_channelComparer = new EntityEqualityComparer()); + public static IEqualityComparer RoleComparer => _roleComparer ?? (_roleComparer = new EntityEqualityComparer()); + public static IEqualityComparer MessageComparer => _messageComparer ?? (_messageComparer = new EntityEqualityComparer()); + + private static IEqualityComparer _userComparer; + private static IEqualityComparer _guildComparer; + private static IEqualityComparer _channelComparer; + private static IEqualityComparer _roleComparer; + private static IEqualityComparer _messageComparer; + + private sealed class EntityEqualityComparer : EqualityComparer + where TEntity : IEntity + where TId : IEquatable + { + public override bool Equals(TEntity x, TEntity y) + { + bool xNull = x == null; + bool yNull = y == null; + + if (xNull && yNull) + return true; + + if (xNull ^ yNull) + return false; + + return x.Id.Equals(y.Id); + } + + public override int GetHashCode(TEntity obj) + { + return obj?.Id.GetHashCode() ?? 0; + } + } + } +} From 87124d3e39b6aaf85ccb74724acf27d65f847cfe Mon Sep 17 00:00:00 2001 From: ObsidianMinor Date: Fri, 12 Jan 2018 16:24:01 -0600 Subject: [PATCH 05/48] Simplify Flatten extension (#933) --- .../Extensions/AsyncEnumerableExtensions.cs | 43 +------------------ 1 file changed, 2 insertions(+), 41 deletions(-) diff --git a/src/Discord.Net.Core/Extensions/AsyncEnumerableExtensions.cs b/src/Discord.Net.Core/Extensions/AsyncEnumerableExtensions.cs index 345154f1d9..dd16d2943c 100644 --- a/src/Discord.Net.Core/Extensions/AsyncEnumerableExtensions.cs +++ b/src/Discord.Net.Core/Extensions/AsyncEnumerableExtensions.cs @@ -1,6 +1,5 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; -using System.Threading; using System.Threading.Tasks; namespace Discord @@ -20,45 +19,7 @@ public static async Task> FlattenAsync(this IAsyncEnumerable Flatten(this IAsyncEnumerable> source) { - return new PagedCollectionEnumerator(source); - } - - internal class PagedCollectionEnumerator : IAsyncEnumerator, IAsyncEnumerable - { - readonly IAsyncEnumerator> _source; - IEnumerator _enumerator; - - public IAsyncEnumerator GetEnumerator() => this; - - internal PagedCollectionEnumerator(IAsyncEnumerable> source) - { - _source = source.GetEnumerator(); - } - - public T Current => _enumerator.Current; - - public void Dispose() - { - _enumerator?.Dispose(); - _source.Dispose(); - } - - public async Task MoveNext(CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - if(!_enumerator?.MoveNext() ?? true) - { - if (!await _source.MoveNext(cancellationToken).ConfigureAwait(false)) - return false; - - _enumerator?.Dispose(); - _enumerator = _source.Current.GetEnumerator(); - return _enumerator.MoveNext(); - } - - return true; - } + return source.SelectMany(enumerable => enumerable.ToAsyncEnumerable()); } } } From f69ef2a8cad3654c36684bee8f09eb246cce4e65 Mon Sep 17 00:00:00 2001 From: Joe4evr Date: Sun, 14 Jan 2018 04:54:47 +0100 Subject: [PATCH 06/48] Add API Analyzer Assembly (#906) * Start on API analyzers * Finish GuildAccessAnalyzer * Update build script (will this do?) * Correct slashes * Extrapolate DerivesFromModuleBase() to an extension method * Quick refactoring * Add doc file --- Discord.Net.sln | 17 +- appveyor.yml | 1 + .../Discord.Net.Analyzers.csproj | 15 + .../GuildAccessAnalyzer.cs | 70 +++++ src/Discord.Net.Analyzers/SymbolExtensions.cs | 21 ++ src/Discord.Net.Analyzers/docs/DNET0001.md | 30 ++ .../Extensions/AppDomainPolyfill.cs | 30 ++ .../AnalyzerTests/GuildAccessTests.cs | 111 +++++++ .../Helpers/CodeFixVerifier.Helper.cs | 85 ++++++ .../AnalyzerTests/Helpers/DiagnosticResult.cs | 87 ++++++ .../Helpers/DiagnosticVerifier.Helper.cs | 207 +++++++++++++ .../Verifiers/CodeFixVerifier.cs | 129 +++++++++ .../Verifiers/DiagnosticVerifier.cs | 271 ++++++++++++++++++ .../Discord.Net.Tests.csproj | 1 + 14 files changed, 1074 insertions(+), 1 deletion(-) create mode 100644 src/Discord.Net.Analyzers/Discord.Net.Analyzers.csproj create mode 100644 src/Discord.Net.Analyzers/GuildAccessAnalyzer.cs create mode 100644 src/Discord.Net.Analyzers/SymbolExtensions.cs create mode 100644 src/Discord.Net.Analyzers/docs/DNET0001.md create mode 100644 test/Discord.Net.Tests/AnalyzerTests/Extensions/AppDomainPolyfill.cs create mode 100644 test/Discord.Net.Tests/AnalyzerTests/GuildAccessTests.cs create mode 100644 test/Discord.Net.Tests/AnalyzerTests/Helpers/CodeFixVerifier.Helper.cs create mode 100644 test/Discord.Net.Tests/AnalyzerTests/Helpers/DiagnosticResult.cs create mode 100644 test/Discord.Net.Tests/AnalyzerTests/Helpers/DiagnosticVerifier.Helper.cs create mode 100644 test/Discord.Net.Tests/AnalyzerTests/Verifiers/CodeFixVerifier.cs create mode 100644 test/Discord.Net.Tests/AnalyzerTests/Verifiers/DiagnosticVerifier.cs diff --git a/Discord.Net.sln b/Discord.Net.sln index cac6c9064c..daf902b96f 100644 --- a/Discord.Net.sln +++ b/Discord.Net.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.27004.2009 +VisualStudioVersion = 15.0.27130.0 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 @@ -22,6 +22,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Tests", "test\D EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Webhook", "src\Discord.Net.Webhook\Discord.Net.Webhook.csproj", "{9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discord.Net.Analyzers", "src\Discord.Net.Analyzers\Discord.Net.Analyzers.csproj", "{BBA8E7FB-C834-40DC-822F-B112CB7F0140}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -116,6 +118,18 @@ Global {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 + {BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Debug|x64.ActiveCfg = Debug|Any CPU + {BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Debug|x64.Build.0 = Debug|Any CPU + {BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Debug|x86.ActiveCfg = Debug|Any CPU + {BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Debug|x86.Build.0 = Debug|Any CPU + {BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Release|Any CPU.Build.0 = Release|Any CPU + {BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Release|x64.ActiveCfg = Release|Any CPU + {BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Release|x64.Build.0 = Release|Any CPU + {BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Release|x86.ActiveCfg = Release|Any CPU + {BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -126,6 +140,7 @@ Global {688FD1D8-7F01-4539-B2E9-F473C5D699C7} = {288C363D-A636-4EAE-9AC1-4698B641B26E} {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7} = {B0657AAE-DCC5-4FBF-8E5D-1FB578CF3012} {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30} = {CC3D4B1C-9DE0-448B-8AE7-F3F1F3EC5C3A} + {BBA8E7FB-C834-40DC-822F-B112CB7F0140} = {CC3D4B1C-9DE0-448B-8AE7-F3F1F3EC5C3A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D2404771-EEC8-45F2-9D71-F3373F6C1495} diff --git a/appveyor.yml b/appveyor.yml index 393485fee4..54b9a12517 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -29,6 +29,7 @@ after_build: - 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: dotnet pack "src\Discord.Net.Analyzers\Discord.Net.Analyzers.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="" diff --git a/src/Discord.Net.Analyzers/Discord.Net.Analyzers.csproj b/src/Discord.Net.Analyzers/Discord.Net.Analyzers.csproj new file mode 100644 index 0000000000..8ab398ff56 --- /dev/null +++ b/src/Discord.Net.Analyzers/Discord.Net.Analyzers.csproj @@ -0,0 +1,15 @@ + + + + Discord.Net.Analyzers + Discord.Analyzers + A Discord.Net extension adding support for design-time analysis of the API usage. + netstandard1.3 + + + + + + + + diff --git a/src/Discord.Net.Analyzers/GuildAccessAnalyzer.cs b/src/Discord.Net.Analyzers/GuildAccessAnalyzer.cs new file mode 100644 index 0000000000..0760d019f3 --- /dev/null +++ b/src/Discord.Net.Analyzers/GuildAccessAnalyzer.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Discord.Commands; + +namespace Discord.Analyzers +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public sealed class GuildAccessAnalyzer : DiagnosticAnalyzer + { + private const string DiagnosticId = "DNET0001"; + private const string Title = "Limit command to Guild contexts."; + private const string MessageFormat = "Command method '{0}' is accessing 'Context.Guild' but is not restricted to Guild contexts."; + private const string Description = "Accessing 'Context.Guild' in a command without limiting the command to run only in guilds."; + private const string Category = "API Usage"; + + private static DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, isEnabledByDefault: true, description: Description); + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); + + public override void Initialize(AnalysisContext context) + { + context.RegisterSyntaxNodeAction(AnalyzeMemberAccess, SyntaxKind.SimpleMemberAccessExpression); + } + + private static void AnalyzeMemberAccess(SyntaxNodeAnalysisContext context) + { + // Bail out if the accessed member isn't named 'Guild' + var memberAccessSymbol = context.SemanticModel.GetSymbolInfo(context.Node).Symbol; + if (memberAccessSymbol.Name != "Guild") + return; + + // Bail out if it happens to be 'ContextType.Guild' in the '[RequireContext]' argument + if (context.Node.Parent is AttributeArgumentSyntax) + return; + + // Bail out if the containing class doesn't derive from 'ModuleBase' + var typeNode = context.Node.FirstAncestorOrSelf(); + var typeSymbol = context.SemanticModel.GetDeclaredSymbol(typeNode); + if (!typeSymbol.DerivesFromModuleBase()) + return; + + // Bail out if the containing method isn't marked with '[Command]' + var methodNode = context.Node.FirstAncestorOrSelf(); + var methodSymbol = context.SemanticModel.GetDeclaredSymbol(methodNode); + var methodAttributes = methodSymbol.GetAttributes(); + if (!methodAttributes.Any(a => a.AttributeClass.Name == nameof(CommandAttribute))) + return; + + // Is the '[RequireContext]' attribute not applied to either the + // method or the class, or its argument isn't 'ContextType.Guild'? + var ctxAttribute = methodAttributes.SingleOrDefault(_attributeDataPredicate) + ?? typeSymbol.GetAttributes().SingleOrDefault(_attributeDataPredicate); + + if (ctxAttribute == null || ctxAttribute.ConstructorArguments.Any(arg => !arg.Value.Equals((int)ContextType.Guild))) + { + // Report the diagnostic + var diagnostic = Diagnostic.Create(Rule, context.Node.GetLocation(), methodSymbol.Name); + context.ReportDiagnostic(diagnostic); + } + } + + private static readonly Func _attributeDataPredicate = + (a => a.AttributeClass.Name == nameof(RequireContextAttribute)); + } +} diff --git a/src/Discord.Net.Analyzers/SymbolExtensions.cs b/src/Discord.Net.Analyzers/SymbolExtensions.cs new file mode 100644 index 0000000000..680de66b5a --- /dev/null +++ b/src/Discord.Net.Analyzers/SymbolExtensions.cs @@ -0,0 +1,21 @@ +using System; +using Microsoft.CodeAnalysis; +using Discord.Commands; + +namespace Discord.Analyzers +{ + internal static class SymbolExtensions + { + private static readonly string _moduleBaseName = typeof(ModuleBase<>).Name; + + public static bool DerivesFromModuleBase(this ITypeSymbol symbol) + { + for (var bType = symbol.BaseType; bType != null; bType = bType.BaseType) + { + if (bType.MetadataName == _moduleBaseName) + return true; + } + return false; + } + } +} diff --git a/src/Discord.Net.Analyzers/docs/DNET0001.md b/src/Discord.Net.Analyzers/docs/DNET0001.md new file mode 100644 index 0000000000..0c1b8098ff --- /dev/null +++ b/src/Discord.Net.Analyzers/docs/DNET0001.md @@ -0,0 +1,30 @@ +# DNET0001 + + + + + + + + + + + + + + +
TypeNameGuildAccessAnalyzer
CheckIdDNET0001
CategoryAPI Usage
+ +## Cause + +A method identified as a command is accessing `Context.Guild` without the requisite precondition. + +## Rule description + +The value of `Context.Guild` is `null` if a command is invoked in a DM channel. Attempting to access +guild properties in such a case will result in a `NullReferenceException` at runtime. +This exception is entirely avoidable by using the library's provided preconditions. + +## How to fix violations + +Add the precondition `[RequireContext(ContextType.Guild)]` to the command or module class. diff --git a/test/Discord.Net.Tests/AnalyzerTests/Extensions/AppDomainPolyfill.cs b/test/Discord.Net.Tests/AnalyzerTests/Extensions/AppDomainPolyfill.cs new file mode 100644 index 0000000000..729bc385c0 --- /dev/null +++ b/test/Discord.Net.Tests/AnalyzerTests/Extensions/AppDomainPolyfill.cs @@ -0,0 +1,30 @@ +using System.Linq; +using System.Reflection; +using Microsoft.DotNet.PlatformAbstractions; +using Microsoft.Extensions.DependencyModel; + +namespace System +{ + /// Polyfill of the AppDomain class from full framework. + internal class AppDomain + { + public static AppDomain CurrentDomain { get; private set; } + + private AppDomain() + { + } + + static AppDomain() + { + CurrentDomain = new AppDomain(); + } + + public Assembly[] GetAssemblies() + { + var rid = RuntimeEnvironment.GetRuntimeIdentifier(); + var ass = DependencyContext.Default.GetRuntimeAssemblyNames(rid); + + return ass.Select(xan => Assembly.Load(xan)).ToArray(); + } + } +} \ No newline at end of file diff --git a/test/Discord.Net.Tests/AnalyzerTests/GuildAccessTests.cs b/test/Discord.Net.Tests/AnalyzerTests/GuildAccessTests.cs new file mode 100644 index 0000000000..073cc1de76 --- /dev/null +++ b/test/Discord.Net.Tests/AnalyzerTests/GuildAccessTests.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Discord.Analyzers; +using TestHelper; +using Xunit; + +namespace Discord +{ + public partial class AnalyserTests + { + public class GuildAccessTests : DiagnosticVerifier + { + [Fact] + public void VerifyDiagnosticWhenLackingRequireContext() + { + string source = @"using System; +using System.Threading.Tasks; +using Discord.Commands; + +namespace Test +{ + public class TestModule : ModuleBase + { + [Command(""test"")] + public Task TestCmd() => ReplyAsync(Context.Guild.Name); + } +}"; + var expected = new DiagnosticResult() + { + Id = "DNET0001", + Locations = new[] { new DiagnosticResultLocation("Test0.cs", line: 10, column: 45) }, + Message = "Command method 'TestCmd' is accessing 'Context.Guild' but is not restricted to Guild contexts.", + Severity = DiagnosticSeverity.Warning + }; + VerifyCSharpDiagnostic(source, expected); + } + + [Fact] + public void VerifyDiagnosticWhenWrongRequireContext() + { + string source = @"using System; +using System.Threading.Tasks; +using Discord.Commands; + +namespace Test +{ + public class TestModule : ModuleBase + { + [Command(""test""), RequireContext(ContextType.Group)] + public Task TestCmd() => ReplyAsync(Context.Guild.Name); + } +}"; + var expected = new DiagnosticResult() + { + Id = "DNET0001", + Locations = new[] { new DiagnosticResultLocation("Test0.cs", line: 10, column: 45) }, + Message = "Command method 'TestCmd' is accessing 'Context.Guild' but is not restricted to Guild contexts.", + Severity = DiagnosticSeverity.Warning + }; + VerifyCSharpDiagnostic(source, expected); + } + + [Fact] + public void VerifyNoDiagnosticWhenRequireContextOnMethod() + { + string source = @"using System; +using System.Threading.Tasks; +using Discord.Commands; + +namespace Test +{ + public class TestModule : ModuleBase + { + [Command(""test""), RequireContext(ContextType.Guild)] + public Task TestCmd() => ReplyAsync(Context.Guild.Name); + } +}"; + + VerifyCSharpDiagnostic(source, Array.Empty()); + } + + [Fact] + public void VerifyNoDiagnosticWhenRequireContextOnClass() + { + string source = @"using System; +using System.Threading.Tasks; +using Discord.Commands; + +namespace Test +{ + [RequireContext(ContextType.Guild)] + public class TestModule : ModuleBase + { + [Command(""test"")] + public Task TestCmd() => ReplyAsync(Context.Guild.Name); + } +}"; + + VerifyCSharpDiagnostic(source, Array.Empty()); + } + + protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer() + => new GuildAccessAnalyzer(); + } + } +} diff --git a/test/Discord.Net.Tests/AnalyzerTests/Helpers/CodeFixVerifier.Helper.cs b/test/Discord.Net.Tests/AnalyzerTests/Helpers/CodeFixVerifier.Helper.cs new file mode 100644 index 0000000000..0f73d06437 --- /dev/null +++ b/test/Discord.Net.Tests/AnalyzerTests/Helpers/CodeFixVerifier.Helper.cs @@ -0,0 +1,85 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.Formatting; +using Microsoft.CodeAnalysis.Simplification; +using System.Collections.Generic; +using System.Linq; +using System.Threading; + +namespace TestHelper +{ + /// + /// Diagnostic Producer class with extra methods dealing with applying codefixes + /// All methods are static + /// + public abstract partial class CodeFixVerifier : DiagnosticVerifier + { + /// + /// Apply the inputted CodeAction to the inputted document. + /// Meant to be used to apply codefixes. + /// + /// The Document to apply the fix on + /// A CodeAction that will be applied to the Document. + /// A Document with the changes from the CodeAction + private static Document ApplyFix(Document document, CodeAction codeAction) + { + var operations = codeAction.GetOperationsAsync(CancellationToken.None).Result; + var solution = operations.OfType().Single().ChangedSolution; + return solution.GetDocument(document.Id); + } + + /// + /// Compare two collections of Diagnostics,and return a list of any new diagnostics that appear only in the second collection. + /// Note: Considers Diagnostics to be the same if they have the same Ids. In the case of multiple diagnostics with the same Id in a row, + /// this method may not necessarily return the new one. + /// + /// The Diagnostics that existed in the code before the CodeFix was applied + /// The Diagnostics that exist in the code after the CodeFix was applied + /// A list of Diagnostics that only surfaced in the code after the CodeFix was applied + private static IEnumerable GetNewDiagnostics(IEnumerable diagnostics, IEnumerable newDiagnostics) + { + var oldArray = diagnostics.OrderBy(d => d.Location.SourceSpan.Start).ToArray(); + var newArray = newDiagnostics.OrderBy(d => d.Location.SourceSpan.Start).ToArray(); + + int oldIndex = 0; + int newIndex = 0; + + while (newIndex < newArray.Length) + { + if (oldIndex < oldArray.Length && oldArray[oldIndex].Id == newArray[newIndex].Id) + { + ++oldIndex; + ++newIndex; + } + else + { + yield return newArray[newIndex++]; + } + } + } + + /// + /// Get the existing compiler diagnostics on the inputted document. + /// + /// The Document to run the compiler diagnostic analyzers on + /// The compiler diagnostics that were found in the code + private static IEnumerable GetCompilerDiagnostics(Document document) + { + return document.GetSemanticModelAsync().Result.GetDiagnostics(); + } + + /// + /// Given a document, turn it into a string based on the syntax root + /// + /// The Document to be converted to a string + /// A string containing the syntax of the Document after formatting + private static string GetStringFromDocument(Document document) + { + var simplifiedDoc = Simplifier.ReduceAsync(document, Simplifier.Annotation).Result; + var root = simplifiedDoc.GetSyntaxRootAsync().Result; + root = Formatter.Format(root, Formatter.Annotation, simplifiedDoc.Project.Solution.Workspace); + return root.GetText().ToString(); + } + } +} + diff --git a/test/Discord.Net.Tests/AnalyzerTests/Helpers/DiagnosticResult.cs b/test/Discord.Net.Tests/AnalyzerTests/Helpers/DiagnosticResult.cs new file mode 100644 index 0000000000..5ae6f528e8 --- /dev/null +++ b/test/Discord.Net.Tests/AnalyzerTests/Helpers/DiagnosticResult.cs @@ -0,0 +1,87 @@ +using Microsoft.CodeAnalysis; +using System; + +namespace TestHelper +{ + /// + /// Location where the diagnostic appears, as determined by path, line number, and column number. + /// + public struct DiagnosticResultLocation + { + public DiagnosticResultLocation(string path, int line, int column) + { + if (line < -1) + { + throw new ArgumentOutOfRangeException(nameof(line), "line must be >= -1"); + } + + if (column < -1) + { + throw new ArgumentOutOfRangeException(nameof(column), "column must be >= -1"); + } + + this.Path = path; + this.Line = line; + this.Column = column; + } + + public string Path { get; } + public int Line { get; } + public int Column { get; } + } + + /// + /// Struct that stores information about a Diagnostic appearing in a source + /// + public struct DiagnosticResult + { + private DiagnosticResultLocation[] locations; + + public DiagnosticResultLocation[] Locations + { + get + { + if (this.locations == null) + { + this.locations = new DiagnosticResultLocation[] { }; + } + return this.locations; + } + + set + { + this.locations = value; + } + } + + public DiagnosticSeverity Severity { get; set; } + + public string Id { get; set; } + + public string Message { get; set; } + + public string Path + { + get + { + return this.Locations.Length > 0 ? this.Locations[0].Path : ""; + } + } + + public int Line + { + get + { + return this.Locations.Length > 0 ? this.Locations[0].Line : -1; + } + } + + public int Column + { + get + { + return this.Locations.Length > 0 ? this.Locations[0].Column : -1; + } + } + } +} \ No newline at end of file diff --git a/test/Discord.Net.Tests/AnalyzerTests/Helpers/DiagnosticVerifier.Helper.cs b/test/Discord.Net.Tests/AnalyzerTests/Helpers/DiagnosticVerifier.Helper.cs new file mode 100644 index 0000000000..7a8eb2e9c2 --- /dev/null +++ b/test/Discord.Net.Tests/AnalyzerTests/Helpers/DiagnosticVerifier.Helper.cs @@ -0,0 +1,207 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Reflection; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Text; +using Discord; +using Discord.Commands; + +namespace TestHelper +{ + /// + /// Class for turning strings into documents and getting the diagnostics on them + /// All methods are static + /// + public abstract partial class DiagnosticVerifier + { + private static readonly MetadataReference CorlibReference = MetadataReference.CreateFromFile(typeof(object).GetTypeInfo().Assembly.Location); + private static readonly MetadataReference SystemCoreReference = MetadataReference.CreateFromFile(typeof(Enumerable).GetTypeInfo().Assembly.Location); + private static readonly MetadataReference CSharpSymbolsReference = MetadataReference.CreateFromFile(typeof(CSharpCompilation).GetTypeInfo().Assembly.Location); + private static readonly MetadataReference CodeAnalysisReference = MetadataReference.CreateFromFile(typeof(Compilation).GetTypeInfo().Assembly.Location); + //private static readonly MetadataReference DiscordNetReference = MetadataReference.CreateFromFile(typeof(IDiscordClient).GetTypeInfo().Assembly.Location); + //private static readonly MetadataReference DiscordCommandsReference = MetadataReference.CreateFromFile(typeof(CommandAttribute).GetTypeInfo().Assembly.Location); + private static readonly Assembly DiscordCommandsAssembly = typeof(CommandAttribute).GetTypeInfo().Assembly; + + internal static string DefaultFilePathPrefix = "Test"; + internal static string CSharpDefaultFileExt = "cs"; + internal static string VisualBasicDefaultExt = "vb"; + internal static string TestProjectName = "TestProject"; + + #region Get Diagnostics + + /// + /// Given classes in the form of strings, their language, and an IDiagnosticAnlayzer to apply to it, return the diagnostics found in the string after converting it to a document. + /// + /// Classes in the form of strings + /// The language the source classes are in + /// The analyzer to be run on the sources + /// An IEnumerable of Diagnostics that surfaced in the source code, sorted by Location + private static Diagnostic[] GetSortedDiagnostics(string[] sources, string language, DiagnosticAnalyzer analyzer) + { + return GetSortedDiagnosticsFromDocuments(analyzer, GetDocuments(sources, language)); + } + + /// + /// Given an analyzer and a document to apply it to, run the analyzer and gather an array of diagnostics found in it. + /// The returned diagnostics are then ordered by location in the source document. + /// + /// The analyzer to run on the documents + /// The Documents that the analyzer will be run on + /// An IEnumerable of Diagnostics that surfaced in the source code, sorted by Location + protected static Diagnostic[] GetSortedDiagnosticsFromDocuments(DiagnosticAnalyzer analyzer, Document[] documents) + { + var projects = new HashSet(); + foreach (var document in documents) + { + projects.Add(document.Project); + } + + var diagnostics = new List(); + foreach (var project in projects) + { + var compilationWithAnalyzers = project.GetCompilationAsync().Result.WithAnalyzers(ImmutableArray.Create(analyzer)); + var diags = compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync().Result; + foreach (var diag in diags) + { + if (diag.Location == Location.None || diag.Location.IsInMetadata) + { + diagnostics.Add(diag); + } + else + { + for (int i = 0; i < documents.Length; i++) + { + var document = documents[i]; + var tree = document.GetSyntaxTreeAsync().Result; + if (tree == diag.Location.SourceTree) + { + diagnostics.Add(diag); + } + } + } + } + } + + var results = SortDiagnostics(diagnostics); + diagnostics.Clear(); + return results; + } + + /// + /// Sort diagnostics by location in source document + /// + /// The list of Diagnostics to be sorted + /// An IEnumerable containing the Diagnostics in order of Location + private static Diagnostic[] SortDiagnostics(IEnumerable diagnostics) + { + return diagnostics.OrderBy(d => d.Location.SourceSpan.Start).ToArray(); + } + + #endregion + + #region Set up compilation and documents + /// + /// Given an array of strings as sources and a language, turn them into a project and return the documents and spans of it. + /// + /// Classes in the form of strings + /// The language the source code is in + /// A Tuple containing the Documents produced from the sources and their TextSpans if relevant + private static Document[] GetDocuments(string[] sources, string language) + { + if (language != LanguageNames.CSharp && language != LanguageNames.VisualBasic) + { + throw new ArgumentException("Unsupported Language"); + } + + var project = CreateProject(sources, language); + var documents = project.Documents.ToArray(); + + if (sources.Length != documents.Length) + { + throw new Exception("Amount of sources did not match amount of Documents created"); + } + + return documents; + } + + /// + /// Create a Document from a string through creating a project that contains it. + /// + /// Classes in the form of a string + /// The language the source code is in + /// A Document created from the source string + protected static Document CreateDocument(string source, string language = LanguageNames.CSharp) + { + return CreateProject(new[] { source }, language).Documents.First(); + } + + /// + /// Create a project using the inputted strings as sources. + /// + /// Classes in the form of strings + /// The language the source code is in + /// A Project created out of the Documents created from the source strings + private static Project CreateProject(string[] sources, string language = LanguageNames.CSharp) + { + string fileNamePrefix = DefaultFilePathPrefix; + string fileExt = language == LanguageNames.CSharp ? CSharpDefaultFileExt : VisualBasicDefaultExt; + + var projectId = ProjectId.CreateNewId(debugName: TestProjectName); + + var solution = new AdhocWorkspace() + .CurrentSolution + .AddProject(projectId, TestProjectName, TestProjectName, language) + .AddMetadataReference(projectId, CorlibReference) + .AddMetadataReference(projectId, SystemCoreReference) + .AddMetadataReference(projectId, CSharpSymbolsReference) + .AddMetadataReference(projectId, CodeAnalysisReference) + .AddMetadataReferences(projectId, Transitive(DiscordCommandsAssembly)); + + int count = 0; + foreach (var source in sources) + { + var newFileName = fileNamePrefix + count + "." + fileExt; + var documentId = DocumentId.CreateNewId(projectId, debugName: newFileName); + solution = solution.AddDocument(documentId, newFileName, SourceText.From(source)); + count++; + } + return solution.GetProject(projectId); + } + #endregion + + /// + /// Get the for and all assemblies referenced by + /// + /// The assembly. + /// s. + private static IEnumerable Transitive(Assembly assembly) + { + foreach (var a in RecursiveReferencedAssemblies(assembly)) + { + yield return MetadataReference.CreateFromFile(a.Location); + } + } + + private static HashSet RecursiveReferencedAssemblies(Assembly a, HashSet assemblies = null) + { + assemblies = assemblies ?? new HashSet(); + if (assemblies.Add(a)) + { + foreach (var referencedAssemblyName in a.GetReferencedAssemblies()) + { + var referencedAssembly = AppDomain.CurrentDomain.GetAssemblies() + .SingleOrDefault(x => x.GetName() == referencedAssemblyName) ?? + Assembly.Load(referencedAssemblyName); + RecursiveReferencedAssemblies(referencedAssembly, assemblies); + } + } + + return assemblies; + } + } +} + diff --git a/test/Discord.Net.Tests/AnalyzerTests/Verifiers/CodeFixVerifier.cs b/test/Discord.Net.Tests/AnalyzerTests/Verifiers/CodeFixVerifier.cs new file mode 100644 index 0000000000..5d057b610c --- /dev/null +++ b/test/Discord.Net.Tests/AnalyzerTests/Verifiers/CodeFixVerifier.cs @@ -0,0 +1,129 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Formatting; +//using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using Xunit; + +namespace TestHelper +{ + /// + /// Superclass of all Unit tests made for diagnostics with codefixes. + /// Contains methods used to verify correctness of codefixes + /// + public abstract partial class CodeFixVerifier : DiagnosticVerifier + { + /// + /// Returns the codefix being tested (C#) - to be implemented in non-abstract class + /// + /// The CodeFixProvider to be used for CSharp code + protected virtual CodeFixProvider GetCSharpCodeFixProvider() + { + return null; + } + + /// + /// Returns the codefix being tested (VB) - to be implemented in non-abstract class + /// + /// The CodeFixProvider to be used for VisualBasic code + protected virtual CodeFixProvider GetBasicCodeFixProvider() + { + return null; + } + + /// + /// Called to test a C# codefix when applied on the inputted string as a source + /// + /// A class in the form of a string before the CodeFix was applied to it + /// A class in the form of a string after the CodeFix was applied to it + /// Index determining which codefix to apply if there are multiple + /// A bool controlling whether or not the test will fail if the CodeFix introduces other warnings after being applied + protected void VerifyCSharpFix(string oldSource, string newSource, int? codeFixIndex = null, bool allowNewCompilerDiagnostics = false) + { + VerifyFix(LanguageNames.CSharp, GetCSharpDiagnosticAnalyzer(), GetCSharpCodeFixProvider(), oldSource, newSource, codeFixIndex, allowNewCompilerDiagnostics); + } + + /// + /// Called to test a VB codefix when applied on the inputted string as a source + /// + /// A class in the form of a string before the CodeFix was applied to it + /// A class in the form of a string after the CodeFix was applied to it + /// Index determining which codefix to apply if there are multiple + /// A bool controlling whether or not the test will fail if the CodeFix introduces other warnings after being applied + protected void VerifyBasicFix(string oldSource, string newSource, int? codeFixIndex = null, bool allowNewCompilerDiagnostics = false) + { + VerifyFix(LanguageNames.VisualBasic, GetBasicDiagnosticAnalyzer(), GetBasicCodeFixProvider(), oldSource, newSource, codeFixIndex, allowNewCompilerDiagnostics); + } + + /// + /// General verifier for codefixes. + /// Creates a Document from the source string, then gets diagnostics on it and applies the relevant codefixes. + /// Then gets the string after the codefix is applied and compares it with the expected result. + /// Note: If any codefix causes new diagnostics to show up, the test fails unless allowNewCompilerDiagnostics is set to true. + /// + /// The language the source code is in + /// The analyzer to be applied to the source code + /// The codefix to be applied to the code wherever the relevant Diagnostic is found + /// A class in the form of a string before the CodeFix was applied to it + /// A class in the form of a string after the CodeFix was applied to it + /// Index determining which codefix to apply if there are multiple + /// A bool controlling whether or not the test will fail if the CodeFix introduces other warnings after being applied + private void VerifyFix(string language, DiagnosticAnalyzer analyzer, CodeFixProvider codeFixProvider, string oldSource, string newSource, int? codeFixIndex, bool allowNewCompilerDiagnostics) + { + var document = CreateDocument(oldSource, language); + var analyzerDiagnostics = GetSortedDiagnosticsFromDocuments(analyzer, new[] { document }); + var compilerDiagnostics = GetCompilerDiagnostics(document); + var attempts = analyzerDiagnostics.Length; + + for (int i = 0; i < attempts; ++i) + { + var actions = new List(); + var context = new CodeFixContext(document, analyzerDiagnostics[0], (a, d) => actions.Add(a), CancellationToken.None); + codeFixProvider.RegisterCodeFixesAsync(context).Wait(); + + if (!actions.Any()) + { + break; + } + + if (codeFixIndex != null) + { + document = ApplyFix(document, actions.ElementAt((int)codeFixIndex)); + break; + } + + document = ApplyFix(document, actions.ElementAt(0)); + analyzerDiagnostics = GetSortedDiagnosticsFromDocuments(analyzer, new[] { document }); + + var newCompilerDiagnostics = GetNewDiagnostics(compilerDiagnostics, GetCompilerDiagnostics(document)); + + //check if applying the code fix introduced any new compiler diagnostics + if (!allowNewCompilerDiagnostics && newCompilerDiagnostics.Any()) + { + // Format and get the compiler diagnostics again so that the locations make sense in the output + document = document.WithSyntaxRoot(Formatter.Format(document.GetSyntaxRootAsync().Result, Formatter.Annotation, document.Project.Solution.Workspace)); + newCompilerDiagnostics = GetNewDiagnostics(compilerDiagnostics, GetCompilerDiagnostics(document)); + + Assert.True(false, + string.Format("Fix introduced new compiler diagnostics:\r\n{0}\r\n\r\nNew document:\r\n{1}\r\n", + string.Join("\r\n", newCompilerDiagnostics.Select(d => d.ToString())), + document.GetSyntaxRootAsync().Result.ToFullString())); + } + + //check if there are analyzer diagnostics left after the code fix + if (!analyzerDiagnostics.Any()) + { + break; + } + } + + //after applying all of the code fixes, compare the resulting string to the inputted one + var actual = GetStringFromDocument(document); + Assert.Equal(newSource, actual); + } + } +} \ No newline at end of file diff --git a/test/Discord.Net.Tests/AnalyzerTests/Verifiers/DiagnosticVerifier.cs b/test/Discord.Net.Tests/AnalyzerTests/Verifiers/DiagnosticVerifier.cs new file mode 100644 index 0000000000..498e5ef275 --- /dev/null +++ b/test/Discord.Net.Tests/AnalyzerTests/Verifiers/DiagnosticVerifier.cs @@ -0,0 +1,271 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +//using Microsoft.VisualStudio.TestTools.UnitTesting; +using Xunit; + +namespace TestHelper +{ + /// + /// Superclass of all Unit Tests for DiagnosticAnalyzers + /// + public abstract partial class DiagnosticVerifier + { + #region To be implemented by Test classes + /// + /// Get the CSharp analyzer being tested - to be implemented in non-abstract class + /// + protected virtual DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer() + { + return null; + } + + /// + /// Get the Visual Basic analyzer being tested (C#) - to be implemented in non-abstract class + /// + protected virtual DiagnosticAnalyzer GetBasicDiagnosticAnalyzer() + { + return null; + } + #endregion + + #region Verifier wrappers + + /// + /// Called to test a C# DiagnosticAnalyzer when applied on the single inputted string as a source + /// Note: input a DiagnosticResult for each Diagnostic expected + /// + /// A class in the form of a string to run the analyzer on + /// DiagnosticResults that should appear after the analyzer is run on the source + protected void VerifyCSharpDiagnostic(string source, params DiagnosticResult[] expected) + { + VerifyDiagnostics(new[] { source }, LanguageNames.CSharp, GetCSharpDiagnosticAnalyzer(), expected); + } + + /// + /// Called to test a VB DiagnosticAnalyzer when applied on the single inputted string as a source + /// Note: input a DiagnosticResult for each Diagnostic expected + /// + /// A class in the form of a string to run the analyzer on + /// DiagnosticResults that should appear after the analyzer is run on the source + protected void VerifyBasicDiagnostic(string source, params DiagnosticResult[] expected) + { + VerifyDiagnostics(new[] { source }, LanguageNames.VisualBasic, GetBasicDiagnosticAnalyzer(), expected); + } + + /// + /// Called to test a C# DiagnosticAnalyzer when applied on the inputted strings as a source + /// Note: input a DiagnosticResult for each Diagnostic expected + /// + /// An array of strings to create source documents from to run the analyzers on + /// DiagnosticResults that should appear after the analyzer is run on the sources + protected void VerifyCSharpDiagnostic(string[] sources, params DiagnosticResult[] expected) + { + VerifyDiagnostics(sources, LanguageNames.CSharp, GetCSharpDiagnosticAnalyzer(), expected); + } + + /// + /// Called to test a VB DiagnosticAnalyzer when applied on the inputted strings as a source + /// Note: input a DiagnosticResult for each Diagnostic expected + /// + /// An array of strings to create source documents from to run the analyzers on + /// DiagnosticResults that should appear after the analyzer is run on the sources + protected void VerifyBasicDiagnostic(string[] sources, params DiagnosticResult[] expected) + { + VerifyDiagnostics(sources, LanguageNames.VisualBasic, GetBasicDiagnosticAnalyzer(), expected); + } + + /// + /// General method that gets a collection of actual diagnostics found in the source after the analyzer is run, + /// then verifies each of them. + /// + /// An array of strings to create source documents from to run the analyzers on + /// The language of the classes represented by the source strings + /// The analyzer to be run on the source code + /// DiagnosticResults that should appear after the analyzer is run on the sources + private void VerifyDiagnostics(string[] sources, string language, DiagnosticAnalyzer analyzer, params DiagnosticResult[] expected) + { + var diagnostics = GetSortedDiagnostics(sources, language, analyzer); + VerifyDiagnosticResults(diagnostics, analyzer, expected); + } + + #endregion + + #region Actual comparisons and verifications + /// + /// Checks each of the actual Diagnostics found and compares them with the corresponding DiagnosticResult in the array of expected results. + /// Diagnostics are considered equal only if the DiagnosticResultLocation, Id, Severity, and Message of the DiagnosticResult match the actual diagnostic. + /// + /// The Diagnostics found by the compiler after running the analyzer on the source code + /// The analyzer that was being run on the sources + /// Diagnostic Results that should have appeared in the code + private static void VerifyDiagnosticResults(IEnumerable actualResults, DiagnosticAnalyzer analyzer, params DiagnosticResult[] expectedResults) + { + int expectedCount = expectedResults.Count(); + int actualCount = actualResults.Count(); + + if (expectedCount != actualCount) + { + string diagnosticsOutput = actualResults.Any() ? FormatDiagnostics(analyzer, actualResults.ToArray()) : " NONE."; + + Assert.True(false, + string.Format("Mismatch between number of diagnostics returned, expected \"{0}\" actual \"{1}\"\r\n\r\nDiagnostics:\r\n{2}\r\n", expectedCount, actualCount, diagnosticsOutput)); + } + + for (int i = 0; i < expectedResults.Length; i++) + { + var actual = actualResults.ElementAt(i); + var expected = expectedResults[i]; + + if (expected.Line == -1 && expected.Column == -1) + { + if (actual.Location != Location.None) + { + Assert.True(false, + string.Format("Expected:\nA project diagnostic with No location\nActual:\n{0}", + FormatDiagnostics(analyzer, actual))); + } + } + else + { + VerifyDiagnosticLocation(analyzer, actual, actual.Location, expected.Locations.First()); + var additionalLocations = actual.AdditionalLocations.ToArray(); + + if (additionalLocations.Length != expected.Locations.Length - 1) + { + Assert.True(false, + string.Format("Expected {0} additional locations but got {1} for Diagnostic:\r\n {2}\r\n", + expected.Locations.Length - 1, additionalLocations.Length, + FormatDiagnostics(analyzer, actual))); + } + + for (int j = 0; j < additionalLocations.Length; ++j) + { + VerifyDiagnosticLocation(analyzer, actual, additionalLocations[j], expected.Locations[j + 1]); + } + } + + if (actual.Id != expected.Id) + { + Assert.True(false, + string.Format("Expected diagnostic id to be \"{0}\" was \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n", + expected.Id, actual.Id, FormatDiagnostics(analyzer, actual))); + } + + if (actual.Severity != expected.Severity) + { + Assert.True(false, + string.Format("Expected diagnostic severity to be \"{0}\" was \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n", + expected.Severity, actual.Severity, FormatDiagnostics(analyzer, actual))); + } + + if (actual.GetMessage() != expected.Message) + { + Assert.True(false, + string.Format("Expected diagnostic message to be \"{0}\" was \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n", + expected.Message, actual.GetMessage(), FormatDiagnostics(analyzer, actual))); + } + } + } + + /// + /// Helper method to VerifyDiagnosticResult that checks the location of a diagnostic and compares it with the location in the expected DiagnosticResult. + /// + /// The analyzer that was being run on the sources + /// The diagnostic that was found in the code + /// The Location of the Diagnostic found in the code + /// The DiagnosticResultLocation that should have been found + private static void VerifyDiagnosticLocation(DiagnosticAnalyzer analyzer, Diagnostic diagnostic, Location actual, DiagnosticResultLocation expected) + { + var actualSpan = actual.GetLineSpan(); + + Assert.True(actualSpan.Path == expected.Path || (actualSpan.Path != null && actualSpan.Path.Contains("Test0.") && expected.Path.Contains("Test.")), + string.Format("Expected diagnostic to be in file \"{0}\" was actually in file \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n", + expected.Path, actualSpan.Path, FormatDiagnostics(analyzer, diagnostic))); + + var actualLinePosition = actualSpan.StartLinePosition; + + // Only check line position if there is an actual line in the real diagnostic + if (actualLinePosition.Line > 0) + { + if (actualLinePosition.Line + 1 != expected.Line) + { + Assert.True(false, + string.Format("Expected diagnostic to be on line \"{0}\" was actually on line \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n", + expected.Line, actualLinePosition.Line + 1, FormatDiagnostics(analyzer, diagnostic))); + } + } + + // Only check column position if there is an actual column position in the real diagnostic + if (actualLinePosition.Character > 0) + { + if (actualLinePosition.Character + 1 != expected.Column) + { + Assert.True(false, + string.Format("Expected diagnostic to start at column \"{0}\" was actually at column \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n", + expected.Column, actualLinePosition.Character + 1, FormatDiagnostics(analyzer, diagnostic))); + } + } + } + #endregion + + #region Formatting Diagnostics + /// + /// Helper method to format a Diagnostic into an easily readable string + /// + /// The analyzer that this verifier tests + /// The Diagnostics to be formatted + /// The Diagnostics formatted as a string + private static string FormatDiagnostics(DiagnosticAnalyzer analyzer, params Diagnostic[] diagnostics) + { + var builder = new StringBuilder(); + for (int i = 0; i < diagnostics.Length; ++i) + { + builder.AppendLine("// " + diagnostics[i].ToString()); + + var analyzerType = analyzer.GetType(); + var rules = analyzer.SupportedDiagnostics; + + foreach (var rule in rules) + { + if (rule != null && rule.Id == diagnostics[i].Id) + { + var location = diagnostics[i].Location; + if (location == Location.None) + { + builder.AppendFormat("GetGlobalResult({0}.{1})", analyzerType.Name, rule.Id); + } + else + { + Assert.True(location.IsInSource, + $"Test base does not currently handle diagnostics in metadata locations. Diagnostic in metadata: {diagnostics[i]}\r\n"); + + string resultMethodName = diagnostics[i].Location.SourceTree.FilePath.EndsWith(".cs") ? "GetCSharpResultAt" : "GetBasicResultAt"; + var linePosition = diagnostics[i].Location.GetLineSpan().StartLinePosition; + + builder.AppendFormat("{0}({1}, {2}, {3}.{4})", + resultMethodName, + linePosition.Line + 1, + linePosition.Character + 1, + analyzerType.Name, + rule.Id); + } + + if (i != diagnostics.Length - 1) + { + builder.Append(','); + } + + builder.AppendLine(); + break; + } + } + } + return builder.ToString(); + } + #endregion + } +} diff --git a/test/Discord.Net.Tests/Discord.Net.Tests.csproj b/test/Discord.Net.Tests/Discord.Net.Tests.csproj index 9e734641c9..bf24571871 100644 --- a/test/Discord.Net.Tests/Discord.Net.Tests.csproj +++ b/test/Discord.Net.Tests/Discord.Net.Tests.csproj @@ -14,6 +14,7 @@ + From a384ce02abcca096cb99d55f7ba9b52c2ec48148 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Sat, 13 Jan 2018 23:20:04 -0500 Subject: [PATCH 07/48] Support listening/watching activity types This resolves #931 As part of this change, StreamingType has been refactored to realign with how Discord seems to define the 'type' field on activities now. StreamType is renamed to ActivityType, and the following properties have been changed: - NotStreaming -> Playing - Twitch -> Streaming Additionally, the StreamType property/parameter has been removed from StreamingGame, and moved up a scope to Game. Normal Games may now set their type, to line up with changes in Discord's official clients. --- .../Entities/Activities/ActivityType.cs | 10 ++++++++++ .../Entities/Activities/Game.cs | 4 +++- .../Entities/Activities/IActivity.cs | 9 ++------- .../Entities/Activities/StreamingGame.cs | 7 +++---- .../Entities/Users/StreamType.cs | 8 -------- src/Discord.Net.Rest/API/Common/Game.cs | 2 +- src/Discord.Net.WebSocket/BaseSocketClient.cs | 2 +- .../DiscordShardedClient.cs | 12 +++++------ .../DiscordSocketClient.cs | 20 +++++++++---------- .../Extensions/EntityExtensions.cs | 5 ++--- 10 files changed, 37 insertions(+), 42 deletions(-) create mode 100644 src/Discord.Net.Core/Entities/Activities/ActivityType.cs delete mode 100644 src/Discord.Net.Core/Entities/Users/StreamType.cs diff --git a/src/Discord.Net.Core/Entities/Activities/ActivityType.cs b/src/Discord.Net.Core/Entities/Activities/ActivityType.cs new file mode 100644 index 0000000000..c7db7b2475 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Activities/ActivityType.cs @@ -0,0 +1,10 @@ +namespace Discord +{ + public enum ActivityType + { + Playing = 0, + Streaming = 1, + Listening = 2, + Watching = 3 + } +} diff --git a/src/Discord.Net.Core/Entities/Activities/Game.cs b/src/Discord.Net.Core/Entities/Activities/Game.cs index f2b7e8eb6a..fe32470eef 100644 --- a/src/Discord.Net.Core/Entities/Activities/Game.cs +++ b/src/Discord.Net.Core/Entities/Activities/Game.cs @@ -6,11 +6,13 @@ namespace Discord public class Game : IActivity { public string Name { get; internal set; } + public ActivityType Type { get; internal set; } internal Game() { } - public Game(string name) + public Game(string name, ActivityType type = ActivityType.Playing) { Name = name; + Type = type; } public override string ToString() => Name; diff --git a/src/Discord.Net.Core/Entities/Activities/IActivity.cs b/src/Discord.Net.Core/Entities/Activities/IActivity.cs index 0dcf342738..1f158217db 100644 --- a/src/Discord.Net.Core/Entities/Activities/IActivity.cs +++ b/src/Discord.Net.Core/Entities/Activities/IActivity.cs @@ -1,13 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Discord +namespace Discord { public interface IActivity { string Name { get; } + ActivityType Type { get; } } } diff --git a/src/Discord.Net.Core/Entities/Activities/StreamingGame.cs b/src/Discord.Net.Core/Entities/Activities/StreamingGame.cs index 140024272a..afbc24cd90 100644 --- a/src/Discord.Net.Core/Entities/Activities/StreamingGame.cs +++ b/src/Discord.Net.Core/Entities/Activities/StreamingGame.cs @@ -6,15 +6,14 @@ namespace Discord public class StreamingGame : Game { public string Url { get; internal set; } - public StreamType StreamType { get; internal set; } - public StreamingGame(string name, string url, StreamType streamType) + public StreamingGame(string name, string url) { Name = name; Url = url; - StreamType = streamType; + Type = ActivityType.Streaming; } - + public override string ToString() => Name; private string DebuggerDisplay => $"{Name} ({Url})"; } diff --git a/src/Discord.Net.Core/Entities/Users/StreamType.cs b/src/Discord.Net.Core/Entities/Users/StreamType.cs deleted file mode 100644 index 7622e3d6ea..0000000000 --- a/src/Discord.Net.Core/Entities/Users/StreamType.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Discord -{ - public enum StreamType - { - NotStreaming = 0, - Twitch = 1 - } -} diff --git a/src/Discord.Net.Rest/API/Common/Game.cs b/src/Discord.Net.Rest/API/Common/Game.cs index bfb8616925..29e0d987d4 100644 --- a/src/Discord.Net.Rest/API/Common/Game.cs +++ b/src/Discord.Net.Rest/API/Common/Game.cs @@ -12,7 +12,7 @@ internal class Game [JsonProperty("url")] public Optional StreamUrl { get; set; } [JsonProperty("type")] - public Optional StreamType { get; set; } + public Optional Type { get; set; } [JsonProperty("details")] public Optional Details { get; set; } [JsonProperty("state")] diff --git a/src/Discord.Net.WebSocket/BaseSocketClient.cs b/src/Discord.Net.WebSocket/BaseSocketClient.cs index 2ab244aeb6..5fa3cbff8c 100644 --- a/src/Discord.Net.WebSocket/BaseSocketClient.cs +++ b/src/Discord.Net.WebSocket/BaseSocketClient.cs @@ -44,7 +44,7 @@ private static DiscordSocketApiClient CreateApiClient(DiscordSocketConfig config /// public abstract Task StopAsync(); public abstract Task SetStatusAsync(UserStatus status); - public abstract Task SetGameAsync(string name, string streamUrl = null, StreamType streamType = StreamType.NotStreaming); + public abstract Task SetGameAsync(string name, string streamUrl = null, ActivityType type = ActivityType.Playing); public abstract Task SetActivityAsync(IActivity activity); public abstract Task DownloadUsersAsync(IEnumerable guilds); diff --git a/src/Discord.Net.WebSocket/DiscordShardedClient.cs b/src/Discord.Net.WebSocket/DiscordShardedClient.cs index 4e99ae28d5..fb78a201f6 100644 --- a/src/Discord.Net.WebSocket/DiscordShardedClient.cs +++ b/src/Discord.Net.WebSocket/DiscordShardedClient.cs @@ -1,4 +1,4 @@ -using Discord.API; +using Discord.API; using Discord.Rest; using System; using System.Collections.Generic; @@ -238,13 +238,13 @@ public override async Task SetStatusAsync(UserStatus status) for (int i = 0; i < _shards.Length; i++) await _shards[i].SetStatusAsync(status).ConfigureAwait(false); } - public override async Task SetGameAsync(string name, string streamUrl = null, StreamType streamType = StreamType.NotStreaming) + public override async Task SetGameAsync(string name, string streamUrl = null, ActivityType type = ActivityType.Playing) { IActivity activity = null; - if (streamUrl != null) - activity = new StreamingGame(name, streamUrl, streamType); - else if (name != null) - activity = new Game(name); + if (!string.IsNullOrEmpty(streamUrl)) + activity = new StreamingGame(name, streamUrl); + else if (!string.IsNullOrEmpty(name)) + activity = new Game(name, type); await SetActivityAsync(activity).ConfigureAwait(false); } public override async Task SetActivityAsync(IActivity activity) diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index cb3f23c571..c220c337b2 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -1,4 +1,4 @@ -#pragma warning disable CS0618 +#pragma warning disable CS0618 using Discord.API; using Discord.API.Gateway; using Discord.Logging; @@ -326,12 +326,12 @@ public override async Task SetStatusAsync(UserStatus status) _statusSince = null; await SendStatusAsync().ConfigureAwait(false); } - public override async Task SetGameAsync(string name, string streamUrl = null, StreamType streamType = StreamType.NotStreaming) + public override async Task SetGameAsync(string name, string streamUrl = null, ActivityType type = ActivityType.Playing) { if (!string.IsNullOrEmpty(streamUrl)) - Activity = new StreamingGame(name, streamUrl, streamType); + Activity = new StreamingGame(name, streamUrl); else if (!string.IsNullOrEmpty(name)) - Activity = new Game(name); + Activity = new Game(name, type); else Activity = null; await SendStatusAsync().ConfigureAwait(false); @@ -354,15 +354,13 @@ private async Task SendStatusAsync() // Discord only accepts rich presence over RPC, don't even bother building a payload if (Activity is RichGame game) throw new NotSupportedException("Outgoing Rich Presences are not supported"); - else if (Activity is StreamingGame stream) - { - gameModel.StreamUrl = stream.Url; - gameModel.StreamType = stream.StreamType; - } - else if (Activity != null) + + if (Activity != null) { gameModel.Name = Activity.Name; - gameModel.StreamType = StreamType.NotStreaming; + gameModel.Type = Activity.Type; + if (Activity is StreamingGame streamGame) + gameModel.StreamUrl = streamGame.Url; } await ApiClient.SendStatusUpdateAsync( diff --git a/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs b/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs index c661636100..f85c89c711 100644 --- a/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs +++ b/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs @@ -27,11 +27,10 @@ public static IActivity ToEntity(this API.Game model) { return new StreamingGame( model.Name, - model.StreamUrl.Value, - model.StreamType.Value.GetValueOrDefault()); + model.StreamUrl.Value); } // Normal Game - return new Game(model.Name); + return new Game(model.Name, model.Type.GetValueOrDefault() ?? ActivityType.Playing); } // (Small, Large) From 05cd1ff85bd4e13ea2aad276eebc09295c9621df Mon Sep 17 00:00:00 2001 From: Christopher F Date: Sun, 14 Jan 2018 19:39:26 -0500 Subject: [PATCH 08/48] Don't attempt to resolve permissions for invalid roles This resolves #824. Discord seems to have inconsistencies where a role can be deleted, but there will still be a few users who still have it in their `role_ids`. I was able to find this bug appearing in 11 members of a 10,000 member guild, so it would make sense that this is relatively rare, and it's why we hadn't noticed it previously. Since our permission resolution code is implementation agnostic, it operates on the user's RoleIds collection, which is what Discord sends us directly, and is not vaidated against the member's guild. In our permission resolution code, we make the assumption that Discord will always be telling us the truth with regard to a member's `role_ids`. This PR changes the behavior of permissions resolution to instead verify that the guild was able to return a role before attempting to resolve its permissions. --- src/Discord.Net.Core/Utils/Permissions.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net.Core/Utils/Permissions.cs b/src/Discord.Net.Core/Utils/Permissions.cs index 367926dd11..7b92c9d3e5 100644 --- a/src/Discord.Net.Core/Utils/Permissions.cs +++ b/src/Discord.Net.Core/Utils/Permissions.cs @@ -133,9 +133,10 @@ public static ulong ResolveChannel(IGuild guild, IGuildUser user, IGuildChannel ulong deniedPermissions = 0UL, allowedPermissions = 0UL; foreach (var roleId in user.RoleIds) { - if (roleId != guild.EveryoneRole.Id) + IRole role = null; + if (roleId != guild.EveryoneRole.Id && (role = guild.GetRole(roleId)) != null) { - perms = channel.GetPermissionOverwrite(guild.GetRole(roleId)); + perms = channel.GetPermissionOverwrite(role); if (perms != null) { allowedPermissions |= perms.Value.AllowValue; From 73ac9d7886aa48b9d809c56e51945056f3b67232 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Sat, 27 Jan 2018 16:08:01 -0500 Subject: [PATCH 09/48] Remove incomplete reconnect handler for certain session invalidations This resolves #938 and #883 Note: This fix is not 'verified' in production (I waited over a week for another full Discord outage and never encountered one), but I do have it on record from b1nzy that Discord may send an OP9 with `{"d": true}` during outages, so this would appear to be the proper fix. The removed code seems to have been leftover from when ConnectionManager was rewritten and never finished. --- src/Discord.Net.WebSocket/DiscordSocketClient.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index c220c337b2..142f244175 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -1,4 +1,4 @@ -#pragma warning disable CS0618 +#pragma warning disable CS0618 using Discord.API; using Discord.API.Gateway; using Discord.Logging; @@ -416,11 +416,8 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty _sessionId = null; _lastSeq = 0; - bool retry = (bool)payload; - if (retry) - _connection.Reconnect(); //TODO: Untested - else - await ApiClient.SendIdentifyAsync(shardID: ShardId, totalShards: TotalShards).ConfigureAwait(false); + + await ApiClient.SendIdentifyAsync(shardID: ShardId, totalShards: TotalShards).ConfigureAwait(false); } break; case GatewayOpCode.Reconnect: From b88ce8c51fbbc47a42c1e1dcacf8af60dadc8a4d Mon Sep 17 00:00:00 2001 From: Paulo Date: Fri, 2 Feb 2018 19:21:16 -0200 Subject: [PATCH 10/48] Remove IGuild.DownloadUsersAsync() from SocketGuild (#944) --- src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index ea68a8f546..e70df8ce8c 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -696,7 +696,6 @@ Task IGuild.GetCurrentUserAsync(CacheMode mode, RequestOptions optio => Task.FromResult(CurrentUser); Task IGuild.GetOwnerAsync(CacheMode mode, RequestOptions options) => Task.FromResult(Owner); - Task IGuild.DownloadUsersAsync() { throw new NotSupportedException(); } async Task IGuild.GetWebhookAsync(ulong id, RequestOptions options) => await GetWebhookAsync(id, options); From 3b2b4342581758eaafa18ee0bc3a4a945ce1bfcb Mon Sep 17 00:00:00 2001 From: Christopher F Date: Sat, 10 Feb 2018 19:37:41 -0500 Subject: [PATCH 11/48] Bump version to 2.0.0-beta2 --- Discord.Net.targets | 2 +- src/Discord.Net/Discord.Net.nuspec | 32 +++++++++++++++--------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/Discord.Net.targets b/Discord.Net.targets index 3f623c619a..958b2053f5 100644 --- a/Discord.Net.targets +++ b/Discord.Net.targets @@ -1,7 +1,7 @@ 2.0.0 - beta + beta2 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 cd57d2fcf1..2bf531cb1c 100644 --- a/src/Discord.Net/Discord.Net.nuspec +++ b/src/Discord.Net/Discord.Net.nuspec @@ -2,7 +2,7 @@ Discord.Net - 2.0.0-beta$suffix$ + 2.0.0-beta2$suffix$ Discord.Net Discord.Net Contributors RogueException @@ -13,25 +13,25 @@ false - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + From 178ea8de4d53183c30cc304781d2f7acedbd0950 Mon Sep 17 00:00:00 2001 From: Alex Gravely Date: Sat, 17 Feb 2018 18:38:14 -0500 Subject: [PATCH 12/48] Change GameParty size types to longs. (#955) --- src/Discord.Net.Core/Entities/Activities/GameParty.cs | 8 ++++---- src/Discord.Net.Rest/API/Common/GameParty.cs | 6 +++--- src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Activities/GameParty.cs b/src/Discord.Net.Core/Entities/Activities/GameParty.cs index dbfe5b6cea..54e6deef40 100644 --- a/src/Discord.Net.Core/Entities/Activities/GameParty.cs +++ b/src/Discord.Net.Core/Entities/Activities/GameParty.cs @@ -1,11 +1,11 @@ -namespace Discord +namespace Discord { public class GameParty { internal GameParty() { } public string Id { get; internal set; } - public int Members { get; internal set; } - public int Capacity { get; internal set; } + public long Members { get; internal set; } + public long Capacity { get; internal set; } } -} \ No newline at end of file +} diff --git a/src/Discord.Net.Rest/API/Common/GameParty.cs b/src/Discord.Net.Rest/API/Common/GameParty.cs index e0da4a0986..4f8ce26549 100644 --- a/src/Discord.Net.Rest/API/Common/GameParty.cs +++ b/src/Discord.Net.Rest/API/Common/GameParty.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; namespace Discord.API { @@ -7,6 +7,6 @@ internal class GameParty [JsonProperty("id")] public string Id { get; set; } [JsonProperty("size")] - public int[] Size { get; set; } + public long[] Size { get; set; } } -} \ No newline at end of file +} diff --git a/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs b/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs index f85c89c711..181a837e4b 100644 --- a/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs +++ b/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs @@ -1,4 +1,4 @@ -namespace Discord.WebSocket +namespace Discord.WebSocket { internal static class EntityExtensions { @@ -56,7 +56,7 @@ public static GameAsset[] ToEntity(this API.GameAssets model, ulong appId) public static GameParty ToEntity(this API.GameParty model) { // Discord will probably send bad data since they don't validate anything - int current = 0, cap = 0; + long current = 0, cap = 0; if (model.Size?.Length == 2) { current = model.Size[0]; From bb8ebc13d282270a59d79910ae7b16c05e766b5c Mon Sep 17 00:00:00 2001 From: Christopher F Date: Sun, 18 Feb 2018 19:15:52 -0500 Subject: [PATCH 13/48] Add callback method for when a module class has been added (#934) commit 5b047bf02b4299f34172cac05dc7e4a84ecc108c Author: Joe4evr Date: Fri Feb 2 22:22:00 2018 +0100 [feature/OnModuleAdded] Quickstart fixes (#946) * Quickstart: fix minor derp * Other overdue fixes commit bd3e9eee943b9092cc45217b19ff95bae359f888 Author: Christopher F Date: Sat Jan 27 16:51:18 2018 -0500 Resort usings in ModuleBase commit 8042767579b337fdae7fe48e0a6ea2f007aef440 Author: Christopher F Date: Sat Jan 27 16:41:39 2018 -0500 Clean up removed owned IServiceProvider commit 30066cb102ffbd65906ead72a377811aa501abba Author: Christopher F Date: Sat Jan 27 16:37:22 2018 -0500 Remove redundant try-catch around OnModuleBuilding invocation If this exception is going to be rethrown, there's no reason to include a try-catch. commit 60c7c31d4476c498a97ae0536ec5792f08efb89b Author: Christopher F Date: Sat Jan 27 16:36:27 2018 -0500 Include the ModuleBuilder in OnModuleBuilding This allows modules hooking into OnModuleBuilding method to mutate theirselves at runtime. commit b6a9ff57860ff3bddbad7ca850fd331529cb8e6e Author: Joe4evr Date: Mon Jan 22 13:17:14 2018 +0100 #DERP commit f623d19c68c5642a44898a561f77ed82d53fd103 Author: Joe4evr Date: Mon Jan 22 13:15:31 2018 +0100 Resolution for #937 because it's literally 4 lines of code commit 8272c9675b0d63b4100aaf57f5067d635b68f5e6 Author: Joe4evr Date: Mon Jan 22 11:39:28 2018 +0100 Re-adjust quickstart commit e30b9071351b69baa30a93a4851516dca9ea43cf Author: Joe4evr Date: Mon Jan 22 11:35:08 2018 +0100 Undo experimental changes, request IServiceProvider instance everywhere instead commit ad7e0a46c8709e845dfacdc298a893e22dc11567 Author: Joe4evr Date: Fri Jan 19 03:40:27 2018 +0100 Fix quickstart leftover from previous draft commit e3349ef3d400bb3ad8cb28dd4234d5316a80bcc4 Author: Joe4evr Date: Fri Jan 19 03:33:46 2018 +0100 Doc comment on items commit 81bd9111faaf98a52679daae863ab04dce96e63e Author: Joe4evr Date: Fri Jan 19 03:16:44 2018 +0100 Add comment about the ServiceProviderFactory in the quickstart commit 72b5e6c8a149d8e989b46351965daa14f8ca318c Author: Joe4evr Date: Fri Jan 19 03:10:40 2018 +0100 Remove superfluous comments, provide simpler alternative for setting the ServiceProvider. commit 74b17b0e04e2c413397a2e1b66ff814615326205 Author: Joe4evr Date: Tue Jan 16 18:06:28 2018 +0100 Experimental change for feedback commit 7b100e99bb119be190006d1cd8e403776930e401 Author: Joe4evr Date: Mon Jan 15 23:34:06 2018 +0100 * Make the service provider parameters required * Adjust quickstart guide to reflect changes commit 7f1b792946ac6b950922b06178aa5cc37d9f4144 Author: Joe4evr Date: Mon Jan 15 20:04:37 2018 +0100 I..... missed one. commit 031b289d80604666dde62619e521af303203d48d Author: Joe4evr Date: Mon Jan 15 20:02:20 2018 +0100 Rename method to more intuitive 'OnModuleBuilding' commit 9a166ef1d0baecd21e4e5b965e2ac364feddbe2e Author: Joe4evr Date: Mon Jan 15 19:09:10 2018 +0100 Add callback method for when a module class has been added to the CommandService. --- .../samples/intro/structure.cs | 68 +++++--- .../Builders/ModuleBuilder.cs | 20 ++- .../Builders/ModuleClassBuilder.cs | 34 ++-- src/Discord.Net.Commands/CommandService.cs | 156 +++++++++--------- .../CommandServiceConfig.cs | 10 +- src/Discord.Net.Commands/IModuleBase.cs | 6 +- src/Discord.Net.Commands/Info/ModuleInfo.cs | 16 +- src/Discord.Net.Commands/ModuleBase.cs | 10 +- 8 files changed, 190 insertions(+), 130 deletions(-) diff --git a/docs/guides/getting_started/samples/intro/structure.cs b/docs/guides/getting_started/samples/intro/structure.cs index bdfc12b67e..a9a018c3a4 100644 --- a/docs/guides/getting_started/samples/intro/structure.cs +++ b/docs/guides/getting_started/samples/intro/structure.cs @@ -19,10 +19,10 @@ static void Main(string[] args) private readonly DiscordSocketClient _client; - // Keep the CommandService and IServiceCollection around for use with commands. + // Keep the CommandService and DI container around for use with commands. // These two types require you install the Discord.Net.Commands package. - private readonly IServiceCollection _map = new ServiceCollection(); - private readonly CommandService _commands = new CommandService(); + private readonly CommandService _commands; + private readonly IServiceProvider _services; private Program() { @@ -41,14 +41,45 @@ private Program() // add the `using` at the top, and uncomment this line: //WebSocketProvider = WS4NetProvider.Instance }); + + _commands = new CommandService(new CommandServiceConfig + { + // Again, log level: + LogLevel = LogSeverity.Info, + + // There's a few more properties you can set, + // for example, case-insensitive commands. + CaseSensitiveCommands = false, + }); + // Subscribe the logging handler to both the client and the CommandService. - _client.Log += Logger; - _commands.Log += Logger; + _client.Log += Log; + _commands.Log += Log; + + // Setup your DI container. + _services = ConfigureServices(), + + } + + // If any services require the client, or the CommandService, or something else you keep on hand, + // pass them as parameters into this method as needed. + // If this method is getting pretty long, you can seperate it out into another file using partials. + private static IServiceProvider ConfigureServices() + { + var map = new ServiceCollection() + // Repeat this for all the service classes + // and other dependencies that your commands might need. + .AddSingleton(new SomeServiceClass()); + + // 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. + return map.BuildServiceProvider(); } // Example of a logging handler. This can be re-used by addons // that ask for a Func. - private static Task Logger(LogMessage message) + private static Task Log(LogMessage message) { switch (message.Severity) { @@ -92,24 +123,15 @@ private async Task MainAsync() await Task.Delay(Timeout.Infinite); } - private IServiceProvider _services; - private async Task InitCommands() { - // Repeat this for all the service classes - // and other dependencies that your commands might need. - _map.AddSingleton(new SomeServiceClass()); - - // 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. // Module classes MUST be marked 'public' or they will be ignored. - await _commands.AddModulesAsync(Assembly.GetEntryAssembly()); + // You also need to pass your 'IServiceProvider' instance now, + // so make sure that's done before you get here. + await _commands.AddModulesAsync(Assembly.GetEntryAssembly(), _services); // Or add Modules manually if you prefer to be a little more explicit: - await _commands.AddModuleAsync(); + await _commands.AddModuleAsync(_services); // Note that the first one is 'Modules' (plural) and the second is 'Module' (singular). // Subscribe a handler to see if a message invokes a command. @@ -123,8 +145,6 @@ private async Task HandleCommandAsync(SocketMessage arg) if (msg == null) return; // We don't want the bot to respond to itself or other bots. - // NOTE: Selfbots should invert this first check and remove the second - // as they should ONLY be allowed to respond to messages from the same account. if (msg.Author.Id == _client.CurrentUser.Id || msg.Author.IsBot) return; // Create a number to track where the prefix ends and the command begins @@ -140,10 +160,12 @@ private async Task HandleCommandAsync(SocketMessage arg) // Execute the command. (result does not indicate a return value, // rather an object stating if the command executed successfully). - var result = await _commands.ExecuteAsync(context, pos, _services); + var result = await _commands.ExecuteAsync(context, pos); // Uncomment the following lines if you want the bot - // to send a message if it failed (not advised for most situations). + // to send a message if it failed. + // This does not catch errors from commands with 'RunMode.Async', + // subscribe a handler for '_commands.CommandExecuted' to see those. //if (!result.IsSuccess && result.Error != CommandError.UnknownCommand) // await msg.Channel.SendMessageAsync(result.ErrorReason); } diff --git a/src/Discord.Net.Commands/Builders/ModuleBuilder.cs b/src/Discord.Net.Commands/Builders/ModuleBuilder.cs index 0a33c9e264..1809c2c636 100644 --- a/src/Discord.Net.Commands/Builders/ModuleBuilder.cs +++ b/src/Discord.Net.Commands/Builders/ModuleBuilder.cs @@ -1,5 +1,6 @@ -using System; +using System; using System.Collections.Generic; +using System.Reflection; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; @@ -18,6 +19,7 @@ public class ModuleBuilder public string Name { get; set; } public string Summary { get; set; } public string Remarks { get; set; } + public string Group { get; set; } public IReadOnlyList Commands => _commands; public IReadOnlyList Modules => _submodules; @@ -25,6 +27,8 @@ public class ModuleBuilder public IReadOnlyList Attributes => _attributes; public IReadOnlyList Aliases => _aliases; + internal TypeInfo TypeInfo { get; set; } + //Automatic internal ModuleBuilder(CommandService service, ModuleBuilder parent) { @@ -111,17 +115,23 @@ internal ModuleBuilder AddModule(Action createFunc) return this; } - private ModuleInfo BuildImpl(CommandService service, ModuleInfo parent = null) + private ModuleInfo BuildImpl(CommandService service, IServiceProvider services, ModuleInfo parent = null) { //Default name to first alias if (Name == null) Name = _aliases[0]; - return new ModuleInfo(this, service, parent); + if (TypeInfo != null) + { + var moduleInstance = ReflectionUtils.CreateObject(TypeInfo, service, services); + moduleInstance.OnModuleBuilding(service, this); + } + + return new ModuleInfo(this, service, services, parent); } - public ModuleInfo Build(CommandService service) => BuildImpl(service); + public ModuleInfo Build(CommandService service, IServiceProvider services) => BuildImpl(service, services); - internal ModuleInfo Build(CommandService service, ModuleInfo parent) => BuildImpl(service, parent); + internal ModuleInfo Build(CommandService service, IServiceProvider services, ModuleInfo parent) => BuildImpl(service, services, parent); } } diff --git a/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs index 5a3a1f25ad..c0a7e9aca4 100644 --- a/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs +++ b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs @@ -42,8 +42,8 @@ bool IsLoadableModule(TypeInfo info) } - public static Task> BuildAsync(CommandService service, params TypeInfo[] validTypes) => BuildAsync(validTypes, service); - public static async Task> BuildAsync(IEnumerable validTypes, CommandService service) + public static Task> BuildAsync(CommandService service, IServiceProvider services, params TypeInfo[] validTypes) => BuildAsync(validTypes, service, services); + public static async Task> BuildAsync(IEnumerable validTypes, CommandService service, IServiceProvider services) { /*if (!validTypes.Any()) throw new InvalidOperationException("Could not find any valid modules from the given selection");*/ @@ -63,11 +63,11 @@ public static async Task> BuildAsync(IEnumerable> BuildAsync(IEnumerable subTypes, List builtTypes, CommandService service) + private static void BuildSubTypes(ModuleBuilder builder, IEnumerable subTypes, List builtTypes, CommandService service, IServiceProvider services) { foreach (var typeInfo in subTypes) { @@ -87,17 +87,18 @@ private static void BuildSubTypes(ModuleBuilder builder, IEnumerable s builder.AddModule((module) => { - BuildModule(module, typeInfo, service); - BuildSubTypes(module, typeInfo.DeclaredNestedTypes, builtTypes, service); + BuildModule(module, typeInfo, service, services); + BuildSubTypes(module, typeInfo.DeclaredNestedTypes, builtTypes, service, services); }); builtTypes.Add(typeInfo); } } - private static void BuildModule(ModuleBuilder builder, TypeInfo typeInfo, CommandService service) + private static void BuildModule(ModuleBuilder builder, TypeInfo typeInfo, CommandService service, IServiceProvider services) { var attributes = typeInfo.GetCustomAttributes(); + builder.TypeInfo = typeInfo; foreach (var attribute in attributes) { @@ -117,6 +118,7 @@ private static void BuildModule(ModuleBuilder builder, TypeInfo typeInfo, Comman break; case GroupAttribute group: builder.Name = builder.Name ?? group.Prefix; + builder.Group = group.Prefix; builder.AddAliases(group.Prefix); break; case PreconditionAttribute precondition: @@ -140,12 +142,12 @@ private static void BuildModule(ModuleBuilder builder, TypeInfo typeInfo, Comman { builder.AddCommand((command) => { - BuildCommand(command, typeInfo, method, service); + BuildCommand(command, typeInfo, method, service, services); }); } } - private static void BuildCommand(CommandBuilder builder, TypeInfo typeInfo, MethodInfo method, CommandService service) + private static void BuildCommand(CommandBuilder builder, TypeInfo typeInfo, MethodInfo method, CommandService service, IServiceProvider serviceprovider) { var attributes = method.GetCustomAttributes(); @@ -191,7 +193,7 @@ private static void BuildCommand(CommandBuilder builder, TypeInfo typeInfo, Meth { builder.AddParameter((parameter) => { - BuildParameter(parameter, paramInfo, pos++, count, service); + BuildParameter(parameter, paramInfo, pos++, count, service, serviceprovider); }); } @@ -227,7 +229,7 @@ async Task ExecuteCallback(ICommandContext context, object[] args, ISer builder.Callback = ExecuteCallback; } - private static void BuildParameter(ParameterBuilder builder, System.Reflection.ParameterInfo paramInfo, int position, int count, CommandService service) + private static void BuildParameter(ParameterBuilder builder, System.Reflection.ParameterInfo paramInfo, int position, int count, CommandService service, IServiceProvider services) { var attributes = paramInfo.GetCustomAttributes(); var paramType = paramInfo.ParameterType; @@ -245,7 +247,7 @@ private static void BuildParameter(ParameterBuilder builder, System.Reflection.P builder.Summary = summary.Text; break; case OverrideTypeReaderAttribute typeReader: - builder.TypeReader = GetTypeReader(service, paramType, typeReader.TypeReader); + builder.TypeReader = GetTypeReader(service, paramType, typeReader.TypeReader, services); break; case ParamArrayAttribute _: builder.IsMultiple = true; @@ -285,7 +287,7 @@ private static void BuildParameter(ParameterBuilder builder, System.Reflection.P } } - private static TypeReader GetTypeReader(CommandService service, Type paramType, Type typeReaderType) + private static TypeReader GetTypeReader(CommandService service, Type paramType, Type typeReaderType, IServiceProvider services) { var readers = service.GetTypeReaders(paramType); TypeReader reader = null; @@ -296,7 +298,7 @@ private static TypeReader GetTypeReader(CommandService service, Type paramType, } //We dont have a cached type reader, create one - reader = ReflectionUtils.CreateObject(typeReaderType.GetTypeInfo(), service, EmptyServiceProvider.Instance); + reader = ReflectionUtils.CreateObject(typeReaderType.GetTypeInfo(), service, services); service.AddTypeReader(paramType, reader); return reader; diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index 8e7dab898f..7efc1bc625 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -1,5 +1,3 @@ -using Discord.Commands.Builders; -using Discord.Logging; using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -8,6 +6,9 @@ using System.Reflection; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Discord.Commands.Builders; +using Discord.Logging; namespace Discord.Commands { @@ -85,7 +86,8 @@ public async Task CreateModuleAsync(string primaryAlias, Action CreateModuleAsync(string primaryAlias, Action AddModuleAsync() => AddModuleAsync(typeof(T)); - public async Task AddModuleAsync(Type type) + public Task AddModuleAsync(IServiceProvider services) => AddModuleAsync(typeof(T), services); + public async Task AddModuleAsync(Type type, IServiceProvider services) { await _moduleLock.WaitAsync().ConfigureAwait(false); try @@ -104,7 +106,7 @@ public async Task AddModuleAsync(Type type) if (_typedModuleDefs.ContainsKey(type)) throw new ArgumentException($"This module has already been added."); - var module = (await ModuleClassBuilder.BuildAsync(this, typeInfo).ConfigureAwait(false)).FirstOrDefault(); + var module = (await ModuleClassBuilder.BuildAsync(this, services, 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?"); @@ -118,13 +120,13 @@ public async Task AddModuleAsync(Type type) _moduleLock.Release(); } } - public async Task> AddModulesAsync(Assembly assembly) + public async Task> AddModulesAsync(Assembly assembly, IServiceProvider services) { await _moduleLock.WaitAsync().ConfigureAwait(false); try { var types = await ModuleClassBuilder.SearchAsync(assembly, this).ConfigureAwait(false); - var moduleDefs = await ModuleClassBuilder.BuildAsync(types, this).ConfigureAwait(false); + var moduleDefs = await ModuleClassBuilder.BuildAsync(types, this, services).ConfigureAwait(false); foreach (var info in moduleDefs) { @@ -224,7 +226,7 @@ internal void AddNullableTypeReader(Type valueType, TypeReader valueTypeReader) var readers = _typeReaders.GetOrAdd(typeof(Nullable<>).MakeGenericType(valueType), x => new ConcurrentDictionary()); var nullableReader = NullableTypeReader.Create(valueType, valueTypeReader); readers[nullableReader.GetType()] = nullableReader; - } + } internal IDictionary GetTypeReaders(Type type) { if (_typeReaders.TryGetValue(type, out var definedTypeReaders)) @@ -277,92 +279,94 @@ public Task ExecuteAsync(ICommandContext context, int argPos, IServiceP public async Task ExecuteAsync(ICommandContext context, string input, IServiceProvider services = null, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) { services = services ?? EmptyServiceProvider.Instance; - - var searchResult = Search(context, input); - if (!searchResult.IsSuccess) - return searchResult; - - var commands = searchResult.Commands; - var preconditionResults = new Dictionary(); - - foreach (var match in commands) + using (var scope = services.CreateScope()) { - preconditionResults[match] = await match.Command.CheckPreconditionsAsync(context, services).ConfigureAwait(false); - } + var searchResult = Search(context, input); + if (!searchResult.IsSuccess) + return searchResult; - var successfulPreconditions = preconditionResults - .Where(x => x.Value.IsSuccess) - .ToArray(); + var commands = searchResult.Commands; + var preconditionResults = new Dictionary(); - 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; - } + foreach (var match in commands) + { + preconditionResults[match] = await match.Command.CheckPreconditionsAsync(context, scope.ServiceProvider).ConfigureAwait(false); + } - //If we get this far, at least one precondition was successful. + var successfulPreconditions = preconditionResults + .Where(x => x.Value.IsSuccess) + .ToArray(); - var parseResultsDict = new Dictionary(); - foreach (var pair in successfulPreconditions) - { - var parseResult = await pair.Key.ParseAsync(context, searchResult, pair.Value, services).ConfigureAwait(false); + 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 (parseResult.Error == CommandError.MultipleMatches) + //If we get this far, at least one precondition was successful. + + var parseResultsDict = new Dictionary(); + foreach (var pair in successfulPreconditions) { - IReadOnlyList argList, paramList; - switch (multiMatchHandling) + var parseResult = await pair.Key.ParseAsync(context, searchResult, pair.Value, scope.ServiceProvider).ConfigureAwait(false); + + if (parseResult.Error == CommandError.MultipleMatches) { - 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; + 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; + } } - } - parseResultsDict[pair.Key] = parseResult; - } + 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) + // Calculates the 'score' of a command given a parse result + float CalculateScore(CommandMatch match, ParseResult parseResult) { - 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; + float argValuesScore = 0, paramValuesScore = 0; + + if (match.Command.Parameters.Count > 0) + { + 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; + argValuesScore = argValuesSum / match.Command.Parameters.Count; + paramValuesScore = paramValuesSum / match.Command.Parameters.Count; + } + + var totalArgsScore = (argValuesScore + paramValuesScore) / 2; + return match.Command.Priority + totalArgsScore * 0.99f; } - 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)); - //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(); - 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; + } - 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; + //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, scope.ServiceProvider).ConfigureAwait(false); } - - //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/CommandServiceConfig.cs b/src/Discord.Net.Commands/CommandServiceConfig.cs index 7fdbe368b3..77c5b2262f 100644 --- a/src/Discord.Net.Commands/CommandServiceConfig.cs +++ b/src/Discord.Net.Commands/CommandServiceConfig.cs @@ -1,4 +1,6 @@ -namespace Discord.Commands +using System; + +namespace Discord.Commands { public class CommandServiceConfig { @@ -18,5 +20,11 @@ public class CommandServiceConfig /// Determines whether extra parameters should be ignored. public bool IgnoreExtraArgs { get; set; } = false; + + ///// Gets or sets the to use. + //public IServiceProvider ServiceProvider { get; set; } = null; + + ///// Gets or sets a factory function for the to use. + //public Func ServiceProviderFactory { get; set; } = null; } } diff --git a/src/Discord.Net.Commands/IModuleBase.cs b/src/Discord.Net.Commands/IModuleBase.cs index 479724ae30..3b641ec5ff 100644 --- a/src/Discord.Net.Commands/IModuleBase.cs +++ b/src/Discord.Net.Commands/IModuleBase.cs @@ -1,4 +1,6 @@ -namespace Discord.Commands +using Discord.Commands.Builders; + +namespace Discord.Commands { internal interface IModuleBase { @@ -7,5 +9,7 @@ internal interface IModuleBase void BeforeExecute(CommandInfo command); void AfterExecute(CommandInfo command); + + void OnModuleBuilding(CommandService commandService, ModuleBuilder builder); } } diff --git a/src/Discord.Net.Commands/Info/ModuleInfo.cs b/src/Discord.Net.Commands/Info/ModuleInfo.cs index 97b90bf4ed..5a7f9208e4 100644 --- a/src/Discord.Net.Commands/Info/ModuleInfo.cs +++ b/src/Discord.Net.Commands/Info/ModuleInfo.cs @@ -2,7 +2,7 @@ using System.Linq; using System.Collections.Generic; using System.Collections.Immutable; - +using System.Reflection; using Discord.Commands.Builders; namespace Discord.Commands @@ -13,6 +13,7 @@ public class ModuleInfo public string Name { get; } public string Summary { get; } public string Remarks { get; } + public string Group { get; } public IReadOnlyList Aliases { get; } public IReadOnlyList Commands { get; } @@ -22,21 +23,26 @@ public class ModuleInfo public ModuleInfo Parent { get; } public bool IsSubmodule => Parent != null; - internal ModuleInfo(ModuleBuilder builder, CommandService service, ModuleInfo parent = null) + //public TypeInfo TypeInfo { get; } + + internal ModuleInfo(ModuleBuilder builder, CommandService service, IServiceProvider services, ModuleInfo parent = null) { Service = service; Name = builder.Name; Summary = builder.Summary; Remarks = builder.Remarks; + Group = builder.Group; Parent = parent; + //TypeInfo = builder.TypeInfo; + 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(); + Submodules = BuildSubmodules(builder, service, services).ToImmutableArray(); } private static IEnumerable BuildAliases(ModuleBuilder builder, CommandService service) @@ -66,12 +72,12 @@ private static IEnumerable BuildAliases(ModuleBuilder builder, CommandSe return result; } - private List BuildSubmodules(ModuleBuilder parent, CommandService service) + private List BuildSubmodules(ModuleBuilder parent, CommandService service, IServiceProvider services) { var result = new List(); foreach (var submodule in parent.Modules) - result.Add(submodule.Build(service, this)); + result.Add(submodule.Build(service, services, this)); return result; } diff --git a/src/Discord.Net.Commands/ModuleBase.cs b/src/Discord.Net.Commands/ModuleBase.cs index f51656e407..c35a3cf676 100644 --- a/src/Discord.Net.Commands/ModuleBase.cs +++ b/src/Discord.Net.Commands/ModuleBase.cs @@ -1,5 +1,6 @@ -using System; +using System; using System.Threading.Tasks; +using Discord.Commands.Builders; namespace Discord.Commands { @@ -23,15 +24,18 @@ protected virtual void AfterExecute(CommandInfo command) { } + protected virtual void OnModuleBuilding(CommandService commandService, ModuleBuilder builder) + { + } + //IModuleBase void IModuleBase.SetContext(ICommandContext context) { var newValue = context as T; Context = newValue ?? throw new InvalidOperationException($"Invalid context type. Expected {typeof(T).Name}, got {context.GetType().Name}"); } - void IModuleBase.BeforeExecute(CommandInfo command) => BeforeExecute(command); - void IModuleBase.AfterExecute(CommandInfo command) => AfterExecute(command); + void IModuleBase.OnModuleBuilding(CommandService commandService, ModuleBuilder builder) => OnModuleBuilding(commandService, builder); } } From 500f5f434a0aa7e0eb027c6da80f96f9d4c06123 Mon Sep 17 00:00:00 2001 From: Alex Gravely Date: Sun, 18 Feb 2018 19:19:10 -0500 Subject: [PATCH 14/48] Add request info to HttpException & RateLimitedException (#957) * Add request info to RateLimitedException * Remove Promise from interface. * Add Request to HttpException. --- src/Discord.Net.Core/Net/HttpException.cs | 6 ++++-- src/Discord.Net.Core/Net/IRequest.cs | 10 ++++++++++ src/Discord.Net.Core/Net/RateLimitedException.cs | 7 +++++-- .../Net/Queue/RequestQueueBucket.cs | 14 +++++++------- .../Net/Queue/Requests/RestRequest.cs | 4 ++-- .../Net/Queue/Requests/WebSocketRequest.cs | 4 ++-- 6 files changed, 30 insertions(+), 15 deletions(-) create mode 100644 src/Discord.Net.Core/Net/IRequest.cs diff --git a/src/Discord.Net.Core/Net/HttpException.cs b/src/Discord.Net.Core/Net/HttpException.cs index 1c872245c8..d0ee65b238 100644 --- a/src/Discord.Net.Core/Net/HttpException.cs +++ b/src/Discord.Net.Core/Net/HttpException.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Net; namespace Discord.Net @@ -8,11 +8,13 @@ public class HttpException : Exception public HttpStatusCode HttpCode { get; } public int? DiscordCode { get; } public string Reason { get; } + public IRequest Request { get; } - public HttpException(HttpStatusCode httpCode, int? discordCode = null, string reason = null) + public HttpException(HttpStatusCode httpCode, IRequest request, int? discordCode = null, string reason = null) : base(CreateMessage(httpCode, discordCode, reason)) { HttpCode = httpCode; + Request = request; DiscordCode = discordCode; Reason = reason; } diff --git a/src/Discord.Net.Core/Net/IRequest.cs b/src/Discord.Net.Core/Net/IRequest.cs new file mode 100644 index 0000000000..d3c708dd55 --- /dev/null +++ b/src/Discord.Net.Core/Net/IRequest.cs @@ -0,0 +1,10 @@ +using System; + +namespace Discord.Net +{ + public interface IRequest + { + DateTimeOffset? TimeoutAt { get; } + RequestOptions Options { get; } + } +} diff --git a/src/Discord.Net.Core/Net/RateLimitedException.cs b/src/Discord.Net.Core/Net/RateLimitedException.cs index e8572f911a..2d34d7bc22 100644 --- a/src/Discord.Net.Core/Net/RateLimitedException.cs +++ b/src/Discord.Net.Core/Net/RateLimitedException.cs @@ -1,12 +1,15 @@ -using System; +using System; namespace Discord.Net { public class RateLimitedException : TimeoutException { - public RateLimitedException() + public IRequest Request { get; } + + public RateLimitedException(IRequest request) : base("You are being rate limited.") { + Request = request; } } } diff --git a/src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs b/src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs index 2cc4b8a107..2d96ca7964 100644 --- a/src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs +++ b/src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; #if DEBUG_LIMITS @@ -86,7 +86,7 @@ public async Task SendAsync(RestRequest request) Debug.WriteLine($"[{id}] (!) 502"); #endif if ((request.Options.RetryMode & RetryMode.Retry502) == 0) - throw new HttpException(HttpStatusCode.BadGateway, null); + throw new HttpException(HttpStatusCode.BadGateway, request, null); continue; //Retry default: @@ -106,7 +106,7 @@ public async Task SendAsync(RestRequest request) } catch { } } - throw new HttpException(response.StatusCode, code, reason); + throw new HttpException(response.StatusCode, request, code, reason); } } else @@ -163,7 +163,7 @@ private async Task EnterAsync(int id, RestRequest request) if (!isRateLimited) throw new TimeoutException(); else - throw new RateLimitedException(); + throw new RateLimitedException(request); } lock (_lock) @@ -182,12 +182,12 @@ private async Task EnterAsync(int id, RestRequest request) } if ((request.Options.RetryMode & RetryMode.RetryRatelimit) == 0) - throw new RateLimitedException(); + throw new RateLimitedException(request); if (resetAt.HasValue) { if (resetAt > timeoutAt) - throw new RateLimitedException(); + throw new RateLimitedException(request); int millis = (int)Math.Ceiling((resetAt.Value - DateTimeOffset.UtcNow).TotalMilliseconds); #if DEBUG_LIMITS Debug.WriteLine($"[{id}] Sleeping {millis} ms (Pre-emptive)"); @@ -198,7 +198,7 @@ private async Task EnterAsync(int id, RestRequest request) else { if ((timeoutAt.Value - DateTimeOffset.UtcNow).TotalMilliseconds < 500.0) - throw new RateLimitedException(); + throw new RateLimitedException(request); #if DEBUG_LIMITS Debug.WriteLine($"[{id}] Sleeping 500* ms (Pre-emptive)"); #endif diff --git a/src/Discord.Net.Rest/Net/Queue/Requests/RestRequest.cs b/src/Discord.Net.Rest/Net/Queue/Requests/RestRequest.cs index 8f160273af..bb5840ce29 100644 --- a/src/Discord.Net.Rest/Net/Queue/Requests/RestRequest.cs +++ b/src/Discord.Net.Rest/Net/Queue/Requests/RestRequest.cs @@ -1,11 +1,11 @@ -using Discord.Net.Rest; +using Discord.Net.Rest; using System; using System.IO; using System.Threading.Tasks; namespace Discord.Net.Queue { - public class RestRequest + public class RestRequest : IRequest { public IRestClient Client { get; } public string Method { get; } diff --git a/src/Discord.Net.Rest/Net/Queue/Requests/WebSocketRequest.cs b/src/Discord.Net.Rest/Net/Queue/Requests/WebSocketRequest.cs index 478289b592..81eb40b317 100644 --- a/src/Discord.Net.Rest/Net/Queue/Requests/WebSocketRequest.cs +++ b/src/Discord.Net.Rest/Net/Queue/Requests/WebSocketRequest.cs @@ -1,4 +1,4 @@ -using Discord.Net.WebSockets; +using Discord.Net.WebSockets; using System; using System.IO; using System.Threading; @@ -6,7 +6,7 @@ namespace Discord.Net.Queue { - public class WebSocketRequest + public class WebSocketRequest : IRequest { public IWebSocketClient Client { get; } public string BucketId { get; } From 88765970ec80bcce8cf8dc59fd6812f7005dad50 Mon Sep 17 00:00:00 2001 From: Anu6is Date: Thu, 22 Feb 2018 16:02:47 -0500 Subject: [PATCH 15/48] Incorrect variable assignment (#959) The username parameter was being used to set args.AvatarUrl as opposed to the actual avatarUrl parameter provided --- src/Discord.Net.Webhook/WebhookClientHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.Webhook/WebhookClientHelper.cs b/src/Discord.Net.Webhook/WebhookClientHelper.cs index f3a3984cf1..1116662a69 100644 --- a/src/Discord.Net.Webhook/WebhookClientHelper.cs +++ b/src/Discord.Net.Webhook/WebhookClientHelper.cs @@ -49,7 +49,7 @@ public static async Task SendFileAsync(DiscordWebhookClient client, Strea if (username != null) args.Username = username; if (avatarUrl != null) - args.AvatarUrl = username; + args.AvatarUrl = avatarUrl; if (embeds != null) args.Embeds = embeds.Select(x => x.ToModel()).ToArray(); var msg = await client.ApiClient.UploadWebhookFileAsync(client.Webhook.Id, args, options).ConfigureAwait(false); From fda19b5a8f05b5aa1b37e4149450864501c49bad Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 24 Feb 2018 22:01:28 +0100 Subject: [PATCH 16/48] [docs] Change 'Echos' to 'Echoes' (#964) --- docs/guides/commands/samples/module.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guides/commands/samples/module.cs b/docs/guides/commands/samples/module.cs index 5014619dab..1e35555016 100644 --- a/docs/guides/commands/samples/module.cs +++ b/docs/guides/commands/samples/module.cs @@ -3,7 +3,7 @@ public class Info : ModuleBase { // ~say hello -> hello [Command("say")] - [Summary("Echos a message.")] + [Summary("Echoes a message.")] public async Task SayAsync([Remainder] [Summary("The text to echo")] string echo) { // ReplyAsync is a method on ModuleBase @@ -38,4 +38,4 @@ public async Task UserInfoAsync([Summary("The (optional) user to get info for")] var userInfo = user ?? Context.Client.CurrentUser; await ReplyAsync($"{userInfo.Username}#{userInfo.Discriminator}"); } -} \ No newline at end of file +} From b1eaa44021e334c70fbe08dd9f92baf41968f699 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Mon, 26 Feb 2018 19:32:26 -0500 Subject: [PATCH 17/48] Don't attempt to load types with generic parameters as a module This fixes an issue where custom ModuleBases that contained a generic parameter would be loaded as a module - only to fail when trying to be built. Realistically, ModuleBases _should_ be abstract - but it was still a bug that we allowed them to be included as a module. --- src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs index c0a7e9aca4..cf0f824747 100644 --- a/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs +++ b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs @@ -307,7 +307,8 @@ private static TypeReader GetTypeReader(CommandService service, Type paramType, private static bool IsValidModuleDefinition(TypeInfo typeInfo) { return _moduleTypeInfo.IsAssignableFrom(typeInfo) && - !typeInfo.IsAbstract; + !typeInfo.IsAbstract && + !typeInfo.ContainsGenericParameters; } private static bool IsValidCommandDefinition(MethodInfo methodInfo) From 32ebdd51f77e2c6428059663e21585c737a2e53e Mon Sep 17 00:00:00 2001 From: Finite Reality Date: Wed, 28 Feb 2018 22:46:01 +0000 Subject: [PATCH 18/48] Correct impl. of HasFlag and ResolveChannel (#966) HasFlag was checking if any of the flags were set, not the ones specified, and ResolveChannel was still treating the ChannelPermission enum as before it was changed to a bitflag. --- src/Discord.Net.Core/Utils/Permissions.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Discord.Net.Core/Utils/Permissions.cs b/src/Discord.Net.Core/Utils/Permissions.cs index 7b92c9d3e5..04e6784c32 100644 --- a/src/Discord.Net.Core/Utils/Permissions.cs +++ b/src/Discord.Net.Core/Utils/Permissions.cs @@ -80,7 +80,7 @@ public static void SetValue(ref ulong allow, ref ulong deny, PermValue? value, u } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool HasFlag(ulong value, ulong flag) => (value & flag) != 0; + private static bool HasFlag(ulong value, ulong flag) => (value & flag) == flag; [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void SetFlag(ref ulong value, ulong flag) => value |= flag; [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -161,10 +161,10 @@ public static ulong ResolveChannel(IGuild guild, IGuildUser user, IGuildChannel 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 &= ~(ulong)ChannelPermission.SendTTSMessages; + resolvedPermissions &= ~(ulong)ChannelPermission.MentionEveryone; + resolvedPermissions &= ~(ulong)ChannelPermission.EmbedLinks; + resolvedPermissions &= ~(ulong)ChannelPermission.AttachFiles; } } resolvedPermissions &= mask; //Ensure we didnt get any permissions this channel doesnt support (from guildPerms, for example) @@ -173,4 +173,4 @@ public static ulong ResolveChannel(IGuild guild, IGuildUser user, IGuildChannel return resolvedPermissions; } } -} \ No newline at end of file +} From 63e670464fd9416137d308ac805c0cac7d0aba74 Mon Sep 17 00:00:00 2001 From: Chris Johnston Date: Thu, 1 Mar 2018 17:06:48 -0800 Subject: [PATCH 19/48] Add more tests for Permissions class (#967) * Add tests for more Permissions code coverage * Add guild tests * Add more in-depth covering of permissions methods * Add tests for OverwritePermissions * Remove unknown ItemGroup tag from csproj * Add missing Fact attributes, separate class so that it is not dependant on main test fixture * Separate out GuildPermissions and ChannelPermissions tests from main partial Tests class because they do not need to have access to the main test fixture --- .../Discord.Net.Tests.csproj | 3 + .../Tests.ChannelPermissions.cs | 4 +- .../Tests.GuildPermissions.cs | 4 +- test/Discord.Net.Tests/Tests.Permissions.cs | 706 ++++++++++++++++++ 4 files changed, 713 insertions(+), 4 deletions(-) create mode 100644 test/Discord.Net.Tests/Tests.Permissions.cs diff --git a/test/Discord.Net.Tests/Discord.Net.Tests.csproj b/test/Discord.Net.Tests/Discord.Net.Tests.csproj index bf24571871..204dca5c4f 100644 --- a/test/Discord.Net.Tests/Discord.Net.Tests.csproj +++ b/test/Discord.Net.Tests/Discord.Net.Tests.csproj @@ -10,6 +10,9 @@ PreserveNewest + + + diff --git a/test/Discord.Net.Tests/Tests.ChannelPermissions.cs b/test/Discord.Net.Tests/Tests.ChannelPermissions.cs index ac8ede4e41..b37a1195e4 100644 --- a/test/Discord.Net.Tests/Tests.ChannelPermissions.cs +++ b/test/Discord.Net.Tests/Tests.ChannelPermissions.cs @@ -1,10 +1,10 @@ -using System; +using System; using System.Threading.Tasks; using Xunit; namespace Discord { - public partial class Tests + public class ChannelPermissionsTests { [Fact] public Task TestChannelPermission() diff --git a/test/Discord.Net.Tests/Tests.GuildPermissions.cs b/test/Discord.Net.Tests/Tests.GuildPermissions.cs index bb113d2214..a562f4afba 100644 --- a/test/Discord.Net.Tests/Tests.GuildPermissions.cs +++ b/test/Discord.Net.Tests/Tests.GuildPermissions.cs @@ -1,10 +1,10 @@ -using System; +using System; using System.Threading.Tasks; using Xunit; namespace Discord { - public partial class Tests + public class GuidPermissionsTests { [Fact] public Task TestGuildPermission() diff --git a/test/Discord.Net.Tests/Tests.Permissions.cs b/test/Discord.Net.Tests/Tests.Permissions.cs new file mode 100644 index 0000000000..e22659d15f --- /dev/null +++ b/test/Discord.Net.Tests/Tests.Permissions.cs @@ -0,0 +1,706 @@ +using System; +using System.Threading.Tasks; +using Xunit; + +namespace Discord +{ + public class PermissionsTests + { + private void TestHelper(ChannelPermissions value, ChannelPermission permission, bool expected = false) + => TestHelper(value.RawValue, (ulong)permission, expected); + + private void TestHelper(GuildPermissions value, GuildPermission permission, bool expected = false) + => TestHelper(value.RawValue, (ulong)permission, expected); + + /// + /// Tests the flag of the given permissions value to the expected output + /// and then tries to toggle the flag on and off + /// + /// + /// + /// + private void TestHelper(ulong rawValue, ulong flagValue, bool expected) + { + Assert.Equal(expected, Permissions.GetValue(rawValue, flagValue)); + + // check that toggling the bit works + Permissions.UnsetFlag(ref rawValue, flagValue); + Assert.Equal(false, Permissions.GetValue(rawValue, flagValue)); + Permissions.SetFlag(ref rawValue, flagValue); + Assert.Equal(true, Permissions.GetValue(rawValue, flagValue)); + + // do the same, but with the SetValue method + Permissions.SetValue(ref rawValue, true, flagValue); + Assert.Equal(true, Permissions.GetValue(rawValue, flagValue)); + Permissions.SetValue(ref rawValue, false, flagValue); + Assert.Equal(false, Permissions.GetValue(rawValue, flagValue)); + } + + /// + /// Tests that flag of the given permissions value to be the expected output + /// and then tries cycling through the states of the allow and deny values + /// for that flag + /// + /// + /// + /// + private void TestHelper(OverwritePermissions value, ChannelPermission flag, PermValue expected) + { + // check that the value matches + Assert.Equal(expected, Permissions.GetValue(value.AllowValue, value.DenyValue, flag)); + + // check toggling bits for both allow and deny + // have to make copies to get around read only property + ulong allow = value.AllowValue; + ulong deny = value.DenyValue; + + // both unset should be inherit + Permissions.UnsetFlag(ref allow, (ulong)flag); + Permissions.UnsetFlag(ref deny, (ulong)flag); + Assert.Equal(PermValue.Inherit, Permissions.GetValue(allow, deny, flag)); + + // allow set should be allow + Permissions.SetFlag(ref allow, (ulong)flag); + Permissions.UnsetFlag(ref deny, (ulong)flag); + Assert.Equal(PermValue.Allow, Permissions.GetValue(allow, deny, flag)); + + // deny should be deny + Permissions.UnsetFlag(ref allow, (ulong)flag); + Permissions.SetFlag(ref deny, (ulong)flag); + Assert.Equal(PermValue.Deny, Permissions.GetValue(allow, deny, flag)); + + // allow takes precedence + Permissions.SetFlag(ref allow, (ulong)flag); + Permissions.SetFlag(ref deny, (ulong)flag); + Assert.Equal(PermValue.Allow, Permissions.GetValue(allow, deny, flag)); + } + + /// + /// Tests for the class. + /// + /// Tests that text channel permissions get the right value + /// from the Has method. + /// + /// + [Fact] + public Task TestPermissionsHasChannelPermissionText() + { + var value = ChannelPermissions.Text; + // check that the result of GetValue matches for all properties of text channel + TestHelper(value, ChannelPermission.CreateInstantInvite, true); + TestHelper(value, ChannelPermission.ManageChannels, true); + TestHelper(value, ChannelPermission.AddReactions, true); + TestHelper(value, ChannelPermission.ViewChannel, true); + TestHelper(value, ChannelPermission.SendMessages, true); + TestHelper(value, ChannelPermission.SendTTSMessages, true); + TestHelper(value, ChannelPermission.ManageMessages, true); + TestHelper(value, ChannelPermission.EmbedLinks, true); + TestHelper(value, ChannelPermission.AttachFiles, true); + TestHelper(value, ChannelPermission.ReadMessageHistory, true); + TestHelper(value, ChannelPermission.MentionEveryone, true); + TestHelper(value, ChannelPermission.UseExternalEmojis, true); + TestHelper(value, ChannelPermission.ManageRoles, true); + TestHelper(value, ChannelPermission.ManageWebhooks, true); + + TestHelper(value, ChannelPermission.Connect, false); + TestHelper(value, ChannelPermission.Speak, false); + TestHelper(value, ChannelPermission.MuteMembers, false); + TestHelper(value, ChannelPermission.DeafenMembers, false); + TestHelper(value, ChannelPermission.MoveMembers, false); + TestHelper(value, ChannelPermission.UseVAD, false); + + return Task.CompletedTask; + } + + /// + /// Tests for the class. + /// + /// Tests that no channel permissions get the right value + /// from the Has method. + /// + /// + [Fact] + public Task TestPermissionsHasChannelPermissionNone() + { + // check that none will fail all + var value = ChannelPermissions.None; + + TestHelper(value, ChannelPermission.CreateInstantInvite, false); + TestHelper(value, ChannelPermission.ManageChannels, false); + TestHelper(value, ChannelPermission.AddReactions, false); + TestHelper(value, ChannelPermission.ViewChannel, false); + TestHelper(value, ChannelPermission.SendMessages, false); + TestHelper(value, ChannelPermission.SendTTSMessages, false); + TestHelper(value, ChannelPermission.ManageMessages, false); + TestHelper(value, ChannelPermission.EmbedLinks, false); + TestHelper(value, ChannelPermission.AttachFiles, false); + TestHelper(value, ChannelPermission.ReadMessageHistory, false); + TestHelper(value, ChannelPermission.MentionEveryone, false); + TestHelper(value, ChannelPermission.UseExternalEmojis, false); + TestHelper(value, ChannelPermission.ManageRoles, false); + TestHelper(value, ChannelPermission.ManageWebhooks, false); + TestHelper(value, ChannelPermission.Connect, false); + TestHelper(value, ChannelPermission.Speak, false); + TestHelper(value, ChannelPermission.MuteMembers, false); + TestHelper(value, ChannelPermission.DeafenMembers, false); + TestHelper(value, ChannelPermission.MoveMembers, false); + TestHelper(value, ChannelPermission.UseVAD, false); + + return Task.CompletedTask; + } + + /// + /// Tests for the class. + /// + /// Tests that the dm channel permissions get the right value + /// from the Has method. + /// + /// + [Fact] + public Task TestPermissionsHasChannelPermissionDM() + { + // check that none will fail all + var value = ChannelPermissions.DM; + + TestHelper(value, ChannelPermission.CreateInstantInvite, false); + TestHelper(value, ChannelPermission.ManageChannels, false); + TestHelper(value, ChannelPermission.AddReactions, false); + TestHelper(value, ChannelPermission.ViewChannel, true); + TestHelper(value, ChannelPermission.SendMessages, true); + TestHelper(value, ChannelPermission.SendTTSMessages, false); + TestHelper(value, ChannelPermission.ManageMessages, false); + TestHelper(value, ChannelPermission.EmbedLinks, true); + TestHelper(value, ChannelPermission.AttachFiles, true); + TestHelper(value, ChannelPermission.ReadMessageHistory, true); + TestHelper(value, ChannelPermission.MentionEveryone, false); + TestHelper(value, ChannelPermission.UseExternalEmojis, true); + TestHelper(value, ChannelPermission.ManageRoles, false); + TestHelper(value, ChannelPermission.ManageWebhooks, false); + TestHelper(value, ChannelPermission.Connect, true); + TestHelper(value, ChannelPermission.Speak, true); + TestHelper(value, ChannelPermission.MuteMembers, false); + TestHelper(value, ChannelPermission.DeafenMembers, false); + TestHelper(value, ChannelPermission.MoveMembers, false); + TestHelper(value, ChannelPermission.UseVAD, true); + + return Task.CompletedTask; + } + + /// + /// Tests for the class. + /// + /// Tests that the group channel permissions get the right value + /// from the Has method. + /// + /// + [Fact] + public Task TestPermissionsHasChannelPermissionGroup() + { + var value = ChannelPermissions.Group; + + TestHelper(value, ChannelPermission.CreateInstantInvite, false); + TestHelper(value, ChannelPermission.ManageChannels, false); + TestHelper(value, ChannelPermission.AddReactions, false); + TestHelper(value, ChannelPermission.ViewChannel, false); + TestHelper(value, ChannelPermission.SendMessages, true); + TestHelper(value, ChannelPermission.SendTTSMessages, true); + TestHelper(value, ChannelPermission.ManageMessages, false); + TestHelper(value, ChannelPermission.EmbedLinks, true); + TestHelper(value, ChannelPermission.AttachFiles, true); + TestHelper(value, ChannelPermission.ReadMessageHistory, false); + TestHelper(value, ChannelPermission.MentionEveryone, false); + TestHelper(value, ChannelPermission.UseExternalEmojis, false); + TestHelper(value, ChannelPermission.ManageRoles, false); + TestHelper(value, ChannelPermission.ManageWebhooks, false); + TestHelper(value, ChannelPermission.Connect, true); + TestHelper(value, ChannelPermission.Speak, true); + TestHelper(value, ChannelPermission.MuteMembers, false); + TestHelper(value, ChannelPermission.DeafenMembers, false); + TestHelper(value, ChannelPermission.MoveMembers, false); + TestHelper(value, ChannelPermission.UseVAD, true); + + return Task.CompletedTask; + } + + + /// + /// Tests for the class. + /// + /// Tests that the voice channel permissions get the right value + /// from the Has method. + /// + /// + [Fact] + public Task TestPermissionsHasChannelPermissionVoice() + { + // make a flag with all possible values for Voice channel permissions + var value = ChannelPermissions.Voice; + + TestHelper(value, ChannelPermission.CreateInstantInvite, true); + TestHelper(value, ChannelPermission.ManageChannels, true); + TestHelper(value, ChannelPermission.AddReactions, false); + TestHelper(value, ChannelPermission.ViewChannel, false); + TestHelper(value, ChannelPermission.SendMessages, false); + TestHelper(value, ChannelPermission.SendTTSMessages, false); + TestHelper(value, ChannelPermission.ManageMessages, false); + TestHelper(value, ChannelPermission.EmbedLinks, false); + TestHelper(value, ChannelPermission.AttachFiles, false); + TestHelper(value, ChannelPermission.ReadMessageHistory, false); + TestHelper(value, ChannelPermission.MentionEveryone, false); + TestHelper(value, ChannelPermission.UseExternalEmojis, false); + TestHelper(value, ChannelPermission.ManageRoles, true); + TestHelper(value, ChannelPermission.ManageWebhooks, false); + TestHelper(value, ChannelPermission.Connect, true); + TestHelper(value, ChannelPermission.Speak, true); + TestHelper(value, ChannelPermission.MuteMembers, true); + TestHelper(value, ChannelPermission.DeafenMembers, true); + TestHelper(value, ChannelPermission.MoveMembers, true); + TestHelper(value, ChannelPermission.UseVAD, true); + + return Task.CompletedTask; + } + + /// + /// Tests for the class. + /// + /// Test that that the Has method of + /// returns the correct value when no permissions are set. + /// + /// + [Fact] + public Task TestPermissionsHasGuildPermissionNone() + { + var value = GuildPermissions.None; + + TestHelper(value, GuildPermission.CreateInstantInvite, false); + TestHelper(value, GuildPermission.KickMembers, false); + TestHelper(value, GuildPermission.BanMembers, false); + TestHelper(value, GuildPermission.Administrator, false); + TestHelper(value, GuildPermission.ManageChannels, false); + TestHelper(value, GuildPermission.ManageGuild, false); + TestHelper(value, GuildPermission.AddReactions, false); + TestHelper(value, GuildPermission.ViewAuditLog, false); + TestHelper(value, GuildPermission.ReadMessages, false); + TestHelper(value, GuildPermission.SendMessages, false); + TestHelper(value, GuildPermission.SendTTSMessages, false); + TestHelper(value, GuildPermission.ManageMessages, false); + TestHelper(value, GuildPermission.EmbedLinks, false); + TestHelper(value, GuildPermission.AttachFiles, false); + TestHelper(value, GuildPermission.ReadMessageHistory, false); + TestHelper(value, GuildPermission.MentionEveryone, false); + TestHelper(value, GuildPermission.UseExternalEmojis, false); + TestHelper(value, GuildPermission.Connect, false); + TestHelper(value, GuildPermission.Speak, false); + TestHelper(value, GuildPermission.MuteMembers, false); + TestHelper(value, GuildPermission.MoveMembers, false); + TestHelper(value, GuildPermission.UseVAD, false); + TestHelper(value, GuildPermission.ChangeNickname, false); + TestHelper(value, GuildPermission.ManageNicknames, false); + TestHelper(value, GuildPermission.ManageRoles, false); + TestHelper(value, GuildPermission.ManageWebhooks, false); + TestHelper(value, GuildPermission.ManageEmojis, false); + + return Task.CompletedTask; + } + + /// + /// Tests for the class. + /// + /// Test that that the Has method of + /// returns the correct value when all permissions are set. + /// + /// + [Fact] + public Task TestPermissionsHasGuildPermissionAll() + { + var value = GuildPermissions.All; + + TestHelper(value, GuildPermission.CreateInstantInvite, true); + TestHelper(value, GuildPermission.KickMembers, true); + TestHelper(value, GuildPermission.BanMembers, true); + TestHelper(value, GuildPermission.Administrator, true); + TestHelper(value, GuildPermission.ManageChannels, true); + TestHelper(value, GuildPermission.ManageGuild, true); + TestHelper(value, GuildPermission.AddReactions, true); + TestHelper(value, GuildPermission.ViewAuditLog, true); + TestHelper(value, GuildPermission.ReadMessages, true); + TestHelper(value, GuildPermission.SendMessages, true); + TestHelper(value, GuildPermission.SendTTSMessages, true); + TestHelper(value, GuildPermission.ManageMessages, true); + TestHelper(value, GuildPermission.EmbedLinks, true); + TestHelper(value, GuildPermission.AttachFiles, true); + TestHelper(value, GuildPermission.ReadMessageHistory, true); + TestHelper(value, GuildPermission.MentionEveryone, true); + TestHelper(value, GuildPermission.UseExternalEmojis, true); + TestHelper(value, GuildPermission.Connect, true); + TestHelper(value, GuildPermission.Speak, true); + TestHelper(value, GuildPermission.MuteMembers, true); + TestHelper(value, GuildPermission.MoveMembers, true); + TestHelper(value, GuildPermission.UseVAD, true); + TestHelper(value, GuildPermission.ChangeNickname, true); + TestHelper(value, GuildPermission.ManageNicknames, true); + TestHelper(value, GuildPermission.ManageRoles, true); + TestHelper(value, GuildPermission.ManageWebhooks, true); + TestHelper(value, GuildPermission.ManageEmojis, true); + + + return Task.CompletedTask; + } + + /// + /// Tests for the class. + /// + /// Test that that the Has method of + /// returns the correct value when webhook permissions are set. + /// + /// + [Fact] + public Task TestPermissionsHasGuildPermissionWebhook() + { + var value = GuildPermissions.Webhook; + + TestHelper(value, GuildPermission.CreateInstantInvite, false); + TestHelper(value, GuildPermission.KickMembers, false); + TestHelper(value, GuildPermission.BanMembers, false); + TestHelper(value, GuildPermission.Administrator, false); + TestHelper(value, GuildPermission.ManageChannels, false); + TestHelper(value, GuildPermission.ManageGuild, false); + TestHelper(value, GuildPermission.AddReactions, false); + TestHelper(value, GuildPermission.ViewAuditLog, false); + TestHelper(value, GuildPermission.ReadMessages, false); + TestHelper(value, GuildPermission.SendMessages, true); + TestHelper(value, GuildPermission.SendTTSMessages, true); + TestHelper(value, GuildPermission.ManageMessages, false); + TestHelper(value, GuildPermission.EmbedLinks, true); + TestHelper(value, GuildPermission.AttachFiles, true); + TestHelper(value, GuildPermission.ReadMessageHistory, false); + TestHelper(value, GuildPermission.MentionEveryone, false); + TestHelper(value, GuildPermission.UseExternalEmojis, false); + TestHelper(value, GuildPermission.Connect, false); + TestHelper(value, GuildPermission.Speak, false); + TestHelper(value, GuildPermission.MuteMembers, false); + TestHelper(value, GuildPermission.MoveMembers, false); + TestHelper(value, GuildPermission.UseVAD, false); + TestHelper(value, GuildPermission.ChangeNickname, false); + TestHelper(value, GuildPermission.ManageNicknames, false); + TestHelper(value, GuildPermission.ManageRoles, false); + TestHelper(value, GuildPermission.ManageWebhooks, false); + TestHelper(value, GuildPermission.ManageEmojis, false); + + return Task.CompletedTask; + } + + /// + /// Test + /// for when all text permissions are allowed and denied + /// + /// + [Fact] + public Task TestOverwritePermissionsText() + { + // allow all for text channel + var value = new OverwritePermissions(ChannelPermissions.Text.RawValue, ChannelPermissions.None.RawValue); + + TestHelper(value, ChannelPermission.CreateInstantInvite, PermValue.Allow); + TestHelper(value, ChannelPermission.ManageChannels, PermValue.Allow); + TestHelper(value, ChannelPermission.AddReactions, PermValue.Allow); + TestHelper(value, ChannelPermission.ViewChannel, PermValue.Allow); + TestHelper(value, ChannelPermission.SendMessages, PermValue.Allow); + TestHelper(value, ChannelPermission.SendTTSMessages, PermValue.Allow); + TestHelper(value, ChannelPermission.ManageMessages, PermValue.Allow); + TestHelper(value, ChannelPermission.EmbedLinks, PermValue.Allow); + TestHelper(value, ChannelPermission.AttachFiles, PermValue.Allow); + TestHelper(value, ChannelPermission.ReadMessageHistory, PermValue.Allow); + TestHelper(value, ChannelPermission.MentionEveryone, PermValue.Allow); + TestHelper(value, ChannelPermission.UseExternalEmojis, PermValue.Allow); + TestHelper(value, ChannelPermission.ManageRoles, PermValue.Allow); + TestHelper(value, ChannelPermission.ManageWebhooks, PermValue.Allow); + TestHelper(value, ChannelPermission.Connect, PermValue.Inherit); + TestHelper(value, ChannelPermission.Speak, PermValue.Inherit); + TestHelper(value, ChannelPermission.MuteMembers, PermValue.Inherit); + TestHelper(value, ChannelPermission.DeafenMembers, PermValue.Inherit); + TestHelper(value, ChannelPermission.MoveMembers, PermValue.Inherit); + TestHelper(value, ChannelPermission.UseVAD, PermValue.Inherit); + + value = new OverwritePermissions(ChannelPermissions.None.RawValue, ChannelPermissions.Text.RawValue); + + TestHelper(value, ChannelPermission.CreateInstantInvite, PermValue.Deny); + TestHelper(value, ChannelPermission.ManageChannels, PermValue.Deny); + TestHelper(value, ChannelPermission.AddReactions, PermValue.Deny); + TestHelper(value, ChannelPermission.ViewChannel, PermValue.Deny); + TestHelper(value, ChannelPermission.SendMessages, PermValue.Deny); + TestHelper(value, ChannelPermission.SendTTSMessages, PermValue.Deny); + TestHelper(value, ChannelPermission.ManageMessages, PermValue.Deny); + TestHelper(value, ChannelPermission.EmbedLinks, PermValue.Deny); + TestHelper(value, ChannelPermission.AttachFiles, PermValue.Deny); + TestHelper(value, ChannelPermission.ReadMessageHistory, PermValue.Deny); + TestHelper(value, ChannelPermission.MentionEveryone, PermValue.Deny); + TestHelper(value, ChannelPermission.UseExternalEmojis, PermValue.Deny); + TestHelper(value, ChannelPermission.ManageRoles, PermValue.Deny); + TestHelper(value, ChannelPermission.ManageWebhooks, PermValue.Deny); + TestHelper(value, ChannelPermission.Connect, PermValue.Inherit); + TestHelper(value, ChannelPermission.Speak, PermValue.Inherit); + TestHelper(value, ChannelPermission.MuteMembers, PermValue.Inherit); + TestHelper(value, ChannelPermission.DeafenMembers, PermValue.Inherit); + TestHelper(value, ChannelPermission.MoveMembers, PermValue.Inherit); + TestHelper(value, ChannelPermission.UseVAD, PermValue.Inherit); + + return Task.CompletedTask; + } + + /// + /// Test + /// for when none of the permissions are set. + /// + /// + [Fact] + public Task TestOverwritePermissionsNone() + { + // allow all for text channel + var value = new OverwritePermissions(ChannelPermissions.None.RawValue, ChannelPermissions.None.RawValue); + + TestHelper(value, ChannelPermission.CreateInstantInvite, PermValue.Inherit); + TestHelper(value, ChannelPermission.ManageChannels, PermValue.Inherit); + TestHelper(value, ChannelPermission.AddReactions, PermValue.Inherit); + TestHelper(value, ChannelPermission.ViewChannel, PermValue.Inherit); + TestHelper(value, ChannelPermission.SendMessages, PermValue.Inherit); + TestHelper(value, ChannelPermission.SendTTSMessages, PermValue.Inherit); + TestHelper(value, ChannelPermission.ManageMessages, PermValue.Inherit); + TestHelper(value, ChannelPermission.EmbedLinks, PermValue.Inherit); + TestHelper(value, ChannelPermission.AttachFiles, PermValue.Inherit); + TestHelper(value, ChannelPermission.ReadMessageHistory, PermValue.Inherit); + TestHelper(value, ChannelPermission.MentionEveryone, PermValue.Inherit); + TestHelper(value, ChannelPermission.UseExternalEmojis, PermValue.Inherit); + TestHelper(value, ChannelPermission.ManageRoles, PermValue.Inherit); + TestHelper(value, ChannelPermission.ManageWebhooks, PermValue.Inherit); + TestHelper(value, ChannelPermission.Connect, PermValue.Inherit); + TestHelper(value, ChannelPermission.Speak, PermValue.Inherit); + TestHelper(value, ChannelPermission.MuteMembers, PermValue.Inherit); + TestHelper(value, ChannelPermission.DeafenMembers, PermValue.Inherit); + TestHelper(value, ChannelPermission.MoveMembers, PermValue.Inherit); + TestHelper(value, ChannelPermission.UseVAD, PermValue.Inherit); + + value = new OverwritePermissions(); + + TestHelper(value, ChannelPermission.CreateInstantInvite, PermValue.Inherit); + TestHelper(value, ChannelPermission.ManageChannels, PermValue.Inherit); + TestHelper(value, ChannelPermission.AddReactions, PermValue.Inherit); + TestHelper(value, ChannelPermission.ViewChannel, PermValue.Inherit); + TestHelper(value, ChannelPermission.SendMessages, PermValue.Inherit); + TestHelper(value, ChannelPermission.SendTTSMessages, PermValue.Inherit); + TestHelper(value, ChannelPermission.ManageMessages, PermValue.Inherit); + TestHelper(value, ChannelPermission.EmbedLinks, PermValue.Inherit); + TestHelper(value, ChannelPermission.AttachFiles, PermValue.Inherit); + TestHelper(value, ChannelPermission.ReadMessageHistory, PermValue.Inherit); + TestHelper(value, ChannelPermission.MentionEveryone, PermValue.Inherit); + TestHelper(value, ChannelPermission.UseExternalEmojis, PermValue.Inherit); + TestHelper(value, ChannelPermission.ManageRoles, PermValue.Inherit); + TestHelper(value, ChannelPermission.ManageWebhooks, PermValue.Inherit); + TestHelper(value, ChannelPermission.Connect, PermValue.Inherit); + TestHelper(value, ChannelPermission.Speak, PermValue.Inherit); + TestHelper(value, ChannelPermission.MuteMembers, PermValue.Inherit); + TestHelper(value, ChannelPermission.DeafenMembers, PermValue.Inherit); + TestHelper(value, ChannelPermission.MoveMembers, PermValue.Inherit); + TestHelper(value, ChannelPermission.UseVAD, PermValue.Inherit); + + value = OverwritePermissions.InheritAll; + + TestHelper(value, ChannelPermission.CreateInstantInvite, PermValue.Inherit); + TestHelper(value, ChannelPermission.ManageChannels, PermValue.Inherit); + TestHelper(value, ChannelPermission.AddReactions, PermValue.Inherit); + TestHelper(value, ChannelPermission.ViewChannel, PermValue.Inherit); + TestHelper(value, ChannelPermission.SendMessages, PermValue.Inherit); + TestHelper(value, ChannelPermission.SendTTSMessages, PermValue.Inherit); + TestHelper(value, ChannelPermission.ManageMessages, PermValue.Inherit); + TestHelper(value, ChannelPermission.EmbedLinks, PermValue.Inherit); + TestHelper(value, ChannelPermission.AttachFiles, PermValue.Inherit); + TestHelper(value, ChannelPermission.ReadMessageHistory, PermValue.Inherit); + TestHelper(value, ChannelPermission.MentionEveryone, PermValue.Inherit); + TestHelper(value, ChannelPermission.UseExternalEmojis, PermValue.Inherit); + TestHelper(value, ChannelPermission.ManageRoles, PermValue.Inherit); + TestHelper(value, ChannelPermission.ManageWebhooks, PermValue.Inherit); + TestHelper(value, ChannelPermission.Connect, PermValue.Inherit); + TestHelper(value, ChannelPermission.Speak, PermValue.Inherit); + TestHelper(value, ChannelPermission.MuteMembers, PermValue.Inherit); + TestHelper(value, ChannelPermission.DeafenMembers, PermValue.Inherit); + TestHelper(value, ChannelPermission.MoveMembers, PermValue.Inherit); + TestHelper(value, ChannelPermission.UseVAD, PermValue.Inherit); + + return Task.CompletedTask; + } + + /// + /// Test + /// for when all dm permissions are allowed and denied + /// + /// + [Fact] + public Task TestOverwritePermissionsDM() + { + // allow all for text channel + var value = new OverwritePermissions(ChannelPermissions.DM.RawValue, ChannelPermissions.None.RawValue); + + TestHelper(value, ChannelPermission.CreateInstantInvite, PermValue.Inherit); + TestHelper(value, ChannelPermission.ManageChannels, PermValue.Inherit); + TestHelper(value, ChannelPermission.AddReactions, PermValue.Inherit); + TestHelper(value, ChannelPermission.ViewChannel, PermValue.Allow); + TestHelper(value, ChannelPermission.SendMessages, PermValue.Allow); + TestHelper(value, ChannelPermission.SendTTSMessages, PermValue.Inherit); + TestHelper(value, ChannelPermission.ManageMessages, PermValue.Inherit); + TestHelper(value, ChannelPermission.EmbedLinks, PermValue.Allow); + TestHelper(value, ChannelPermission.AttachFiles, PermValue.Allow); + TestHelper(value, ChannelPermission.ReadMessageHistory, PermValue.Allow); + TestHelper(value, ChannelPermission.MentionEveryone, PermValue.Inherit); + TestHelper(value, ChannelPermission.UseExternalEmojis, PermValue.Allow); + TestHelper(value, ChannelPermission.ManageRoles, PermValue.Inherit); + TestHelper(value, ChannelPermission.ManageWebhooks, PermValue.Inherit); + TestHelper(value, ChannelPermission.Connect, PermValue.Allow); + TestHelper(value, ChannelPermission.Speak, PermValue.Allow); + TestHelper(value, ChannelPermission.MuteMembers, PermValue.Inherit); + TestHelper(value, ChannelPermission.DeafenMembers, PermValue.Inherit); + TestHelper(value, ChannelPermission.MoveMembers, PermValue.Inherit); + TestHelper(value, ChannelPermission.UseVAD, PermValue.Allow); + + value = new OverwritePermissions(ChannelPermissions.None.RawValue, ChannelPermissions.DM.RawValue); + + TestHelper(value, ChannelPermission.CreateInstantInvite, PermValue.Inherit); + TestHelper(value, ChannelPermission.ManageChannels, PermValue.Inherit); + TestHelper(value, ChannelPermission.AddReactions, PermValue.Inherit); + TestHelper(value, ChannelPermission.ViewChannel, PermValue.Deny); + TestHelper(value, ChannelPermission.SendMessages, PermValue.Deny); + TestHelper(value, ChannelPermission.SendTTSMessages, PermValue.Inherit); + TestHelper(value, ChannelPermission.ManageMessages, PermValue.Inherit); + TestHelper(value, ChannelPermission.EmbedLinks, PermValue.Deny); + TestHelper(value, ChannelPermission.AttachFiles, PermValue.Deny); + TestHelper(value, ChannelPermission.ReadMessageHistory, PermValue.Deny); + TestHelper(value, ChannelPermission.MentionEveryone, PermValue.Inherit); + TestHelper(value, ChannelPermission.UseExternalEmojis, PermValue.Deny); + TestHelper(value, ChannelPermission.ManageRoles, PermValue.Inherit); + TestHelper(value, ChannelPermission.ManageWebhooks, PermValue.Inherit); + TestHelper(value, ChannelPermission.Connect, PermValue.Deny); + TestHelper(value, ChannelPermission.Speak, PermValue.Deny); + TestHelper(value, ChannelPermission.MuteMembers, PermValue.Inherit); + TestHelper(value, ChannelPermission.DeafenMembers, PermValue.Inherit); + TestHelper(value, ChannelPermission.MoveMembers, PermValue.Inherit); + TestHelper(value, ChannelPermission.UseVAD, PermValue.Deny); + + return Task.CompletedTask; + } + + /// + /// Test + /// for when all group permissions are allowed and denied + /// + /// + [Fact] + public Task TestOverwritePermissionsGroup() + { + // allow all for group channels + var value = new OverwritePermissions(ChannelPermissions.Group.RawValue, ChannelPermissions.None.RawValue); + + TestHelper(value, ChannelPermission.CreateInstantInvite, PermValue.Inherit); + TestHelper(value, ChannelPermission.ManageChannels, PermValue.Inherit); + TestHelper(value, ChannelPermission.AddReactions, PermValue.Inherit); + TestHelper(value, ChannelPermission.ViewChannel, PermValue.Inherit); + TestHelper(value, ChannelPermission.SendMessages, PermValue.Allow); + TestHelper(value, ChannelPermission.SendTTSMessages, PermValue.Allow); + TestHelper(value, ChannelPermission.ManageMessages, PermValue.Inherit); + TestHelper(value, ChannelPermission.EmbedLinks, PermValue.Allow); + TestHelper(value, ChannelPermission.AttachFiles, PermValue.Allow); + TestHelper(value, ChannelPermission.ReadMessageHistory, PermValue.Inherit); + TestHelper(value, ChannelPermission.MentionEveryone, PermValue.Inherit); + TestHelper(value, ChannelPermission.UseExternalEmojis, PermValue.Inherit); + TestHelper(value, ChannelPermission.ManageRoles, PermValue.Inherit); + TestHelper(value, ChannelPermission.ManageWebhooks, PermValue.Inherit); + TestHelper(value, ChannelPermission.Connect, PermValue.Allow); + TestHelper(value, ChannelPermission.Speak, PermValue.Allow); + TestHelper(value, ChannelPermission.MuteMembers, PermValue.Inherit); + TestHelper(value, ChannelPermission.DeafenMembers, PermValue.Inherit); + TestHelper(value, ChannelPermission.MoveMembers, PermValue.Inherit); + TestHelper(value, ChannelPermission.UseVAD, PermValue.Allow); + + value = new OverwritePermissions(ChannelPermissions.None.RawValue, ChannelPermissions.Group.RawValue); + + TestHelper(value, ChannelPermission.CreateInstantInvite, PermValue.Inherit); + TestHelper(value, ChannelPermission.ManageChannels, PermValue.Inherit); + TestHelper(value, ChannelPermission.AddReactions, PermValue.Inherit); + TestHelper(value, ChannelPermission.ViewChannel, PermValue.Inherit); + TestHelper(value, ChannelPermission.SendMessages, PermValue.Deny); + TestHelper(value, ChannelPermission.SendTTSMessages, PermValue.Deny); + TestHelper(value, ChannelPermission.ManageMessages, PermValue.Inherit); + TestHelper(value, ChannelPermission.EmbedLinks, PermValue.Deny); + TestHelper(value, ChannelPermission.AttachFiles, PermValue.Deny); + TestHelper(value, ChannelPermission.ReadMessageHistory, PermValue.Inherit); + TestHelper(value, ChannelPermission.MentionEveryone, PermValue.Inherit); + TestHelper(value, ChannelPermission.UseExternalEmojis, PermValue.Inherit); + TestHelper(value, ChannelPermission.ManageRoles, PermValue.Inherit); + TestHelper(value, ChannelPermission.ManageWebhooks, PermValue.Inherit); + TestHelper(value, ChannelPermission.Connect, PermValue.Deny); + TestHelper(value, ChannelPermission.Speak, PermValue.Deny); + TestHelper(value, ChannelPermission.MuteMembers, PermValue.Inherit); + TestHelper(value, ChannelPermission.DeafenMembers, PermValue.Inherit); + TestHelper(value, ChannelPermission.MoveMembers, PermValue.Inherit); + TestHelper(value, ChannelPermission.UseVAD, PermValue.Deny); + + return Task.CompletedTask; + } + + /// + /// Test + /// for when all group permissions are allowed and denied + /// + /// + [Fact] + public Task TestOverwritePermissionsVoice() + { + // allow all for group channels + var value = new OverwritePermissions(ChannelPermissions.Voice.RawValue, ChannelPermissions.None.RawValue); + + TestHelper(value, ChannelPermission.CreateInstantInvite, PermValue.Allow); + TestHelper(value, ChannelPermission.ManageChannels, PermValue.Allow); + TestHelper(value, ChannelPermission.AddReactions, PermValue.Inherit); + TestHelper(value, ChannelPermission.ViewChannel, PermValue.Inherit); + TestHelper(value, ChannelPermission.SendMessages, PermValue.Inherit); + TestHelper(value, ChannelPermission.SendTTSMessages, PermValue.Inherit); + TestHelper(value, ChannelPermission.ManageMessages, PermValue.Inherit); + TestHelper(value, ChannelPermission.EmbedLinks, PermValue.Inherit); + TestHelper(value, ChannelPermission.AttachFiles, PermValue.Inherit); + TestHelper(value, ChannelPermission.ReadMessageHistory, PermValue.Inherit); + TestHelper(value, ChannelPermission.MentionEveryone, PermValue.Inherit); + TestHelper(value, ChannelPermission.UseExternalEmojis, PermValue.Inherit); + TestHelper(value, ChannelPermission.ManageRoles, PermValue.Allow); + TestHelper(value, ChannelPermission.ManageWebhooks, PermValue.Inherit); + TestHelper(value, ChannelPermission.Connect, PermValue.Allow); + TestHelper(value, ChannelPermission.Speak, PermValue.Allow); + TestHelper(value, ChannelPermission.MuteMembers, PermValue.Allow); + TestHelper(value, ChannelPermission.DeafenMembers, PermValue.Allow); + TestHelper(value, ChannelPermission.MoveMembers, PermValue.Allow); + TestHelper(value, ChannelPermission.UseVAD, PermValue.Allow); + + value = new OverwritePermissions(ChannelPermissions.None.RawValue, ChannelPermissions.Voice.RawValue); + + TestHelper(value, ChannelPermission.CreateInstantInvite, PermValue.Deny); + TestHelper(value, ChannelPermission.ManageChannels, PermValue.Deny); + TestHelper(value, ChannelPermission.AddReactions, PermValue.Inherit); + TestHelper(value, ChannelPermission.ViewChannel, PermValue.Inherit); + TestHelper(value, ChannelPermission.SendMessages, PermValue.Inherit); + TestHelper(value, ChannelPermission.SendTTSMessages, PermValue.Inherit); + TestHelper(value, ChannelPermission.ManageMessages, PermValue.Inherit); + TestHelper(value, ChannelPermission.EmbedLinks, PermValue.Inherit); + TestHelper(value, ChannelPermission.AttachFiles, PermValue.Inherit); + TestHelper(value, ChannelPermission.ReadMessageHistory, PermValue.Inherit); + TestHelper(value, ChannelPermission.MentionEveryone, PermValue.Inherit); + TestHelper(value, ChannelPermission.UseExternalEmojis, PermValue.Inherit); + TestHelper(value, ChannelPermission.ManageRoles, PermValue.Deny); + TestHelper(value, ChannelPermission.ManageWebhooks, PermValue.Inherit); + TestHelper(value, ChannelPermission.Connect, PermValue.Deny); + TestHelper(value, ChannelPermission.Speak, PermValue.Deny); + TestHelper(value, ChannelPermission.MuteMembers, PermValue.Deny); + TestHelper(value, ChannelPermission.DeafenMembers, PermValue.Deny); + TestHelper(value, ChannelPermission.MoveMembers, PermValue.Deny); + TestHelper(value, ChannelPermission.UseVAD, PermValue.Deny); + + return Task.CompletedTask; + } + } +} From f19730e43385c2e07b19b9677a67d061b51661be Mon Sep 17 00:00:00 2001 From: Christopher F Date: Sat, 3 Mar 2018 20:36:06 -0500 Subject: [PATCH 20/48] AddModuleAsync/AddModulesAsync should not require an IServiceProvider If one is not passed, they will default to an EmptyServiceProvider This resolves issues where since the merging of OnModuleBuilding, users were effectively forced to use the DI pattern, where before they were not. While this isn't necessarily a bad idea, we shouldn't just change this behavior for no reason (though I assume making the IServiceProvider argument required was a mistake) --- src/Discord.Net.Commands/CommandService.cs | 10 +++++++--- src/Discord.Net.Commands/Utilities/ReflectionUtils.cs | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index 7efc1bc625..d79a3a03b9 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -95,9 +95,11 @@ public async Task CreateModuleAsync(string primaryAlias, Action AddModuleAsync(IServiceProvider services) => AddModuleAsync(typeof(T), services); - public async Task AddModuleAsync(Type type, IServiceProvider services) + public Task AddModuleAsync(IServiceProvider services = null) => AddModuleAsync(typeof(T), services); + public async Task AddModuleAsync(Type type, IServiceProvider services = null) { + services = services ?? EmptyServiceProvider.Instance; + await _moduleLock.WaitAsync().ConfigureAwait(false); try { @@ -120,8 +122,10 @@ public async Task AddModuleAsync(Type type, IServiceProvider service _moduleLock.Release(); } } - public async Task> AddModulesAsync(Assembly assembly, IServiceProvider services) + public async Task> AddModulesAsync(Assembly assembly, IServiceProvider services = null) { + services = services ?? EmptyServiceProvider.Instance; + await _moduleLock.WaitAsync().ConfigureAwait(false); try { diff --git a/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs b/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs index ab88f66ae0..30dd7c36b4 100644 --- a/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs +++ b/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Reflection; From 4edbd8d4b9cc46a48dbf02ad4bbd04fccc67ea27 Mon Sep 17 00:00:00 2001 From: Alex Gravely Date: Sun, 4 Mar 2018 13:15:00 -0500 Subject: [PATCH 21/48] Allow nested ModuleBase classes to be built when declared from non-module classes. (#969) * Allow modules to be built regardless of their declaring type. * Need to exclude submodules. --- src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs index cf0f824747..996706a7c9 100644 --- a/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs +++ b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs @@ -48,8 +48,7 @@ public static async Task> BuildAsync(IEnumerable x.DeclaringType == null); - var subGroups = validTypes.Intersect(topLevelGroups); + var topLevelGroups = validTypes.Where(x => x.DeclaringType == null || !IsValidModuleDefinition(x.DeclaringType.GetTypeInfo())); var builtTypes = new List(); From 8537924d9b48f4a5942fff2c8d9595456e15fb5b Mon Sep 17 00:00:00 2001 From: Michael Flaherty Date: Sun, 4 Mar 2018 10:24:09 -0800 Subject: [PATCH 22/48] Add Missing Parameter For WebSocket4Net Constructor (#968) * Add missing param to constructor This should fix `15:02:44 Gateway System.MissingMethodException: Method not found: 'Void WebSocket4Net.WebSocket..ctor(System.String, System.String, System.Collections.Generic.List`1>, System.Collections.Generic.List`1>, System.String, System.String, WebSocket4Net.WebSocketVersion, System.Net.EndPoint)'.` * Bump WS4N to latest stable --- .../Discord.Net.Providers.WS4Net.csproj | 2 +- src/Discord.Net.Providers.WS4Net/WS4NetClient.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) 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 78987e739d..bfd0983ceb 100644 --- a/src/Discord.Net.Providers.WS4Net/Discord.Net.Providers.WS4Net.csproj +++ b/src/Discord.Net.Providers.WS4Net/Discord.Net.Providers.WS4Net.csproj @@ -10,6 +10,6 @@ - + diff --git a/src/Discord.Net.Providers.WS4Net/WS4NetClient.cs b/src/Discord.Net.Providers.WS4Net/WS4NetClient.cs index 93d6a83d6d..1894a89068 100644 --- a/src/Discord.Net.Providers.WS4Net/WS4NetClient.cs +++ b/src/Discord.Net.Providers.WS4Net/WS4NetClient.cs @@ -66,7 +66,7 @@ private async Task ConnectInternalAsync(string host) _cancelTokenSource = new CancellationTokenSource(); _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _cancelTokenSource.Token).Token; - _client = new WS4NetSocket(host, customHeaderItems: _headers.ToList()) + _client = new WS4NetSocket(host, "", customHeaderItems: _headers.ToList()) { EnableAutoSendPing = false, NoDelay = true, @@ -163,4 +163,4 @@ private void OnClosed(object sender, object e) Closed(ex).GetAwaiter().GetResult(); } } -} \ No newline at end of file +} From 170a2e00bdba9bdb2370040ee6f3015d05a11a0a Mon Sep 17 00:00:00 2001 From: Joe4evr Date: Thu, 8 Mar 2018 22:09:34 +0100 Subject: [PATCH 23/48] Resolve #936 (#941) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Resolve #936 * Remember to not search *only* typereaders for primitives 😒 --- .../Builders/ModuleClassBuilder.cs | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs index 996706a7c9..e9ce9eb862 100644 --- a/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs +++ b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs @@ -274,15 +274,8 @@ private static void BuildParameter(ParameterBuilder builder, System.Reflection.P if (builder.TypeReader == null) { - var readers = service.GetTypeReaders(paramType); - TypeReader reader = null; - - if (readers != null) - reader = readers.FirstOrDefault().Value; - else - reader = service.GetDefaultTypeReader(paramType); - - builder.TypeReader = reader; + builder.TypeReader = service.GetDefaultTypeReader(paramType) + ?? service.GetTypeReaders(paramType)?.FirstOrDefault().Value; } } From 2fd4f5670edf9e7db72bb71f19efd1e1502b015e Mon Sep 17 00:00:00 2001 From: Joe4evr Date: Mon, 12 Mar 2018 18:59:22 +0100 Subject: [PATCH 24/48] Remove support for TokenType.User (#958) * Set usage of TokenType.User as an error rather than a warning. * Remove commented sections and #pragma's Additionally, changes use of ReadMessages to ViewChannel since that Obsolete was also suppressed by the pragma --- .../Preconditions/RequireOwnerAttribute.cs | 5 ----- src/Discord.Net.Core/TokenType.cs | 4 ++-- src/Discord.Net.Rest/DiscordRestApiClient.cs | 22 +++++-------------- .../DiscordSocketClient.cs | 8 +------ .../Entities/Guilds/SocketGuild.cs | 12 ++++------ 5 files changed, 13 insertions(+), 38 deletions(-) diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs index 7a8a009be2..93e3cbe181 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS0618 using System; using System.Threading.Tasks; @@ -20,10 +19,6 @@ public override async Task CheckPermissionsAsync(ICommandCon 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/TokenType.cs b/src/Discord.Net.Core/TokenType.cs index c351b1c193..62181420a8 100644 --- a/src/Discord.Net.Core/TokenType.cs +++ b/src/Discord.Net.Core/TokenType.cs @@ -1,10 +1,10 @@ -using System; +using System; namespace Discord { public enum TokenType { - [Obsolete("User logins are being deprecated and may result in a ToS strike against your account - please see https://github.com/RogueException/Discord.Net/issues/827")] + [Obsolete("User logins are deprecated and may result in a ToS strike against your account - please see https://github.com/RogueException/Discord.Net/issues/827", error: true)] User, Bearer, Bot, diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index 689cba9c3d..c93ed628b0 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable CS0618 using Discord.API.Rest; using Discord.Net; using Discord.Net.Converters; @@ -74,8 +73,6 @@ internal static string GetPrefixedToken(TokenType tokenType, string token) return $"Bot {token}"; case TokenType.Bearer: return $"Bearer {token}"; - case TokenType.User: - return token; default: throw new ArgumentException("Unknown OAuth token type", nameof(tokenType)); } @@ -113,7 +110,6 @@ private async Task LoginInternalAsync(TokenType tokenType, string token, Request { _loginCancelToken = new CancellationTokenSource(); - AuthTokenType = TokenType.User; AuthToken = null; await RequestQueue.SetCancelTokenAsync(_loginCancelToken.Token).ConfigureAwait(false); RestClient.SetCancelToken(_loginCancelToken.Token); @@ -172,8 +168,7 @@ public async Task SendAsync(string method, string endpoint, { options = options ?? new RequestOptions(); options.HeaderOnly = true; - options.BucketId = AuthTokenType == TokenType.User ? ClientBucket.Get(clientBucket).Id : bucketId; - options.IsClientBucket = AuthTokenType == TokenType.User; + options.BucketId = bucketId; var request = new RestRequest(RestClient, method, endpoint, options); await SendInternalAsync(method, endpoint, request).ConfigureAwait(false); @@ -187,8 +182,7 @@ public async Task SendJsonAsync(string method, string endpoint, object payload, { options = options ?? new RequestOptions(); options.HeaderOnly = true; - options.BucketId = AuthTokenType == TokenType.User ? ClientBucket.Get(clientBucket).Id : bucketId; - options.IsClientBucket = AuthTokenType == TokenType.User; + options.BucketId = bucketId; string json = payload != null ? SerializeJson(payload) : null; var request = new JsonRestRequest(RestClient, method, endpoint, json, options); @@ -203,8 +197,7 @@ public async Task SendMultipartAsync(string method, string endpoint, IReadOnlyDi { options = options ?? new RequestOptions(); options.HeaderOnly = true; - options.BucketId = AuthTokenType == TokenType.User ? ClientBucket.Get(clientBucket).Id : bucketId; - options.IsClientBucket = AuthTokenType == TokenType.User; + options.BucketId = bucketId; var request = new MultipartRestRequest(RestClient, method, endpoint, multipartArgs, options); await SendInternalAsync(method, endpoint, request).ConfigureAwait(false); @@ -217,8 +210,7 @@ public async Task SendAsync(string method, string endpoint string bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) where TResponse : class { options = options ?? new RequestOptions(); - options.BucketId = AuthTokenType == TokenType.User ? ClientBucket.Get(clientBucket).Id : bucketId; - options.IsClientBucket = AuthTokenType == TokenType.User; + options.BucketId = bucketId; var request = new RestRequest(RestClient, method, endpoint, options); return DeserializeJson(await SendInternalAsync(method, endpoint, request).ConfigureAwait(false)); @@ -231,8 +223,7 @@ public async Task SendJsonAsync(string method, string endp string bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) where TResponse : class { options = options ?? new RequestOptions(); - options.BucketId = AuthTokenType == TokenType.User ? ClientBucket.Get(clientBucket).Id : bucketId; - options.IsClientBucket = AuthTokenType == TokenType.User; + options.BucketId = bucketId; string json = payload != null ? SerializeJson(payload) : null; var request = new JsonRestRequest(RestClient, method, endpoint, json, options); @@ -246,8 +237,7 @@ public async Task SendMultipartAsync(string method, string string bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) { options = options ?? new RequestOptions(); - options.BucketId = AuthTokenType == TokenType.User ? ClientBucket.Get(clientBucket).Id : bucketId; - options.IsClientBucket = AuthTokenType == TokenType.User; + options.BucketId = bucketId; var request = new MultipartRestRequest(RestClient, method, endpoint, multipartArgs, 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 142f244175..593f796c27 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS0618 using Discord.API; using Discord.API.Gateway; using Discord.Logging; @@ -446,7 +445,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty { var model = data.Guilds[i]; var guild = AddGuild(model, state); - if (!guild.IsAvailable || ApiClient.AuthTokenType == TokenType.User) + if (!guild.IsAvailable) unavailableGuilds++; else await GuildAvailableAsync(guild).ConfigureAwait(false); @@ -465,9 +464,6 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty return; } - if (ApiClient.AuthTokenType == TokenType.User) - await SyncGuildsAsync().ConfigureAwait(false); - _lastGuildAvailableTime = Environment.TickCount; _guildDownloadTask = WaitForGuildsAsync(_connection.CancelToken, _gatewayLogger) .ContinueWith(async x => @@ -542,8 +538,6 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty var guild = AddGuild(data, State); if (guild != null) { - if (ApiClient.AuthTokenType == TokenType.User) - await SyncGuildsAsync().ConfigureAwait(false); await TimedInvokeAsync(_joinedGuildEvent, nameof(JoinedGuild), guild).ConfigureAwait(false); } else diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index e70df8ce8c..259dae5a8d 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -1,4 +1,3 @@ -#pragma warning disable CS0618 using Discord.Audio; using Discord.Rest; using System; @@ -64,7 +63,7 @@ public class SocketGuild : SocketEntity, IGuild public Task DownloaderPromise => _downloaderPromise.Task; public IAudioClient AudioClient => _audioClient; public SocketTextChannel DefaultChannel => TextChannels - .Where(c => CurrentUser.GetPermissions(c).ReadMessages) + .Where(c => CurrentUser.GetPermissions(c).ViewChannel) .OrderBy(c => c.Position) .FirstOrDefault(); public SocketVoiceChannel AFKChannel @@ -192,12 +191,9 @@ internal void Update(ClientState state, ExtendedModel model) _syncPromise = new TaskCompletionSource(); _downloaderPromise = new TaskCompletionSource(); - if (Discord.ApiClient.AuthTokenType != TokenType.User) - { - var _ = _syncPromise.TrySetResultAsync(true); - /*if (!model.Large) - _ = _downloaderPromise.TrySetResultAsync(true);*/ - } + var _ = _syncPromise.TrySetResultAsync(true); + /*if (!model.Large) + _ = _downloaderPromise.TrySetResultAsync(true);*/ } internal void Update(ClientState state, Model model) { From e68ef63bc6ffb70eb4b43cfe9197e4ac20fac7b0 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Mon, 12 Mar 2018 20:46:49 -0400 Subject: [PATCH 25/48] Allow GetPrefixedToken to handle the default TokenType This prevents an issue where no clients could be constructed. In 2fd4f56, the case for user tokens was removed from GetPrefixedToken, which means that the default value for TokenType would now fallthrough to the default case, which throws an error. --- src/Discord.Net.Rest/DiscordRestApiClient.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index c93ed628b0..c2176ca603 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -69,6 +69,8 @@ internal static string GetPrefixedToken(TokenType tokenType, string token) { switch (tokenType) { + case default(TokenType): + return token; case TokenType.Bot: return $"Bot {token}"; case TokenType.Bearer: From f9ac190e9a54b24196f67155d0f856d24760ff8e Mon Sep 17 00:00:00 2001 From: Christopher F Date: Tue, 13 Mar 2018 18:23:42 -0400 Subject: [PATCH 26/48] Don't create a service scope in CommandService#ExecuteAsync anymore IServiceProvider does not support scopes by itself - this is a behavior introduced by Microsoft's DI container. As such, not all DI containers may support an IScopeFactory the way that Microsoft's DI is expecting them to. This also means that our builtin EmptyServiceProvider does not support scopes - meaning that users who do not use a DI container can not invoke commands. --- src/Discord.Net.Commands/CommandService.cs | 134 ++++++++++----------- 1 file changed, 66 insertions(+), 68 deletions(-) diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index d79a3a03b9..de990ab474 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -283,94 +283,92 @@ public Task ExecuteAsync(ICommandContext context, int argPos, IServiceP public async Task ExecuteAsync(ICommandContext context, string input, IServiceProvider services = null, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) { services = services ?? EmptyServiceProvider.Instance; - using (var scope = services.CreateScope()) - { - var searchResult = Search(context, input); - if (!searchResult.IsSuccess) - return searchResult; - var commands = searchResult.Commands; - var preconditionResults = new Dictionary(); + var searchResult = Search(context, input); + if (!searchResult.IsSuccess) + return searchResult; - foreach (var match in commands) - { - preconditionResults[match] = await match.Command.CheckPreconditionsAsync(context, scope.ServiceProvider).ConfigureAwait(false); - } + var commands = searchResult.Commands; + var preconditionResults = new Dictionary(); - var successfulPreconditions = preconditionResults - .Where(x => x.Value.IsSuccess) - .ToArray(); + foreach (var match in commands) + { + preconditionResults[match] = await match.Command.CheckPreconditionsAsync(context, services).ConfigureAwait(false); + } - 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; - } + var successfulPreconditions = preconditionResults + .Where(x => x.Value.IsSuccess) + .ToArray(); - //If we get this far, at least one precondition was successful. + 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; + } - var parseResultsDict = new Dictionary(); - foreach (var pair in successfulPreconditions) - { - var parseResult = await pair.Key.ParseAsync(context, searchResult, pair.Value, scope.ServiceProvider).ConfigureAwait(false); + //If we get this far, at least one precondition was successful. - if (parseResult.Error == CommandError.MultipleMatches) + var parseResultsDict = new Dictionary(); + foreach (var pair in successfulPreconditions) + { + var parseResult = await pair.Key.ParseAsync(context, searchResult, pair.Value, services).ConfigureAwait(false); + + 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; } - - 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; + parseResultsDict[pair.Key] = parseResult; + } - if (match.Command.Parameters.Count > 0) - { - 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; + // Calculates the 'score' of a command given a parse result + float CalculateScore(CommandMatch match, ParseResult parseResult) + { + float argValuesScore = 0, paramValuesScore = 0; - argValuesScore = argValuesSum / match.Command.Parameters.Count; - paramValuesScore = paramValuesSum / match.Command.Parameters.Count; - } + if (match.Command.Parameters.Count > 0) + { + 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; - var totalArgsScore = (argValuesScore + paramValuesScore) / 2; - return match.Command.Priority + totalArgsScore * 0.99f; + argValuesScore = argValuesSum / match.Command.Parameters.Count; + paramValuesScore = paramValuesSum / match.Command.Parameters.Count; } - //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 totalArgsScore = (argValuesScore + paramValuesScore) / 2; + return match.Command.Priority + totalArgsScore * 0.99f; + } - var successfulParses = parseResults - .Where(x => x.Value.IsSuccess) - .ToArray(); + //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)); - 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; - } + var successfulParses = parseResults + .Where(x => x.Value.IsSuccess) + .ToArray(); - //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, scope.ServiceProvider).ConfigureAwait(false); + 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; } + + //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); } } } From e9f9b484b660bc682239c8de2afb3efd84f60ff1 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Tue, 13 Mar 2018 18:28:49 -0400 Subject: [PATCH 27/48] Allow attaching embeds alongside a file upload. (#978) commit a8bafb90cd1c5ea12abaa1aa01d0929833c999a8 Merge: f38dd4c4 7e04285e Author: WamWooWam Date: Mon Mar 12 08:05:52 2018 +0000 Merge branch 'dev' of https://github.com/WamWooWam/Discord.Net into dev commit f38dd4c42149380f3f7f86c21c86cb76d0f104b7 Author: WamWooWam Date: Mon Mar 12 08:05:49 2018 +0000 Cleaned up & fixed code style. commit 7e04285e5dcc0102c9b958e75155834ca35724b9 Author: Thomas May Date: Sun Mar 11 14:11:28 2018 +0000 Revert changes to DefaultRestClient Didn't actually need to change this, whoops. commit 3f5b2c8ef16d356e7b9588c4b36af03162ab7089 Author: WamWooWam Date: Sat Mar 10 19:30:44 2018 +0000 Enabled embeds alongside uploaded files. God damn Discord is a mess. Co-authored-by: WamWooWam --- .../Entities/Channels/IMessageChannel.cs | 6 ++--- .../Extensions/UserExtensions.cs | 22 +++++++++------- .../API/Rest/UploadFileParams.cs | 22 +++++++++++++++- .../Entities/Channels/ChannelHelper.cs | 10 +++---- .../Entities/Channels/IRestMessageChannel.cs | 6 ++--- .../Entities/Channels/RestDMChannel.cs | 20 +++++++------- .../Entities/Channels/RestGroupChannel.cs | 20 +++++++------- .../Entities/Channels/RestTextChannel.cs | 24 ++++++++--------- .../Channels/RpcVirtualMessageChannel.cs | 20 +++++++------- src/Discord.Net.Rest/Net/DefaultRestClient.cs | 3 ++- .../Channels/ISocketMessageChannel.cs | 6 ++--- .../Entities/Channels/SocketDMChannel.cs | 20 +++++++------- .../Entities/Channels/SocketGroupChannel.cs | 20 +++++++------- .../Entities/Channels/SocketTextChannel.cs | 26 +++++++++---------- 14 files changed, 124 insertions(+), 101 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs b/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs index a1df5b4c78..5a6e5df597 100644 --- a/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Threading.Tasks; @@ -11,10 +11,10 @@ public interface IMessageChannel : IChannel Task SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null); #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); + Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null); #endif /// Sends a file to this text channel, with an optional caption. - Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, RequestOptions options = null); + Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null); /// Gets a message from this message channel with the given id, or null if not found. Task GetMessageAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); diff --git a/src/Discord.Net.Core/Extensions/UserExtensions.cs b/src/Discord.Net.Core/Extensions/UserExtensions.cs index eac25391e9..863201cfe5 100644 --- a/src/Discord.Net.Core/Extensions/UserExtensions.cs +++ b/src/Discord.Net.Core/Extensions/UserExtensions.cs @@ -1,4 +1,4 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; using System.IO; namespace Discord @@ -8,10 +8,10 @@ public static class UserExtensions /// /// Sends a message to the user via DM. /// - public static async Task SendMessageAsync(this IUser user, - string text, + public static async Task SendMessageAsync(this IUser user, + string text, bool isTTS = false, - Embed embed = null, + Embed embed = null, RequestOptions options = null) { return await (await user.GetOrCreateDMChannelAsync().ConfigureAwait(false)).SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); @@ -25,23 +25,25 @@ public static async Task SendFileAsync(this IUser user, string filename, string text = null, bool isTTS = false, + Embed embed = null, RequestOptions options = null ) { - return await (await user.GetOrCreateDMChannelAsync().ConfigureAwait(false)).SendFileAsync(stream, filename, text, isTTS, options).ConfigureAwait(false); + return await (await user.GetOrCreateDMChannelAsync().ConfigureAwait(false)).SendFileAsync(stream, filename, text, isTTS, embed, options).ConfigureAwait(false); } #if FILESYSTEM /// /// Sends a file to the user via DM. /// - public static async Task SendFileAsync(this IUser user, - string filePath, - string text = null, - bool isTTS = false, + public static async Task SendFileAsync(this IUser user, + string filePath, + string text = null, + bool isTTS = false, + Embed embed = null, RequestOptions options = null) { - return await (await user.GetOrCreateDMChannelAsync().ConfigureAwait(false)).SendFileAsync(filePath, text, isTTS, options).ConfigureAwait(false); + return await (await user.GetOrCreateDMChannelAsync().ConfigureAwait(false)).SendFileAsync(filePath, text, isTTS, embed, options).ConfigureAwait(false); } #endif } diff --git a/src/Discord.Net.Rest/API/Rest/UploadFileParams.cs b/src/Discord.Net.Rest/API/Rest/UploadFileParams.cs index 30bfc7f9ad..d6e629b5df 100644 --- a/src/Discord.Net.Rest/API/Rest/UploadFileParams.cs +++ b/src/Discord.Net.Rest/API/Rest/UploadFileParams.cs @@ -1,18 +1,25 @@ -#pragma warning disable CS1591 +#pragma warning disable CS1591 +using Discord.Net.Converters; using Discord.Net.Rest; +using Newtonsoft.Json; using System.Collections.Generic; +using System.Globalization; using System.IO; +using System.Text; namespace Discord.API.Rest { internal class UploadFileParams { + private static JsonSerializer _serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() }; + 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 Embed { get; set; } public UploadFileParams(Stream file) { @@ -29,6 +36,19 @@ public IReadOnlyDictionary ToDictionary() d["tts"] = IsTTS.Value.ToString(); if (Nonce.IsSpecified) d["nonce"] = Nonce.Value; + if (Embed.IsSpecified) + { + var sb = new StringBuilder(256); + using (TextWriter text = new StringWriter(sb, CultureInfo.InvariantCulture)) + using (JsonWriter writer = new JsonTextWriter(text)) + { + Dictionary dictionary = new Dictionary(); + dictionary["embed"] = Embed.Value; + + _serializer.Serialize(writer, dictionary); + } + d["payload_json"] = sb.ToString(); + } return d; } } diff --git a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs index f4b6c7f23b..710746896c 100644 --- a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs +++ b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs @@ -1,4 +1,4 @@ -using Discord.API.Rest; +using Discord.API.Rest; using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -170,17 +170,17 @@ public static async Task SendMessageAsync(IMessageChannel chann #if FILESYSTEM public static async Task SendFileAsync(IMessageChannel channel, BaseDiscordClient client, - string filePath, string text, bool isTTS, RequestOptions options) + string filePath, string text, bool isTTS, Embed embed, RequestOptions options) { string filename = Path.GetFileName(filePath); using (var file = File.OpenRead(filePath)) - return await SendFileAsync(channel, client, file, filename, text, isTTS, options).ConfigureAwait(false); + return await SendFileAsync(channel, client, file, filename, text, isTTS, embed, options).ConfigureAwait(false); } #endif public static async Task SendFileAsync(IMessageChannel channel, BaseDiscordClient client, - Stream stream, string filename, string text, bool isTTS, RequestOptions options) + Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options) { - var args = new UploadFileParams(stream) { Filename = filename, Content = text, IsTTS = isTTS }; + var args = new UploadFileParams(stream) { Filename = filename, Content = text, IsTTS = isTTS, Embed = embed?.ToModel() }; var model = await client.ApiClient.UploadFileAsync(channel.Id, args, options).ConfigureAwait(false); return RestUserMessage.Create(client, channel, client.CurrentUser, model); } diff --git a/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs b/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs index 3a104dd9cf..8ab6e98199 100644 --- a/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Threading.Tasks; @@ -10,10 +10,10 @@ public interface IRestMessageChannel : IMessageChannel new Task SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null); #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); + new Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null); #endif /// Sends a file to this text channel, with an optional caption. - new Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, RequestOptions options = null); + new Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null); /// Gets a message from this message channel with the given id, or null if not found. Task GetMessageAsync(ulong id, RequestOptions options = null); diff --git a/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs index d41441967e..08acdf32bb 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; @@ -37,7 +37,7 @@ internal override void Update(Model model) public override async Task UpdateAsync(RequestOptions options = null) { var model = await Discord.ApiClient.GetChannelAsync(Id, options).ConfigureAwait(false); - Update(model); + Update(model); } public Task CloseAsync(RequestOptions options = null) => ChannelHelper.DeleteAsync(this, Discord, options); @@ -66,11 +66,11 @@ public Task> GetPinnedMessagesAsync(RequestOpti public Task SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); #if FILESYSTEM - public Task SendFileAsync(string filePath, string text, bool isTTS = false, RequestOptions options = null) - => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, options); + public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options); #endif - public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, RequestOptions options = null) - => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); + public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options); public Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); @@ -122,11 +122,11 @@ async Task> IMessageChannel.GetPinnedMessagesAsync => await GetPinnedMessagesAsync(options).ConfigureAwait(false); #if FILESYSTEM - async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options) - => await SendFileAsync(filePath, text, isTTS, options).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options) + => await SendFileAsync(filePath, text, isTTS, embed, options).ConfigureAwait(false); #endif - async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, RequestOptions options) - => await SendFileAsync(stream, filename, text, isTTS, options).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options) + => await SendFileAsync(stream, filename, text, isTTS, embed, options).ConfigureAwait(false); async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); IDisposable IMessageChannel.EnterTypingState(RequestOptions options) diff --git a/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs index aa5c0f7dc0..a1868573e3 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs @@ -1,4 +1,4 @@ -using Discord.Audio; +using Discord.Audio; using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -19,7 +19,7 @@ public class RestGroupChannel : RestChannel, IGroupChannel, IRestPrivateChannel, public string Name { get; private set; } public IReadOnlyCollection Users => _users.ToReadOnlyCollection(); - public IReadOnlyCollection Recipients + public IReadOnlyCollection Recipients => _users.Select(x => x.Value).Where(x => x.Id != Discord.CurrentUser.Id).ToReadOnlyCollection(() => _users.Count - 1); internal RestGroupChannel(BaseDiscordClient discord, ulong id) @@ -79,11 +79,11 @@ public Task> GetPinnedMessagesAsync(RequestOpti public Task SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); #if FILESYSTEM - public Task SendFileAsync(string filePath, string text, bool isTTS = false, RequestOptions options = null) - => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, options); + public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options); #endif - public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, RequestOptions options = null) - => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); + public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options); public Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); @@ -132,11 +132,11 @@ async Task> IMessageChannel.GetPinnedMessagesAsync => await GetPinnedMessagesAsync(options).ConfigureAwait(false); #if FILESYSTEM - async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options) - => await SendFileAsync(filePath, text, isTTS, options).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options) + => await SendFileAsync(filePath, text, isTTS, embed, options).ConfigureAwait(false); #endif - async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, RequestOptions options) - => await SendFileAsync(stream, filename, text, isTTS, options).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options) + => await SendFileAsync(stream, filename, text, isTTS, embed, options).ConfigureAwait(false); async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); IDisposable IMessageChannel.EnterTypingState(RequestOptions options) diff --git a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs index 9c29624c1d..600b197d61 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -61,11 +61,11 @@ public Task> GetPinnedMessagesAsync(RequestOpti public Task SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); #if FILESYSTEM - public Task SendFileAsync(string filePath, string text, bool isTTS = false, RequestOptions options = null) - => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, options); + public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options); #endif - public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, RequestOptions options = null) - => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); + public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options); public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) => ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options); @@ -123,18 +123,18 @@ IAsyncEnumerable> IMessageChannel.GetMessagesAsync else return AsyncEnumerable.Empty>(); } - async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) + async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) => await GetPinnedMessagesAsync(options).ConfigureAwait(false); #if FILESYSTEM - async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options) - => await SendFileAsync(filePath, text, isTTS, options).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options) + => await SendFileAsync(filePath, text, isTTS, embed, options).ConfigureAwait(false); #endif - async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, RequestOptions options) - => await SendFileAsync(stream, filename, text, isTTS, options).ConfigureAwait(false); - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) + async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options) + => await SendFileAsync(stream, filename, text, isTTS, embed, options).ConfigureAwait(false); + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); - IDisposable IMessageChannel.EnterTypingState(RequestOptions options) + IDisposable IMessageChannel.EnterTypingState(RequestOptions options) => EnterTypingState(options); //IGuildChannel diff --git a/src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs index eb807423f8..3b6a68193f 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -21,7 +21,7 @@ internal static RestVirtualMessageChannel Create(BaseDiscordClient discord, ulon { return new RestVirtualMessageChannel(discord, id); } - + public Task GetMessageAsync(ulong id, RequestOptions options = null) => ChannelHelper.GetMessageAsync(this, Discord, id, options); public IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) @@ -36,11 +36,11 @@ public Task> GetPinnedMessagesAsync(RequestOpti public Task SendMessageAsync(string text, bool isTTS, Embed embed = null, RequestOptions options = null) => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); #if FILESYSTEM - public Task SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options = null) - => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, options); + public Task SendFileAsync(string filePath, string text, bool isTTS, Embed embed = null, RequestOptions options = null) + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options); #endif - public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS, RequestOptions options = null) - => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); + public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed = null, RequestOptions options = null) + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options); public Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); @@ -82,11 +82,11 @@ async Task> IMessageChannel.GetPinnedMessagesAsync => await GetPinnedMessagesAsync(options); #if FILESYSTEM - async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options) - => await SendFileAsync(filePath, text, isTTS, options); + async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options) + => await SendFileAsync(filePath, text, isTTS, embed, options); #endif - async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, RequestOptions options) - => await SendFileAsync(stream, filename, text, isTTS, options); + async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options) + => await SendFileAsync(stream, filename, text, isTTS, embed, options); async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) => await SendMessageAsync(text, isTTS, embed, options); IDisposable IMessageChannel.EnterTypingState(RequestOptions options) diff --git a/src/Discord.Net.Rest/Net/DefaultRestClient.cs b/src/Discord.Net.Rest/Net/DefaultRestClient.cs index 637099fd60..ec789be591 100644 --- a/src/Discord.Net.Rest/Net/DefaultRestClient.cs +++ b/src/Discord.Net.Rest/Net/DefaultRestClient.cs @@ -1,4 +1,5 @@ -using Newtonsoft.Json; +using Discord.Net.Converters; +using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Globalization; diff --git a/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs index e2119e7a2d..026bd8378e 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs @@ -1,4 +1,4 @@ -using Discord.Rest; +using Discord.Rest; using System.Collections.Generic; using System.IO; using System.Threading.Tasks; @@ -14,10 +14,10 @@ public interface ISocketMessageChannel : IMessageChannel new Task SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null); #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); + new Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null); #endif /// Sends a file to this text channel, with an optional caption. - new Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, RequestOptions options = null); + new Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null); SocketMessage GetCachedMessage(ulong id); /// Gets the last N messages from this message channel. diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs index 00cef60f86..451240e662 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs @@ -1,4 +1,4 @@ -using Discord.Rest; +using Discord.Rest; using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -70,11 +70,11 @@ public Task> GetPinnedMessagesAsync(RequestOpti public Task SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); #if FILESYSTEM - public Task SendFileAsync(string filePath, string text, bool isTTS = false, RequestOptions options = null) - => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, options); + public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options); #endif - public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, RequestOptions options = null) - => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); + public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options); public Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); @@ -113,7 +113,7 @@ internal SocketMessage RemoveMessage(ulong id) //IPrivateChannel IReadOnlyCollection IPrivateChannel.Recipients => ImmutableArray.Create(Recipient); - + //IMessageChannel async Task IMessageChannel.GetMessageAsync(ulong id, CacheMode mode, RequestOptions options) { @@ -131,11 +131,11 @@ IAsyncEnumerable> IMessageChannel.GetMessagesAsync async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) => await GetPinnedMessagesAsync(options).ConfigureAwait(false); #if FILESYSTEM - async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options) - => await SendFileAsync(filePath, text, isTTS, options).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options) + => await SendFileAsync(filePath, text, isTTS, embed, options).ConfigureAwait(false); #endif - async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, RequestOptions options) - => await SendFileAsync(stream, filename, text, isTTS, options).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options) + => await SendFileAsync(stream, filename, text, isTTS, embed, options).ConfigureAwait(false); async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); IDisposable IMessageChannel.EnterTypingState(RequestOptions options) diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs index 92a93a9030..d46c5fc59d 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs @@ -1,4 +1,4 @@ -using Discord.Audio; +using Discord.Audio; using Discord.Rest; using System; using System.Collections.Concurrent; @@ -61,7 +61,7 @@ private void UpdateUsers(ClientState state, UserModel[] models) users[models[i].Id] = SocketGroupUser.Create(this, state, models[i]); _users = users; } - + public Task LeaveAsync(RequestOptions options = null) => ChannelHelper.DeleteAsync(this, Discord, options); @@ -98,11 +98,11 @@ public Task> GetPinnedMessagesAsync(RequestOpti public Task SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); #if FILESYSTEM - public Task SendFileAsync(string filePath, string text, bool isTTS = false, RequestOptions options = null) - => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, options); + public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options); #endif - public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, RequestOptions options = null) - => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); + public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options); public Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); @@ -195,11 +195,11 @@ IAsyncEnumerable> IMessageChannel.GetMessagesAsync async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) => await GetPinnedMessagesAsync(options).ConfigureAwait(false); #if FILESYSTEM - async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options) - => await SendFileAsync(filePath, text, isTTS, options).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options) + => await SendFileAsync(filePath, text, isTTS, embed, options).ConfigureAwait(false); #endif - async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, RequestOptions options) - => await SendFileAsync(stream, filename, text, isTTS, options).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options) + => await SendFileAsync(stream, filename, text, isTTS, embed, options).ConfigureAwait(false); async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); IDisposable IMessageChannel.EnterTypingState(RequestOptions options) diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs index 7b8f572d25..ec7615b55e 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs @@ -1,4 +1,4 @@ -using Discord.Rest; +using Discord.Rest; using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -16,7 +16,7 @@ public class SocketTextChannel : SocketGuildChannel, ITextChannel, ISocketMessag private readonly MessageCache _messages; public string Topic { get; private set; } - + private bool _nsfw; public bool IsNsfw => _nsfw || ChannelHelper.IsNsfw(this); @@ -24,9 +24,9 @@ public class SocketTextChannel : SocketGuildChannel, ITextChannel, ISocketMessag public IReadOnlyCollection CachedMessages => _messages?.Messages ?? ImmutableArray.Create(); public override IReadOnlyCollection Users => Guild.Users.Where(x => Permissions.GetValue( - Permissions.ResolveChannel(Guild, x, this, Permissions.ResolveGuild(Guild, x)), + Permissions.ResolveChannel(Guild, x, this, Permissions.ResolveGuild(Guild, x)), ChannelPermission.ViewChannel)).ToImmutableArray(); - + internal SocketTextChannel(DiscordSocketClient discord, ulong id, SocketGuild guild) : base(discord, id, guild) { @@ -78,11 +78,11 @@ public Task> GetPinnedMessagesAsync(RequestOpti public Task SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); #if FILESYSTEM - public Task SendFileAsync(string filePath, string text, bool isTTS = false, RequestOptions options = null) - => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, options); + public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options); #endif - public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, RequestOptions options = null) - => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); + public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options); public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) => ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options); @@ -155,14 +155,14 @@ IAsyncEnumerable> IMessageChannel.GetMessagesAsync async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) => await GetPinnedMessagesAsync(options).ConfigureAwait(false); #if FILESYSTEM - async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options) - => await SendFileAsync(filePath, text, isTTS, options).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options) + => await SendFileAsync(filePath, text, isTTS, embed, options).ConfigureAwait(false); #endif - async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, RequestOptions options) - => await SendFileAsync(stream, filename, text, isTTS, options).ConfigureAwait(false); + async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options) + => await SendFileAsync(stream, filename, text, isTTS, embed, options).ConfigureAwait(false); async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); IDisposable IMessageChannel.EnterTypingState(RequestOptions options) => EnterTypingState(options); } -} \ No newline at end of file +} From f175dde2b3197201392e2356baa7fbaabb0928a3 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Tue, 13 Mar 2018 19:16:12 -0400 Subject: [PATCH 28/48] Clean embed serialization up slightly --- .../API/Rest/UploadFileParams.cs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/Discord.Net.Rest/API/Rest/UploadFileParams.cs b/src/Discord.Net.Rest/API/Rest/UploadFileParams.cs index d6e629b5df..5c06a033e1 100644 --- a/src/Discord.Net.Rest/API/Rest/UploadFileParams.cs +++ b/src/Discord.Net.Rest/API/Rest/UploadFileParams.cs @@ -38,16 +38,19 @@ public IReadOnlyDictionary ToDictionary() d["nonce"] = Nonce.Value; if (Embed.IsSpecified) { - var sb = new StringBuilder(256); - using (TextWriter text = new StringWriter(sb, CultureInfo.InvariantCulture)) - using (JsonWriter writer = new JsonTextWriter(text)) + var payload = new StringBuilder(); + using (var text = new StringWriter(payload)) + using (var writer = new JsonTextWriter(text)) { - Dictionary dictionary = new Dictionary(); - dictionary["embed"] = Embed.Value; + var map = new Dictionary() + { + ["embed"] = Embed.Value, + }; - _serializer.Serialize(writer, dictionary); + _serializer.Serialize(writer, map); } - d["payload_json"] = sb.ToString(); + d["payload_json"] = payload.ToString(); + } return d; } From fc5e70c9dde57cde7f9321414f62bd9281909d20 Mon Sep 17 00:00:00 2001 From: Darnell Williams Date: Thu, 15 Mar 2018 18:49:25 -0400 Subject: [PATCH 29/48] Attempts to resolve #961 (#962) * Move REST requests to appropiate class * Add call to ClientHelper and expose to public API * Expose shard count request in public api * Expose method from interface * Update sharded client to utilize the new method * Method is already implemented in a base class * Refactor name to fit pattern for methods returning a `Task` * Adds missing ConfigureAwait * Corrects unnecessary whitespace * Removes unneeded whitespace --- src/Discord.Net.Core/IDiscordClient.cs | 2 ++ src/Discord.Net.Rest/BaseDiscordClient.cs | 6 +++++- src/Discord.Net.Rest/ClientHelper.cs | 8 +++++++- src/Discord.Net.Rest/DiscordRestApiClient.cs | 12 ++++++++++++ src/Discord.Net.Rest/DiscordRestClient.cs | 2 +- src/Discord.Net.WebSocket/BaseSocketClient.cs | 2 +- src/Discord.Net.WebSocket/DiscordShardedClient.cs | 6 +++--- .../DiscordSocketApiClient.cs | 15 ++------------- 8 files changed, 33 insertions(+), 20 deletions(-) diff --git a/src/Discord.Net.Core/IDiscordClient.cs b/src/Discord.Net.Core/IDiscordClient.cs index 9abb959b52..a383c37da8 100644 --- a/src/Discord.Net.Core/IDiscordClient.cs +++ b/src/Discord.Net.Core/IDiscordClient.cs @@ -36,5 +36,7 @@ public interface IDiscordClient : IDisposable Task GetVoiceRegionAsync(string id, RequestOptions options = null); Task GetWebhookAsync(ulong id, RequestOptions options = null); + + Task GetRecommendedShardCountAsync(RequestOptions options = null); } } diff --git a/src/Discord.Net.Rest/BaseDiscordClient.cs b/src/Discord.Net.Rest/BaseDiscordClient.cs index 269dedd71d..f8642b96c8 100644 --- a/src/Discord.Net.Rest/BaseDiscordClient.cs +++ b/src/Discord.Net.Rest/BaseDiscordClient.cs @@ -1,4 +1,4 @@ -using Discord.Logging; +using Discord.Logging; using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -125,6 +125,10 @@ internal virtual void Dispose(bool disposing) /// public void Dispose() => Dispose(true); + /// + public Task GetRecommendedShardCountAsync(RequestOptions options = null) + => ClientHelper.GetRecommendShardCountAsync(this, options); + //IDiscordClient ConnectionState IDiscordClient.ConnectionState => ConnectionState.Disconnected; ISelfUser IDiscordClient.CurrentUser => CurrentUser; diff --git a/src/Discord.Net.Rest/ClientHelper.cs b/src/Discord.Net.Rest/ClientHelper.cs index 5c9e264335..08305f8570 100644 --- a/src/Discord.Net.Rest/ClientHelper.cs +++ b/src/Discord.Net.Rest/ClientHelper.cs @@ -1,4 +1,4 @@ -using Discord.API.Rest; +using Discord.API.Rest; using System.Collections.Generic; using System.Collections.Immutable; using System.IO; @@ -163,5 +163,11 @@ public static async Task GetVoiceRegionAsync(BaseDiscordClient var models = await client.ApiClient.GetVoiceRegionsAsync(options).ConfigureAwait(false); return models.Select(x => RestVoiceRegion.Create(client, x)).FirstOrDefault(x => x.Id == id); } + + public static async Task GetRecommendShardCountAsync(BaseDiscordClient client, RequestOptions options) + { + var response = await client.ApiClient.GetBotGatewayAsync(options).ConfigureAwait(false); + return response.Shards; + } } } diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index c2176ca603..556d6fbe68 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -269,6 +269,18 @@ public async Task ValidateTokenAsync(RequestOptions options = null) await SendAsync("GET", () => "auth/login", new BucketIds(), options: options).ConfigureAwait(false); } + //Gateway + public async Task GetGatewayAsync(RequestOptions options = null) + { + options = RequestOptions.CreateOrClone(options); + return await SendAsync("GET", () => "gateway", new BucketIds(), options: options).ConfigureAwait(false); + } + public async Task GetBotGatewayAsync(RequestOptions options = null) + { + options = RequestOptions.CreateOrClone(options); + return await SendAsync("GET", () => "gateway/bot", new BucketIds(), options: options).ConfigureAwait(false); + } + //Channels public async Task GetChannelAsync(ulong channelId, RequestOptions options = null) { diff --git a/src/Discord.Net.Rest/DiscordRestClient.cs b/src/Discord.Net.Rest/DiscordRestClient.cs index 3d90b6c00a..8850da3a59 100644 --- a/src/Discord.Net.Rest/DiscordRestClient.cs +++ b/src/Discord.Net.Rest/DiscordRestClient.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Collections.Immutable; using System.IO; using System.Threading.Tasks; diff --git a/src/Discord.Net.WebSocket/BaseSocketClient.cs b/src/Discord.Net.WebSocket/BaseSocketClient.cs index 5fa3cbff8c..923b2c23ba 100644 --- a/src/Discord.Net.WebSocket/BaseSocketClient.cs +++ b/src/Discord.Net.WebSocket/BaseSocketClient.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Threading.Tasks; using Discord.API; diff --git a/src/Discord.Net.WebSocket/DiscordShardedClient.cs b/src/Discord.Net.WebSocket/DiscordShardedClient.cs index fb78a201f6..ad89067df1 100644 --- a/src/Discord.Net.WebSocket/DiscordShardedClient.cs +++ b/src/Discord.Net.WebSocket/DiscordShardedClient.cs @@ -1,4 +1,4 @@ -using Discord.API; +using Discord.API; using Discord.Rest; using System; using System.Collections.Generic; @@ -75,8 +75,8 @@ internal override async Task OnLoginAsync(TokenType tokenType, string token) { if (_automaticShards) { - var response = await ApiClient.GetBotGatewayAsync().ConfigureAwait(false); - _shardIds = Enumerable.Range(0, response.Shards).ToArray(); + var shardCount = await GetRecommendedShardCountAsync().ConfigureAwait(false); + _shardIds = Enumerable.Range(0, shardCount).ToArray(); _totalShards = _shardIds.Length; _shards = new DiscordSocketClient[_shardIds.Length]; for (int i = 0; i < _shardIds.Length; i++) diff --git a/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs b/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs index 72781204c4..8ae41cc594 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs @@ -1,4 +1,4 @@ -#pragma warning disable CS1591 +#pragma warning disable CS1591 using Discord.API.Gateway; using Discord.API.Rest; using Discord.Net.Queue; @@ -207,18 +207,7 @@ private async Task SendGatewayInternalAsync(GatewayOpCode opCode, object payload await RequestQueue.SendAsync(new WebSocketRequest(WebSocketClient, null, bytes, true, options)).ConfigureAwait(false); await _sentGatewayMessageEvent.InvokeAsync(opCode).ConfigureAwait(false); } - - //Gateway - public async Task GetGatewayAsync(RequestOptions options = null) - { - options = RequestOptions.CreateOrClone(options); - return await SendAsync("GET", () => "gateway", new BucketIds(), options: options).ConfigureAwait(false); - } - public async Task GetBotGatewayAsync(RequestOptions options = null) - { - options = RequestOptions.CreateOrClone(options); - return await SendAsync("GET", () => "gateway/bot", new BucketIds(), options: options).ConfigureAwait(false); - } + public async Task SendIdentifyAsync(int largeThreshold = 100, int shardID = 0, int totalShards = 1, RequestOptions options = null) { options = RequestOptions.CreateOrClone(options); From b70ae4128599b6c7f3fa9890d6765e5c8fd505e5 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Sun, 18 Mar 2018 15:29:23 -0400 Subject: [PATCH 30/48] AddModule(s)Async should be explicit about IServiceProvider In f19730e4, AddModule(s)Async was changed so that the IServiceProvider was optional, both at compile time and runtime. This had the side effect of meaning that there was no longer a compile-time hint that users would need to pass an IServiceProvider to AddModulesAsync. I assumed this would not be an issue - users would recognize the runtime exception here and self correct - but activity in our Discord support channel would indicate otherwise. We now require the user to explicitly opt-out of dependency injection - they are still free to pass null in place of an IServiceProvider if they do not intend to use one, and the library will handle this at runtime. --- src/Discord.Net.Commands/CommandService.cs | 23 +++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index de990ab474..f4fbcf8b2a 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -95,8 +95,15 @@ public async Task CreateModuleAsync(string primaryAlias, Action AddModuleAsync(IServiceProvider services = null) => AddModuleAsync(typeof(T), services); - public async Task AddModuleAsync(Type type, IServiceProvider services = null) + + /// + /// Add a command module from a type + /// + /// The type of module + /// An IServiceProvider for your dependency injection solution, if using one - otherwise, pass null + /// A built module + public Task AddModuleAsync(IServiceProvider services) => AddModuleAsync(typeof(T), services); + public async Task AddModuleAsync(Type type, IServiceProvider services) { services = services ?? EmptyServiceProvider.Instance; @@ -122,7 +129,13 @@ public async Task AddModuleAsync(Type type, IServiceProvider service _moduleLock.Release(); } } - public async Task> AddModulesAsync(Assembly assembly, IServiceProvider services = null) + /// + /// Add command modules from an assembly + /// + /// The assembly containing command modules + /// An IServiceProvider for your dependency injection solution, if using one - otherwise, pass null + /// A collection of built modules + public async Task> AddModulesAsync(Assembly assembly, IServiceProvider services) { services = services ?? EmptyServiceProvider.Instance; @@ -278,9 +291,9 @@ public SearchResult Search(ICommandContext context, string input) return SearchResult.FromError(CommandError.UnknownCommand, "Unknown command."); } - public Task ExecuteAsync(ICommandContext context, int argPos, IServiceProvider services = null, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) + public Task ExecuteAsync(ICommandContext context, int argPos, IServiceProvider services, 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) + public async Task ExecuteAsync(ICommandContext context, string input, IServiceProvider services, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) { services = services ?? EmptyServiceProvider.Instance; From b38dca7803806a46f51395b2c0673500eda25755 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Sun, 18 Mar 2018 15:48:34 -0400 Subject: [PATCH 31/48] All arguments in ReplyAsync should be optional To reply with just a rich embed, users have to invoke ReplyAsync with `ReplyAsync("", embed: embed)`, which seems wasteful, when they only need to specify the embed. --- src/Discord.Net.Commands/ModuleBase.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net.Commands/ModuleBase.cs b/src/Discord.Net.Commands/ModuleBase.cs index c35a3cf676..3e6fbbd9bb 100644 --- a/src/Discord.Net.Commands/ModuleBase.cs +++ b/src/Discord.Net.Commands/ModuleBase.cs @@ -11,7 +11,13 @@ public abstract class ModuleBase : IModuleBase { public T Context { get; private set; } - protected virtual async Task ReplyAsync(string message, bool isTTS = false, Embed embed = null, RequestOptions options = null) + /// + /// Sends a message to the source channel + /// + /// Contents of the message; optional only if is specified + /// Specifies if Discord should read this message aloud using TTS + /// An embed to be displayed alongside the message + protected virtual async Task ReplyAsync(string message = null, bool isTTS = false, Embed embed = null, RequestOptions options = null) { return await Context.Channel.SendMessageAsync(message, isTTS, embed, options).ConfigureAwait(false); } From b9be6deb4f07adca13d33f23cfc9c8d21f0297e9 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Sun, 18 Mar 2018 15:59:45 -0400 Subject: [PATCH 32/48] Move EmbedBuilder+Extensions to Discord.Net.Core Previously it was implemented under Discord.Net.Rest, which seems inconsistent and unnecessary. This also allows Commands docstrings to reference EmbedBuilder, since Commands only has a dependency on Core. --- .../Entities/Messages/EmbedBuilder.cs | 0 .../Extensions/EmbedBuilderExtensions.cs | 0 src/Discord.Net.Rest/AssemblyInfo.cs | 7 +++++-- 3 files changed, 5 insertions(+), 2 deletions(-) rename src/{Discord.Net.Rest => Discord.Net.Core}/Entities/Messages/EmbedBuilder.cs (100%) rename src/{Discord.Net.Rest => Discord.Net.Core}/Extensions/EmbedBuilderExtensions.cs (100%) diff --git a/src/Discord.Net.Rest/Entities/Messages/EmbedBuilder.cs b/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs similarity index 100% rename from src/Discord.Net.Rest/Entities/Messages/EmbedBuilder.cs rename to src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs diff --git a/src/Discord.Net.Rest/Extensions/EmbedBuilderExtensions.cs b/src/Discord.Net.Core/Extensions/EmbedBuilderExtensions.cs similarity index 100% rename from src/Discord.Net.Rest/Extensions/EmbedBuilderExtensions.cs rename to src/Discord.Net.Core/Extensions/EmbedBuilderExtensions.cs diff --git a/src/Discord.Net.Rest/AssemblyInfo.cs b/src/Discord.Net.Rest/AssemblyInfo.cs index a4f045ab5e..126365e473 100644 --- a/src/Discord.Net.Rest/AssemblyInfo.cs +++ b/src/Discord.Net.Rest/AssemblyInfo.cs @@ -1,7 +1,10 @@ -using System.Runtime.CompilerServices; +using System.Runtime.CompilerServices; [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 +[assembly: InternalsVisibleTo("Discord.Net.Tests")] + +[assembly: TypeForwardedTo(typeof(Discord.EmbedBuilder))] +[assembly: TypeForwardedTo(typeof(Discord.EmbedBuilderExtensions))] From 64b9cc7a538195040cd60779b68c0d11b77ca3c0 Mon Sep 17 00:00:00 2001 From: Still Hsu <341464@gmail.com> Date: Mon, 19 Mar 2018 04:06:53 +0800 Subject: [PATCH 33/48] Add Spotify track support (#970) * Initial Spotify support * Remove GameAsset#ToEntity - appId doesn't seem to be necessary, and Spotify Game doesn't return appId either. * Implement SpotifyGame details * Implement song Duration prop * Add album art CDN * Fix ActivityType * Remove payload debug * Add changes according to review + Make `ApplicationId` nullable + Move ctor after props --- .../Entities/Activities/Game.cs | 2 +- .../Entities/Activities/GameAsset.cs | 8 +++---- .../Entities/Activities/RichGame.cs | 8 +++---- .../Entities/Activities/SpotifyGame.cs | 23 +++++++++++++++++++ src/Discord.Net.Rest/API/Common/Game.cs | 6 ++++- .../Extensions/EntityExtensions.cs | 23 ++++++++++++++++++- 6 files changed, 59 insertions(+), 11 deletions(-) create mode 100644 src/Discord.Net.Core/Entities/Activities/SpotifyGame.cs diff --git a/src/Discord.Net.Core/Entities/Activities/Game.cs b/src/Discord.Net.Core/Entities/Activities/Game.cs index fe32470eef..179ad4eaa7 100644 --- a/src/Discord.Net.Core/Entities/Activities/Game.cs +++ b/src/Discord.Net.Core/Entities/Activities/Game.cs @@ -1,4 +1,4 @@ -using System.Diagnostics; +using System.Diagnostics; namespace Discord { diff --git a/src/Discord.Net.Core/Entities/Activities/GameAsset.cs b/src/Discord.Net.Core/Entities/Activities/GameAsset.cs index 385f372149..02c29ba41b 100644 --- a/src/Discord.Net.Core/Entities/Activities/GameAsset.cs +++ b/src/Discord.Net.Core/Entities/Activities/GameAsset.cs @@ -1,15 +1,15 @@ -namespace Discord +namespace Discord { public class GameAsset { internal GameAsset() { } - internal ulong ApplicationId { get; set; } + internal ulong? ApplicationId { get; set; } public string Text { get; internal set; } public string ImageId { get; internal set; } public string GetImageUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) - => CDN.GetRichAssetUrl(ApplicationId, ImageId, size, format); + => ApplicationId.HasValue ? CDN.GetRichAssetUrl(ApplicationId.Value, ImageId, size, format) : null; } -} \ No newline at end of file +} diff --git a/src/Discord.Net.Core/Entities/Activities/RichGame.cs b/src/Discord.Net.Core/Entities/Activities/RichGame.cs index e66eac1d20..fc3f68cf0d 100644 --- a/src/Discord.Net.Core/Entities/Activities/RichGame.cs +++ b/src/Discord.Net.Core/Entities/Activities/RichGame.cs @@ -1,4 +1,4 @@ -using System.Diagnostics; +using System.Diagnostics; namespace Discord { @@ -7,8 +7,8 @@ public class RichGame : Game { internal RichGame() { } - public string Details { get; internal set;} - public string State { get; internal set;} + public string Details { get; internal set; } + public string State { get; internal set; } public ulong ApplicationId { get; internal set; } public GameAsset SmallAsset { get; internal set; } public GameAsset LargeAsset { get; internal set; } @@ -19,4 +19,4 @@ internal RichGame() { } public override string ToString() => Name; private string DebuggerDisplay => $"{Name} (Rich)"; } -} \ No newline at end of file +} diff --git a/src/Discord.Net.Core/Entities/Activities/SpotifyGame.cs b/src/Discord.Net.Core/Entities/Activities/SpotifyGame.cs new file mode 100644 index 0000000000..b8a4b80434 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Activities/SpotifyGame.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Discord +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class SpotifyGame : Game + { + public string[] Artists { get; internal set; } + public string AlbumArt { get; internal set; } + public string AlbumTitle { get; internal set; } + public string TrackTitle { get; internal set; } + public string SyncId { get; internal set; } + public string SessionId { get; internal set; } + public TimeSpan? Duration { get; internal set; } + + internal SpotifyGame() { } + + public override string ToString() => Name; + private string DebuggerDisplay => $"{Name} (Spotify)"; + } +} diff --git a/src/Discord.Net.Rest/API/Common/Game.cs b/src/Discord.Net.Rest/API/Common/Game.cs index 29e0d987d4..4cde8444ab 100644 --- a/src/Discord.Net.Rest/API/Common/Game.cs +++ b/src/Discord.Net.Rest/API/Common/Game.cs @@ -1,4 +1,4 @@ -#pragma warning disable CS1591 +#pragma warning disable CS1591 using Newtonsoft.Json; using Newtonsoft.Json.Serialization; using System.Runtime.Serialization; @@ -29,6 +29,10 @@ internal class Game public Optional Timestamps { get; set; } [JsonProperty("instance")] public Optional Instance { get; set; } + [JsonProperty("sync_id")] + public Optional SyncId { get; set; } + [JsonProperty("session_id")] + public Optional SessionId { get; set; } [OnError] internal void OnError(StreamingContext context, ErrorContext errorContext) diff --git a/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs b/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs index 181a837e4b..f268a7ff21 100644 --- a/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs +++ b/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs @@ -4,6 +4,27 @@ internal static class EntityExtensions { public static IActivity ToEntity(this API.Game model) { + // Spotify Game + if (model.SyncId.IsSpecified) + { + var assets = model.Assets.GetValueOrDefault()?.ToEntity(); + string albumText = assets?[1]?.Text; + string albumArtId = assets?[1]?.ImageId?.Replace("spotify:",""); + var timestamps = model.Timestamps.IsSpecified ? model.Timestamps.Value.ToEntity() : null; + return new SpotifyGame + { + Name = model.Name, + SessionId = model.SessionId.GetValueOrDefault(), + SyncId = model.SyncId.Value, + AlbumTitle = albumText, + TrackTitle = model.Details.GetValueOrDefault(), + Artists = model.State.GetValueOrDefault()?.Split(';'), + Duration = timestamps?.End - timestamps?.Start, + AlbumArt = albumArtId != null ? $"https://i.scdn.co/image/{albumArtId}" : null, + Type = ActivityType.Listening + }; + } + // Rich Game if (model.ApplicationId.IsSpecified) { @@ -34,7 +55,7 @@ public static IActivity ToEntity(this API.Game model) } // (Small, Large) - public static GameAsset[] ToEntity(this API.GameAssets model, ulong appId) + public static GameAsset[] ToEntity(this API.GameAssets model, ulong? appId = null) { return new GameAsset[] { From 02c650773d657a8a9d0ee95d8c5d4af7d6157418 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Sun, 18 Mar 2018 16:18:40 -0400 Subject: [PATCH 34/48] Clean up SpotifyGame PR - Add a helper under CDN for cover art URLs It would be bad practice of us to leave CDN urls hardcoded in the deserializer, would be harder to change down the line should Spotify ever change their CDN. I'm not entirely supportive of leaving Spotify's CDN hardcoded in our lib either, but there's no better alternative. - Change SpotifyGame#Artists to an IEnumerable Seems pretty common to prefer IEnumerables in place of Arrays. --- src/Discord.Net.Core/CDN.cs | 5 ++++- src/Discord.Net.Core/Entities/Activities/SpotifyGame.cs | 2 +- src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Discord.Net.Core/CDN.cs b/src/Discord.Net.Core/CDN.cs index 070b965ee2..f23f552387 100644 --- a/src/Discord.Net.Core/CDN.cs +++ b/src/Discord.Net.Core/CDN.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace Discord { @@ -28,6 +28,9 @@ public static string GetRichAssetUrl(ulong appId, string assetId, ushort size, I return $"{DiscordConfig.CDNUrl}app-assets/{appId}/{assetId}.{extension}?size={size}"; } + public static string GetSpotifyAlbumArtUrl(string albumArtId) + => $"https://i.scdn.co/image/{albumArtId}"; + private static string FormatToExtension(ImageFormat format, string imageId) { if (format == ImageFormat.Auto) diff --git a/src/Discord.Net.Core/Entities/Activities/SpotifyGame.cs b/src/Discord.Net.Core/Entities/Activities/SpotifyGame.cs index b8a4b80434..a203842427 100644 --- a/src/Discord.Net.Core/Entities/Activities/SpotifyGame.cs +++ b/src/Discord.Net.Core/Entities/Activities/SpotifyGame.cs @@ -7,7 +7,7 @@ namespace Discord [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class SpotifyGame : Game { - public string[] Artists { get; internal set; } + public IEnumerable Artists { get; internal set; } public string AlbumArt { get; internal set; } public string AlbumTitle { get; internal set; } public string TrackTitle { get; internal set; } diff --git a/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs b/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs index f268a7ff21..ff395a932a 100644 --- a/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs +++ b/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs @@ -20,7 +20,7 @@ public static IActivity ToEntity(this API.Game model) TrackTitle = model.Details.GetValueOrDefault(), Artists = model.State.GetValueOrDefault()?.Split(';'), Duration = timestamps?.End - timestamps?.Start, - AlbumArt = albumArtId != null ? $"https://i.scdn.co/image/{albumArtId}" : null, + AlbumArt = albumArtId != null ? CDN.GetSpotifyAlbumArtUrl(albumArtId) : null, Type = ActivityType.Listening }; } From bfaa6fc97a392867fd7c5251cdf6e396196b5f46 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Sun, 18 Mar 2018 16:22:27 -0400 Subject: [PATCH 35/48] Enforce a maximum value when parsing unix timestamps (#981) * UnixTimestampConverter should now obey a maximum value This change prevents an issue where the converter would be unable to handle obscenely large timestamp values - which are actually quite common on Discord. OptionalConverter had to be rewritten to support checking whether or not an InnerConverter returned an Optional. The perf impacts from this _shouldn't_ be too bad, as types without a custom parser (which should be the majority of Optionals in the lib) will bypass the type-check. * optimizations on OptionalConverter --- .../Net/Converters/OptionalConverter.cs | 12 ++++++++++-- .../Net/Converters/UnixTimestampConverter.cs | 17 +++++++++++------ 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/Discord.Net.Rest/Net/Converters/OptionalConverter.cs b/src/Discord.Net.Rest/Net/Converters/OptionalConverter.cs index 18b2a9e1cf..d3d6191c09 100644 --- a/src/Discord.Net.Rest/Net/Converters/OptionalConverter.cs +++ b/src/Discord.Net.Rest/Net/Converters/OptionalConverter.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; using System; namespace Discord.Net.Converters @@ -19,10 +19,18 @@ public OptionalConverter(JsonConverter innerConverter) public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { T obj; + // custom converters need to be able to safely fail; move this check in here to prevent wasteful casting when parsing primitives if (_innerConverter != null) - obj = (T)_innerConverter.ReadJson(reader, typeof(T), null, serializer); + { + object o = _innerConverter.ReadJson(reader, typeof(T), null, serializer); + if (o is Optional) + return o; + + obj = (T)o; + } else obj = serializer.Deserialize(reader); + return new Optional(obj); } diff --git a/src/Discord.Net.Rest/Net/Converters/UnixTimestampConverter.cs b/src/Discord.Net.Rest/Net/Converters/UnixTimestampConverter.cs index d4660dc442..0b50cb166d 100644 --- a/src/Discord.Net.Rest/Net/Converters/UnixTimestampConverter.cs +++ b/src/Discord.Net.Rest/Net/Converters/UnixTimestampConverter.cs @@ -1,4 +1,4 @@ -using System; +using System; using Newtonsoft.Json; namespace Discord.Net.Converters @@ -11,13 +11,18 @@ public class UnixTimestampConverter : JsonConverter public override bool CanRead => true; public override bool CanWrite => true; + // 1e13 unix ms = year 2286 + // necessary to prevent discord.js from sending values in the e15 and overflowing a DTO + private const long MaxSaneMs = 1_000_000_000_000_0; + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { - // Discord doesn't validate if timestamps contain decimals or not - if (reader.Value is double d) + // Discord doesn't validate if timestamps contain decimals or not, and they also don't validate if timestamps are reasonably sized + if (reader.Value is double d && d < MaxSaneMs) return new DateTimeOffset(1970, 1, 1, 0, 0, 0, 0, TimeSpan.Zero).AddMilliseconds(d); - long offset = (long)reader.Value; - return new DateTimeOffset(1970, 1, 1, 0, 0, 0, 0, TimeSpan.Zero).AddMilliseconds(offset); + else if (reader.Value is long l && l < MaxSaneMs) + return new DateTimeOffset(1970, 1, 1, 0, 0, 0, 0, TimeSpan.Zero).AddMilliseconds(l); + return Optional.Unspecified; } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) @@ -25,4 +30,4 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s throw new NotImplementedException(); } } -} \ No newline at end of file +} From ac5ecd365d05281c68ec574d8b13d9e145f5af50 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Mon, 19 Mar 2018 16:29:51 -0400 Subject: [PATCH 36/48] Include the content in `payload_json` for file uploads This resolves #987 Previous behavior was that even if `null` was passed for an embed in UploadFileAsync, the Embed property on UploadFileArgs was still specified - this meant we were always sending a payload_json. If a payload_json is specified, it seems like Discord will only read from the payload_json, and will ignore properties set outside of it. To prevent unnecessary code duplication, this commit always specifies parameters in the payload_json, and also will only include the embed if one was actually specified with real data (not null). --- .../API/Rest/UploadFileParams.cs | 32 ++++++++----------- .../Entities/Channels/ChannelHelper.cs | 2 +- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/src/Discord.Net.Rest/API/Rest/UploadFileParams.cs b/src/Discord.Net.Rest/API/Rest/UploadFileParams.cs index 5c06a033e1..9e909b50c4 100644 --- a/src/Discord.Net.Rest/API/Rest/UploadFileParams.cs +++ b/src/Discord.Net.Rest/API/Rest/UploadFileParams.cs @@ -30,28 +30,24 @@ public IReadOnlyDictionary ToDictionary() { var d = new Dictionary(); d["file"] = new MultipartFile(File, Filename.GetValueOrDefault("unknown.dat")); + + var payload = new Dictionary(); if (Content.IsSpecified) - d["content"] = Content.Value; + payload["content"] = Content.Value; if (IsTTS.IsSpecified) - d["tts"] = IsTTS.Value.ToString(); + payload["tts"] = IsTTS.Value.ToString(); if (Nonce.IsSpecified) - d["nonce"] = Nonce.Value; + payload["nonce"] = Nonce.Value; if (Embed.IsSpecified) - { - var payload = new StringBuilder(); - using (var text = new StringWriter(payload)) - using (var writer = new JsonTextWriter(text)) - { - var map = new Dictionary() - { - ["embed"] = Embed.Value, - }; - - _serializer.Serialize(writer, map); - } - d["payload_json"] = payload.ToString(); - - } + payload["embed"] = Embed.Value; + + var json = new StringBuilder(); + using (var text = new StringWriter(json)) + using (var writer = new JsonTextWriter(text)) + _serializer.Serialize(writer, payload); + + d["payload_json"] = json.ToString(); + return d; } } diff --git a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs index 710746896c..6784f7f6a9 100644 --- a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs +++ b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs @@ -180,7 +180,7 @@ public static async Task SendFileAsync(IMessageChannel channel, public static async Task SendFileAsync(IMessageChannel channel, BaseDiscordClient client, Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options) { - var args = new UploadFileParams(stream) { Filename = filename, Content = text, IsTTS = isTTS, Embed = embed?.ToModel() }; + var args = new UploadFileParams(stream) { Filename = filename, Content = text, IsTTS = isTTS, Embed = embed != null ? embed.ToModel() : Optional.Unspecified }; var model = await client.ApiClient.UploadFileAsync(channel.Id, args, options).ConfigureAwait(false); return RestUserMessage.Create(client, channel, client.CurrentUser, model); } From 1905fdec04152e1fd63e26ebf7707a1d11a4d0f3 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Tue, 20 Mar 2018 16:44:30 -0400 Subject: [PATCH 37/48] Add BanAsync extension to IGuildUser --- src/Discord.Net.Core/Extensions/UserExtensions.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Discord.Net.Core/Extensions/UserExtensions.cs b/src/Discord.Net.Core/Extensions/UserExtensions.cs index 863201cfe5..d3e968e393 100644 --- a/src/Discord.Net.Core/Extensions/UserExtensions.cs +++ b/src/Discord.Net.Core/Extensions/UserExtensions.cs @@ -46,5 +46,8 @@ public static async Task SendFileAsync(this IUser user, return await (await user.GetOrCreateDMChannelAsync().ConfigureAwait(false)).SendFileAsync(filePath, text, isTTS, embed, options).ConfigureAwait(false); } #endif + + public static Task BanAsync(this IGuildUser user, int pruneDays = 0, string reason = null, RequestOptions options = null) + => user.Guild.AddBanAsync(user, pruneDays, reason, options); } } From 55299ff14f4af6e9f046b2a33e78e32eb0523ade Mon Sep 17 00:00:00 2001 From: Quahu Date: Sat, 24 Mar 2018 00:49:45 +0100 Subject: [PATCH 38/48] Prevents NREs when sending/modifying messages (#993) --- src/Discord.Net.Rest/DiscordRestApiClient.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index 556d6fbe68..f0c4358ad2 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -470,7 +470,7 @@ public async Task CreateMessageAsync(ulong channelId, CreateMessagePara if (!args.Embed.IsSpecified || args.Embed.Value == null) Preconditions.NotNullOrEmpty(args.Content, nameof(args.Content)); - if (args.Content.Length > DiscordConfig.MaxMessageSize) + 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); @@ -487,7 +487,7 @@ public async Task CreateWebhookMessageAsync(ulong webhookId, CreateWebh 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) + 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); @@ -568,7 +568,7 @@ public async Task ModifyMessageAsync(ulong channelId, ulong messageId, { if (!args.Embed.IsSpecified) Preconditions.NotNullOrEmpty(args.Content, nameof(args.Content)); - if (args.Content.Value.Length > DiscordConfig.MaxMessageSize) + 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)); } options = RequestOptions.CreateOrClone(options); From 88e62440758c6a5d498be0a55180a77d9cd01bdd Mon Sep 17 00:00:00 2001 From: Chris Johnston Date: Sat, 24 Mar 2018 09:28:50 -0700 Subject: [PATCH 39/48] Add release version to docs footer, Add doc build instructions (#963) * Add guide for building the docs * Add version to the footer of the docs * fix links for readme * change formatting of doc build readme * proper capitalization of DocFX in readme * Remove code tags around version --- docs/README.md | 16 ++++++++++++++++ docs/docfx.json | 4 ++-- 2 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 docs/README.md diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000000..a672330d46 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,16 @@ +# Instructions for Building Documentation + +The documentation for the Discord.NET library uses [DocFX][docfx-main]. [Instructions for installing this tool can be found here.][docfx-installing] + +1. Navigate to the root of the repository. +2. (Optional) If you intend to target a specific version, ensure that you +have the correct version checked out. +3. Build the library. Run `dotnet build` in the root of this repository. + Ensure that the build passes without errors. +4. Build the docs using `docfx .\docs\docfx.json`. Add the `--serve` parameter +to preview the site locally. Some elements of the page may appear incorrect +when not hosted by a server. + - Remarks: According to the docfx website, this tool does work on Linux under mono. + +[docfx-main]: https://dotnet.github.io/docfx/ +[docfx-installing]: https://dotnet.github.io/docfx/tutorial/docfx_getting_started.html diff --git a/docs/docfx.json b/docs/docfx.json index 3c0b0611e8..50ae390926 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -67,8 +67,8 @@ "default" ], "globalMetadata": { - "_appFooter": "Discord.Net (c) 2015-2017" + "_appFooter": "Discord.Net (c) 2015-2018 2.0.0-beta" }, "noLangKeyword": false } -} \ No newline at end of file +} From 6d58796f2dae251afbbc6175354d27ed69118cfb Mon Sep 17 00:00:00 2001 From: advorange Date: Sat, 24 Mar 2018 10:11:43 -0700 Subject: [PATCH 40/48] Added in an all value for category channels. (#952) --- .../Entities/Permissions/ChannelPermissions.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs b/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs index 1a8aad53c0..ef10ee1067 100644 --- a/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs +++ b/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; @@ -13,6 +13,8 @@ public struct ChannelPermissions public static readonly ChannelPermissions Text = new ChannelPermissions(0b01100_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 category channels. + public static readonly ChannelPermissions Category = new ChannelPermissions(0b01100_1111110_1111111110001_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. @@ -24,6 +26,7 @@ public static ChannelPermissions All(IChannel channel) { case ITextChannel _: return Text; case IVoiceChannel _: return Voice; + case ICategoryChannel _: return Category; case IDMChannel _: return DM; case IGroupChannel _: return Group; default: throw new ArgumentException("Unknown channel type", nameof(channel)); @@ -157,4 +160,4 @@ public List ToList() public override string ToString() => RawValue.ToString(); private string DebuggerDisplay => $"{string.Join(", ", ToList())}"; } -} \ No newline at end of file +} From 810f6d610eb2f27db777a1e408a7954efb2cc1a3 Mon Sep 17 00:00:00 2001 From: Paulo Date: Sat, 24 Mar 2018 14:11:55 -0300 Subject: [PATCH 41/48] Fix SocketCategoryChannel properties (#945) * Update Users property for category channels * Wrong property being used for Channels property CategoryId is the category that "owns" this channel. That is actually impossible right now for category channels, so it returns null and get all channels wrongly. * Resolve permissions for category * Remove spaces * Small fix for IChannel.GetUsersAsync --- .../Channels/SocketCategoryChannel.cs | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs index d5a183b1e6..e7a165c2f1 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; @@ -15,10 +15,12 @@ namespace Discord.WebSocket public class SocketCategoryChannel : SocketGuildChannel, ICategoryChannel { public override IReadOnlyCollection Users - => Guild.Users.Where(x => x.VoiceChannel?.Id == Id).ToImmutableArray(); + => Guild.Users.Where(x => Permissions.GetValue( + Permissions.ResolveChannel(Guild, x, this, Permissions.ResolveGuild(Guild, x)), + ChannelPermission.ViewChannel)).ToImmutableArray(); public IReadOnlyCollection Channels - => Guild.Channels.Where(x => x.CategoryId == CategoryId).ToImmutableArray(); + => Guild.Channels.Where(x => x.CategoryId == Id).ToImmutableArray(); internal SocketCategoryChannel(DiscordSocketClient discord, ulong id, SocketGuild guild) : base(discord, id, guild) @@ -31,14 +33,28 @@ internal SocketCategoryChannel(DiscordSocketClient discord, ulong id, SocketGuil return entity; } + //Users + public override SocketGuildUser GetUser(ulong id) + { + var user = Guild.GetUser(id); + if (user != null) + { + var guildPerms = Permissions.ResolveGuild(Guild, user); + var channelPerms = Permissions.ResolveChannel(Guild, user, this, guildPerms); + if (Permissions.GetValue(channelPerms, ChannelPermission.ViewChannel)) + return user; + } + return null; + } + private string DebuggerDisplay => $"{Name} ({Id}, Category)"; internal new SocketCategoryChannel Clone() => MemberwiseClone() as SocketCategoryChannel; // IGuildChannel IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options) - => throw new NotSupportedException(); + => ImmutableArray.Create>(Users).ToAsyncEnumerable(); Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) - => throw new NotSupportedException(); + => Task.FromResult(GetUser(id)); Task IGuildChannel.CreateInviteAsync(int? maxAge, int? maxUses, bool isTemporary, bool isUnique, RequestOptions options) => throw new NotSupportedException(); Task> IGuildChannel.GetInvitesAsync(RequestOptions options) @@ -46,8 +62,8 @@ Task> IGuildChannel.GetInvitesAsync(Request //IChannel IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) - => throw new NotSupportedException(); + => ImmutableArray.Create>(Users).ToAsyncEnumerable(); Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) - => throw new NotSupportedException(); + => Task.FromResult(GetUser(id)); } } From d50fc3b4e1ebc42762d085a92adfb34e118ee0ee Mon Sep 17 00:00:00 2001 From: Joe4evr Date: Sat, 24 Mar 2018 18:12:34 +0100 Subject: [PATCH 42/48] Throw when attempting to modify a message not made by the current user (#992) * Throw when attempting to modify a message not made by the current user * Didn't realize the client is passed into the MessageHelper function * Respond to feedback --- src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs | 5 ++++- src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs | 2 +- .../Entities/Messages/SocketUserMessage.cs | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs b/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs index 47bb6f9262..8ae41cc372 100644 --- a/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs +++ b/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs @@ -1,4 +1,4 @@ -using Discord.API.Rest; +using Discord.API.Rest; using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -13,6 +13,9 @@ internal static class MessageHelper public static async Task ModifyAsync(IMessage msg, BaseDiscordClient client, Action func, RequestOptions options) { + if (msg.Author.Id != client.CurrentUser.Id) + throw new InvalidOperationException("Only the author of a message may change it."); + var args = new MessageProperties(); func(args); var apiArgs = new API.Rest.ModifyMessageParams diff --git a/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs b/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs index e5eed874e5..0d1f3be2bb 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs index b240645e55..5489ad2bb7 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs @@ -1,4 +1,4 @@ -using Discord.Rest; +using Discord.Rest; using System; using System.Collections.Generic; using System.Collections.Immutable; From 2988b38ea80cb4778e4d0993d827c2cedd2f5481 Mon Sep 17 00:00:00 2001 From: Alex Gravely Date: Fri, 30 Mar 2018 15:36:58 -0400 Subject: [PATCH 43/48] Resolve mutability issues with EmbedBuilder. (#1010) * Create new entities on each build call. Added Length property to EmbedBuilder. * Resolve Length issues per #1012 --- .../Entities/Messages/EmbedBuilder.cs | 138 +++++++++--------- 1 file changed, 68 insertions(+), 70 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs b/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs index f5663cea3f..62834ebf34 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs @@ -1,89 +1,105 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Linq; namespace Discord { public class EmbedBuilder { - private readonly Embed _embed; + private string _title; + private string _description; + private string _url; + private EmbedImage? _image; + private EmbedThumbnail? _thumbnail; + private List _fields; 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 const int MaxEmbedLength = 6000; public EmbedBuilder() { - _embed = new Embed(EmbedType.Rich); Fields = new List(); } public string Title { - get => _embed.Title; + get => _title; set { if (value?.Length > MaxTitleLength) throw new ArgumentException($"Title length must be less than or equal to {MaxTitleLength}.", nameof(Title)); - _embed.Title = value; + _title = value; } } - public string Description { - get => _embed.Description; + get => _description; set { if (value?.Length > MaxDescriptionLength) throw new ArgumentException($"Description length must be less than or equal to {MaxDescriptionLength}.", nameof(Description)); - _embed.Description = value; + _description = value; } } public string Url { - get => _embed.Url; + get => _url; set { if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(Url)); - _embed.Url = value; + _url = value; } } public string ThumbnailUrl { - get => _embed.Thumbnail?.Url; + get => _thumbnail?.Url; set { if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(ThumbnailUrl)); - _embed.Thumbnail = new EmbedThumbnail(value, null, null, null); + _thumbnail = new EmbedThumbnail(value, null, null, null); } } public string ImageUrl { - get => _embed.Image?.Url; + get => _image?.Url; set { if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(ImageUrl)); - _embed.Image = new EmbedImage(value, null, null, null); + _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; } - private List _fields; public List Fields { get => _fields; set { - 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; } } + public DateTimeOffset? Timestamp { get; set; } + public Color? Color { get; set; } + public EmbedAuthorBuilder Author { get; set; } + public EmbedFooterBuilder Footer { get; set; } + + public int Length + { + get + { + int titleLength = Title?.Length ?? 0; + int authorLength = Author?.Name?.Length ?? 0; + int descriptionLength = Description?.Length ?? 0; + int footerLength = Footer?.Text?.Length ?? 0; + int fieldSum = Fields.Sum(f => f.Name.Length + f.Value.ToString().Length); + + return titleLength + authorLength + descriptionLength + footerLength + fieldSum; + } + } + public EmbedBuilder WithTitle(string title) { Title = title; @@ -180,7 +196,6 @@ public EmbedBuilder AddField(string name, object value, bool inline = false) AddField(field); return this; } - public EmbedBuilder AddField(EmbedFieldBuilder field) { if (Fields.Count >= MaxFieldCount) @@ -195,63 +210,54 @@ public EmbedBuilder AddField(Action action) { var field = new EmbedFieldBuilder(); action(field); - this.AddField(field); + AddField(field); return this; } public Embed Build() { - _embed.Footer = Footer?.Build(); - _embed.Author = Author?.Build(); + if (Length > MaxEmbedLength) + throw new InvalidOperationException($"Total embed length must be less than or equal to {MaxEmbedLength}"); + var fields = ImmutableArray.CreateBuilder(Fields.Count); 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; + return new Embed(EmbedType.Rich, Title, Description, Url, Timestamp, Color, _image, null, Author?.Build(), Footer?.Build(), null, _thumbnail, fields.ToImmutable()); } } public class EmbedFieldBuilder { + private string _name; + private string _value; private EmbedField _field; - public const int MaxFieldNameLength = 256; public const int MaxFieldValueLength = 1024; public string Name { - get => _field.Name; + get => _name; set { if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException($"Field name must not be null, empty or entirely whitespace.", 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; + _name = value; } } public object Value { - get => _field.Value; + get => _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; + _value = stringValue; } } - public bool IsInline { get => _field.Inline; set { _field.Inline = value; } } - - public EmbedFieldBuilder() - { - _field = new EmbedField(); - } + public bool IsInline { get; set; } public EmbedFieldBuilder WithName(string name) { @@ -270,48 +276,44 @@ public EmbedFieldBuilder WithIsInline(bool isInline) } public EmbedField Build() - => _field; + => new EmbedField(Name, Value.ToString(), IsInline); } public class EmbedAuthorBuilder { - private EmbedAuthor _author; - + private string _name; + private string _url; + private string _iconUrl; public const int MaxAuthorNameLength = 256; public string Name { - get => _author.Name; + get => _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; + _name = value; } } public string Url { - get => _author.Url; + get => _url; set { if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(Url)); - _author.Url = value; + _url = value; } } public string IconUrl { - get => _author.IconUrl; + get => _iconUrl; set { if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(IconUrl)); - _author.IconUrl = value; + _iconUrl = value; } } - public EmbedAuthorBuilder() - { - _author = new EmbedAuthor(); - } - public EmbedAuthorBuilder WithName(string name) { Name = name; @@ -329,39 +331,35 @@ public EmbedAuthorBuilder WithIconUrl(string iconUrl) } public EmbedAuthor Build() - => _author; + => new EmbedAuthor(Name, Url, IconUrl, null); } public class EmbedFooterBuilder { - private EmbedFooter _footer; + private string _text; + private string _iconUrl; public const int MaxFooterTextLength = 2048; public string Text { - get => _footer.Text; + get => _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; + _text = value; } } public string IconUrl { - get => _footer.IconUrl; + get => _iconUrl; set { if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(IconUrl)); - _footer.IconUrl = value; + _iconUrl = value; } } - public EmbedFooterBuilder() - { - _footer = new EmbedFooter(); - } - public EmbedFooterBuilder WithText(string text) { Text = text; @@ -374,6 +372,6 @@ public EmbedFooterBuilder WithIconUrl(string iconUrl) } public EmbedFooter Build() - => _footer; + => new EmbedFooter(Text, IconUrl, null); } } From 6b7c6e9667105d51d589585dcafb43500e722e28 Mon Sep 17 00:00:00 2001 From: Paulo Date: Fri, 30 Mar 2018 16:40:43 -0300 Subject: [PATCH 44/48] Add new overload for AddTypeReader (#1009) --- .../Builders/ModuleClassBuilder.cs | 2 +- src/Discord.Net.Commands/CommandService.cs | 49 ++++++++++++++++--- 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs index e9ce9eb862..1dd66cc77c 100644 --- a/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs +++ b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs @@ -291,7 +291,7 @@ private static TypeReader GetTypeReader(CommandService service, Type paramType, //We dont have a cached type reader, create one reader = ReflectionUtils.CreateObject(typeReaderType.GetTypeInfo(), service, services); - service.AddTypeReader(paramType, reader); + service.AddTypeReader(paramType, reader, false); return reader; } diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index f4fbcf8b2a..7c7cdf7c58 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -215,10 +215,11 @@ private bool RemoveModuleInternal(ModuleInfo module) return true; } - //Type Readers + //Type Readers /// /// Adds a custom to this for the supplied object type. /// If is a , a will also be added. + /// If a default exists for , a warning will be logged and the default will be replaced. /// /// The object type to be read by the . /// An instance of the to be added. @@ -226,17 +227,53 @@ public void AddTypeReader(TypeReader reader) => AddTypeReader(typeof(T), reader); /// /// Adds a custom to this for the supplied object type. - /// If is a , a for the value type will also be added. + /// If is a , a for the value type will also be added. + /// If a default exists for , a warning will be logged and the default will be replaced. /// /// A instance for the type to be read. /// An instance of the to be added. public void AddTypeReader(Type type, TypeReader reader) { - var readers = _typeReaders.GetOrAdd(type, x => new ConcurrentDictionary()); - readers[reader.GetType()] = reader; + if (_defaultTypeReaders.ContainsKey(type)) + _cmdLogger.WarningAsync($"The default TypeReader for {type.FullName} was replaced by {reader.GetType().FullName}. To suppress this message, use the overload that has a Boolean to replace the default one and pass true.").GetAwaiter().GetResult(); + AddTypeReader(type, reader, true); + } + /// + /// Adds a custom to this for the supplied object type. + /// If is a , a will also be added. + /// + /// The object type to be read by the . + /// An instance of the to be added. + /// If should replace the default for if one exists. + public void AddTypeReader(TypeReader reader, bool replaceDefaultTypeReader) + => AddTypeReader(typeof(T), reader, replaceDefaultTypeReader); + /// + /// Adds a custom to this for the supplied object type. + /// If is a , a for the value type will also be added. + /// + /// A instance for the type to be read. + /// An instance of the to be added. + /// If should replace the default for if one exists. + public void AddTypeReader(Type type, TypeReader reader, bool replaceDefaultTypeReader) + { + if (replaceDefaultTypeReader && _defaultTypeReaders.ContainsKey(type)) + { + _defaultTypeReaders.AddOrUpdate(type, reader, (k, v) => reader); + if (type.GetTypeInfo().IsValueType) + { + var nullableType = typeof(Nullable<>).MakeGenericType(type); + var nullableReader = NullableTypeReader.Create(type, reader); + _defaultTypeReaders.AddOrUpdate(nullableType, nullableReader, (k, v) => nullableReader); + } + } + else + { + var readers = _typeReaders.GetOrAdd(type, x => new ConcurrentDictionary()); + readers[reader.GetType()] = reader; - if (type.GetTypeInfo().IsValueType) - AddNullableTypeReader(type, reader); + if (type.GetTypeInfo().IsValueType) + AddNullableTypeReader(type, reader); + } } internal void AddNullableTypeReader(Type valueType, TypeReader valueTypeReader) { From b918712ad2826a64f784a8c5c3882ff737ce814a Mon Sep 17 00:00:00 2001 From: Christopher F Date: Fri, 30 Mar 2018 15:51:28 -0400 Subject: [PATCH 45/48] Cleanup of #1009 --- src/Discord.Net.Commands/CommandService.cs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index 7c7cdf7c58..c880dd4547 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -235,7 +235,8 @@ public void AddTypeReader(TypeReader reader) public void AddTypeReader(Type type, TypeReader reader) { if (_defaultTypeReaders.ContainsKey(type)) - _cmdLogger.WarningAsync($"The default TypeReader for {type.FullName} was replaced by {reader.GetType().FullName}. To suppress this message, use the overload that has a Boolean to replace the default one and pass true.").GetAwaiter().GetResult(); + _ = _cmdLogger.WarningAsync($"The default TypeReader for {type.FullName} was replaced by {reader.GetType().FullName}." + + $"To suppress this message, use AddTypeReader(reader, true)."); AddTypeReader(type, reader, true); } /// @@ -244,19 +245,19 @@ public void AddTypeReader(Type type, TypeReader reader) /// /// The object type to be read by the . /// An instance of the to be added. - /// If should replace the default for if one exists. - public void AddTypeReader(TypeReader reader, bool replaceDefaultTypeReader) - => AddTypeReader(typeof(T), reader, replaceDefaultTypeReader); + /// If should replace the default for if one exists. + public void AddTypeReader(TypeReader reader, bool replaceDefault) + => AddTypeReader(typeof(T), reader, replaceDefault); /// /// Adds a custom to this for the supplied object type. /// If is a , a for the value type will also be added. /// /// A instance for the type to be read. /// An instance of the to be added. - /// If should replace the default for if one exists. - public void AddTypeReader(Type type, TypeReader reader, bool replaceDefaultTypeReader) + /// If should replace the default for if one exists. + public void AddTypeReader(Type type, TypeReader reader, bool replaceDefault) { - if (replaceDefaultTypeReader && _defaultTypeReaders.ContainsKey(type)) + if (replaceDefault && _defaultTypeReaders.ContainsKey(type)) { _defaultTypeReaders.AddOrUpdate(type, reader, (k, v) => reader); if (type.GetTypeInfo().IsValueType) From c67db8896113615f2404cd7b5c2a121528c3bb44 Mon Sep 17 00:00:00 2001 From: HelpfulStranger999 Date: Sun, 25 Mar 2018 13:25:49 -0500 Subject: [PATCH 46/48] Cleaned up and refactored slightly --- 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 f0d406e8d4..6e74c8abc5 100644 --- a/src/Discord.Net.Commands/Info/CommandInfo.cs +++ b/src/Discord.Net.Commands/Info/CommandInfo.cs @@ -165,11 +165,11 @@ public async Task ExecuteAsync(ICommandContext context, IEnumerable { - await ExecuteAsyncInternalAsync(context, args, services).ConfigureAwait(false); + await ExecuteInternalAsync(context, args, services).ConfigureAwait(false); }); break; } @@ -181,7 +181,7 @@ public async Task ExecuteAsync(ICommandContext context, IEnumerable ExecuteAsyncInternalAsync(ICommandContext context, object[] args, IServiceProvider services) + private async Task ExecuteInternalAsync(ICommandContext context, object[] args, IServiceProvider services) { await Module.Service._cmdLogger.DebugAsync($"Executing {GetLogText(context)}").ConfigureAwait(false); try From 109f663a9adc450823f9cd91da03812256553f51 Mon Sep 17 00:00:00 2001 From: Fyers Date: Sun, 1 Apr 2018 20:02:50 +0200 Subject: [PATCH 47/48] added UserDefaultAvatar to IUser (#973) * added UserDefaultAvatar to IUser * pass ushort as discriminator * removed unneeded ushort.parse --- src/Discord.Net.Core/CDN.cs | 19 ++++++++++++++----- src/Discord.Net.Core/Entities/Users/IUser.cs | 2 ++ .../Entities/Users/RestUser.cs | 5 ++++- .../Entities/Users/SocketUser.cs | 13 ++++++++----- 4 files changed, 28 insertions(+), 11 deletions(-) diff --git a/src/Discord.Net.Core/CDN.cs b/src/Discord.Net.Core/CDN.cs index f23f552387..52b9a28781 100644 --- a/src/Discord.Net.Core/CDN.cs +++ b/src/Discord.Net.Core/CDN.cs @@ -13,6 +13,10 @@ public static string GetUserAvatarUrl(ulong userId, string avatarId, ushort size string extension = FormatToExtension(format, avatarId); return $"{DiscordConfig.CDNUrl}avatars/{userId}/{avatarId}.{extension}?size={size}"; } + public static string GetDefaultUserAvatarUrl(ushort discriminator) + { + return $"{DiscordConfig.CDNUrl}embed/avatars/{discriminator % 5}.png"; + } 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) @@ -37,11 +41,16 @@ private static string FormatToExtension(ImageFormat format, string imageId) 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)); + 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/IUser.cs b/src/Discord.Net.Core/Entities/Users/IUser.cs index e3f270f6f2..c5cce7a25c 100644 --- a/src/Discord.Net.Core/Entities/Users/IUser.cs +++ b/src/Discord.Net.Core/Entities/Users/IUser.cs @@ -8,6 +8,8 @@ public interface IUser : ISnowflakeEntity, IMentionable, IPresence string AvatarId { get; } /// Gets the url to this user's avatar. string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128); + /// Gets the url to this user's default avatar. + string GetDefaultAvatarUrl(); /// 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 c6cf6103a3..c484986b12 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestUser.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Diagnostics; using System.Threading.Tasks; using Model = Discord.API.User; @@ -60,6 +60,9 @@ public Task GetOrCreateDMChannelAsync(RequestOptions options = nu public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) => CDN.GetUserAvatarUrl(Id, AvatarId, size, format); + public string GetDefaultAvatarUrl() + => CDN.GetDefaultUserAvatarUrl(DiscriminatorValue); + public override string ToString() => $"{Username}#{Discriminator}"; private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")})"; diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs index 58d5c62a17..00899d47eb 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs @@ -1,4 +1,4 @@ -using Discord.Rest; +using Discord.Rest; using System; using System.Threading.Tasks; using Model = Discord.API.User; @@ -37,23 +37,23 @@ internal virtual bool Update(ClientState state, Model model) { 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; hasChanges = true; } if (model.Username.IsSpecified && model.Username.Value != Username) - { + { Username = model.Username.Value; hasChanges = true; } return hasChanges; - } + } public async Task GetOrCreateDMChannelAsync(RequestOptions options = null) => GlobalUser.DMChannel ?? await UserHelper.CreateDMChannelAsync(this, Discord, options) as IDMChannel; @@ -61,6 +61,9 @@ public async Task GetOrCreateDMChannelAsync(RequestOptions options = public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) => CDN.GetUserAvatarUrl(Id, AvatarId, size, format); + public string GetDefaultAvatarUrl() + => CDN.GetDefaultUserAvatarUrl(DiscriminatorValue); + public override string ToString() => $"{Username}#{Discriminator}"; private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")})"; internal SocketUser Clone() => MemberwiseClone() as SocketUser; From b8b59d97ae788a9d971258010bced393dea365f6 Mon Sep 17 00:00:00 2001 From: Joe4evr Date: Fri, 6 Apr 2018 23:28:54 +0200 Subject: [PATCH 48/48] Forward all embed-related types for non-updated addons (#1021) --- src/Discord.Net.Rest/AssemblyInfo.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/Discord.Net.Rest/AssemblyInfo.cs b/src/Discord.Net.Rest/AssemblyInfo.cs index 126365e473..9c90919afb 100644 --- a/src/Discord.Net.Rest/AssemblyInfo.cs +++ b/src/Discord.Net.Rest/AssemblyInfo.cs @@ -6,5 +6,17 @@ [assembly: InternalsVisibleTo("Discord.Net.Commands")] [assembly: InternalsVisibleTo("Discord.Net.Tests")] +[assembly: TypeForwardedTo(typeof(Discord.Embed))] [assembly: TypeForwardedTo(typeof(Discord.EmbedBuilder))] [assembly: TypeForwardedTo(typeof(Discord.EmbedBuilderExtensions))] +[assembly: TypeForwardedTo(typeof(Discord.EmbedAuthor))] +[assembly: TypeForwardedTo(typeof(Discord.EmbedAuthorBuilder))] +[assembly: TypeForwardedTo(typeof(Discord.EmbedField))] +[assembly: TypeForwardedTo(typeof(Discord.EmbedFieldBuilder))] +[assembly: TypeForwardedTo(typeof(Discord.EmbedFooter))] +[assembly: TypeForwardedTo(typeof(Discord.EmbedFooterBuilder))] +[assembly: TypeForwardedTo(typeof(Discord.EmbedImage))] +[assembly: TypeForwardedTo(typeof(Discord.EmbedProvider))] +[assembly: TypeForwardedTo(typeof(Discord.EmbedThumbnail))] +[assembly: TypeForwardedTo(typeof(Discord.EmbedType))] +[assembly: TypeForwardedTo(typeof(Discord.EmbedVideo))]