diff --git a/Discord.Net.sln b/Discord.Net.sln
index cac6c9064..daf902b96 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/Discord.Net.targets b/Discord.Net.targets
index 3f623c619..958b2053f 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/appveyor.yml b/appveyor.yml
index 393485fee..54b9a1251 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/docs/README.md b/docs/README.md
new file mode 100644
index 000000000..a672330d4
--- /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 3c0b0611e..50ae39092 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
+}
diff --git a/docs/guides/commands/samples/module.cs b/docs/guides/commands/samples/module.cs
index 5014619da..1e3555501 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 class Sample : ModuleBase
var userInfo = user ?? Context.Client.CurrentUser;
await ReplyAsync($"{userInfo.Username}#{userInfo.Discriminator}");
}
-}
\ No newline at end of file
+}
diff --git a/docs/guides/getting_started/samples/intro/structure.cs b/docs/guides/getting_started/samples/intro/structure.cs
index bdfc12b67..a9a018c3a 100644
--- a/docs/guides/getting_started/samples/intro/structure.cs
+++ b/docs/guides/getting_started/samples/intro/structure.cs
@@ -19,10 +19,10 @@ class Program
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 @@ class 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 @@ class Program
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 @@ class Program
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 @@ class Program
// 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.Analyzers/Discord.Net.Analyzers.csproj b/src/Discord.Net.Analyzers/Discord.Net.Analyzers.csproj
new file mode 100644
index 000000000..8ab398ff5
--- /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 000000000..0760d019f
--- /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 000000000..680de66b5
--- /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 000000000..0c1b8098f
--- /dev/null
+++ b/src/Discord.Net.Analyzers/docs/DNET0001.md
@@ -0,0 +1,30 @@
+# DNET0001
+
+
+
+ TypeName |
+ GuildAccessAnalyzer |
+
+
+ CheckId |
+ DNET0001 |
+
+
+ Category |
+ API 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/src/Discord.Net.Commands/Attributes/AliasAttribute.cs b/src/Discord.Net.Commands/Attributes/AliasAttribute.cs
index 6e115bd60..6cd0abbb7 100644
--- a/src/Discord.Net.Commands/Attributes/AliasAttribute.cs
+++ b/src/Discord.Net.Commands/Attributes/AliasAttribute.cs
@@ -3,7 +3,7 @@ using System;
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 5ae6092eb..5f8e9ceaf 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 d6a1c646e..cc23a6d15 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 bd966e129..c982d93a1 100644
--- a/src/Discord.Net.Commands/Attributes/DontInjectAttribute.cs
+++ b/src/Discord.Net.Commands/Attributes/DontInjectAttribute.cs
@@ -2,7 +2,7 @@ using System;
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 105d256ec..b1760d149 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 0a5156fee..4a4b2bfed 100644
--- a/src/Discord.Net.Commands/Attributes/NameAttribute.cs
+++ b/src/Discord.Net.Commands/Attributes/NameAttribute.cs
@@ -3,7 +3,7 @@ using System;
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 37f685c95..44ab6d214 100644
--- a/src/Discord.Net.Commands/Attributes/OverrideTypeReaderAttribute.cs
+++ b/src/Discord.Net.Commands/Attributes/OverrideTypeReaderAttribute.cs
@@ -4,7 +4,7 @@ using System.Reflection;
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 @@ namespace Discord.Commands
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 209822583..3c5e8cf92 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 @@ namespace Discord.Commands
{
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 5fa0fb1b9..90af035e4 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 @@ namespace Discord.Commands
///
/// 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 c8e3bfa82..273c764bd 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 e370aeec4..93e3cbe18 100644
--- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs
+++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs
@@ -1,7 +1,5 @@
-#pragma warning disable CS0618
using System;
using System.Threading.Tasks;
-using Microsoft.Extensions.DependencyInjection;
namespace Discord.Commands
{
@@ -9,7 +7,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)
@@ -21,10 +19,6 @@ namespace Discord.Commands
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.Commands/Attributes/PriorityAttribute.cs b/src/Discord.Net.Commands/Attributes/PriorityAttribute.cs
index 5120bb7d1..353e96e41 100644
--- a/src/Discord.Net.Commands/Attributes/PriorityAttribute.cs
+++ b/src/Discord.Net.Commands/Attributes/PriorityAttribute.cs
@@ -3,7 +3,7 @@ using System;
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 4aa16bebb..56938f167 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 44db18a79..c11f790a7 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 46d52f3d9..641163408 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; }
diff --git a/src/Discord.Net.Commands/Builders/ModuleBuilder.cs b/src/Discord.Net.Commands/Builders/ModuleBuilder.cs
index 0a33c9e26..1809c2c63 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 @@ namespace Discord.Commands.Builders
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 @@ namespace Discord.Commands.Builders
public IReadOnlyList Attributes => _attributes;
public IReadOnlyList Aliases => _aliases;
+ internal TypeInfo TypeInfo { get; set; }
+
//Automatic
internal ModuleBuilder(CommandService service, ModuleBuilder parent)
{
@@ -111,17 +115,23 @@ namespace Discord.Commands.Builders
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 5a3a1f25a..1dd66cc77 100644
--- a/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs
+++ b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs
@@ -42,14 +42,13 @@ namespace Discord.Commands
}
- 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");*/
- var topLevelGroups = validTypes.Where(x => x.DeclaringType == null);
- var subGroups = validTypes.Intersect(topLevelGroups);
+ var topLevelGroups = validTypes.Where(x => x.DeclaringType == null || !IsValidModuleDefinition(x.DeclaringType.GetTypeInfo()));
var builtTypes = new List();
@@ -63,11 +62,11 @@ namespace Discord.Commands
var module = new ModuleBuilder(service, null);
- 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);
- result[typeInfo.AsType()] = module.Build(service);
+ result[typeInfo.AsType()] = module.Build(service, services);
}
await service._cmdLogger.DebugAsync($"Successfully built {builtTypes.Count} modules.").ConfigureAwait(false);
@@ -75,7 +74,7 @@ namespace Discord.Commands
return result;
}
- private static void BuildSubTypes(ModuleBuilder builder, 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 +86,18 @@ namespace Discord.Commands
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 +117,7 @@ namespace Discord.Commands
break;
case GroupAttribute group:
builder.Name = builder.Name ?? group.Prefix;
+ builder.Group = group.Prefix;
builder.AddAliases(group.Prefix);
break;
case PreconditionAttribute precondition:
@@ -140,12 +141,12 @@ namespace Discord.Commands
{
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 +192,7 @@ namespace Discord.Commands
{
builder.AddParameter((parameter) =>
{
- BuildParameter(parameter, paramInfo, pos++, count, service);
+ BuildParameter(parameter, paramInfo, pos++, count, service, serviceprovider);
});
}
@@ -227,7 +228,7 @@ namespace Discord.Commands
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 +246,7 @@ namespace Discord.Commands
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;
@@ -273,19 +274,12 @@ namespace Discord.Commands
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;
}
}
- 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,8 +290,8 @@ namespace Discord.Commands
}
//We dont have a cached type reader, create one
- reader = ReflectionUtils.CreateObject(typeReaderType.GetTypeInfo(), service, EmptyServiceProvider.Instance);
- service.AddTypeReader(paramType, reader);
+ reader = ReflectionUtils.CreateObject(typeReaderType.GetTypeInfo(), service, services);
+ service.AddTypeReader(paramType, reader, false);
return reader;
}
@@ -305,7 +299,8 @@ namespace Discord.Commands
private static bool IsValidModuleDefinition(TypeInfo typeInfo)
{
return _moduleTypeInfo.IsAssignableFrom(typeInfo) &&
- !typeInfo.IsAbstract;
+ !typeInfo.IsAbstract &&
+ !typeInfo.ContainsGenericParameters;
}
private static bool IsValidCommandDefinition(MethodInfo methodInfo)
diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs
index 8e7dab898..c880dd454 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.Linq;
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 @@ namespace Discord.Commands
var builder = new ModuleBuilder(this, null, primaryAlias);
buildFunc(builder);
- var module = builder.Build(this);
+ var module = builder.Build(this, null);
+
return LoadModuleInternal(module);
}
finally
@@ -93,9 +95,18 @@ namespace Discord.Commands
_moduleLock.Release();
}
}
- public Task AddModuleAsync() => AddModuleAsync(typeof(T));
- public async Task AddModuleAsync(Type type)
+
+ ///
+ /// 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;
+
await _moduleLock.WaitAsync().ConfigureAwait(false);
try
{
@@ -104,7 +115,7 @@ namespace Discord.Commands
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 +129,21 @@ namespace Discord.Commands
_moduleLock.Release();
}
}
- public async Task> AddModulesAsync(Assembly assembly)
+ ///
+ /// 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;
+
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)
{
@@ -196,10 +215,11 @@ namespace Discord.Commands
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.
@@ -207,24 +227,61 @@ namespace Discord.Commands
=> 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 AddTypeReader(reader, true).");
+ 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 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 replaceDefault)
+ {
+ if (replaceDefault && _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)
{
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))
@@ -272,9 +329,9 @@ namespace Discord.Commands
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;
@@ -330,7 +387,7 @@ namespace Discord.Commands
float CalculateScore(CommandMatch match, ParseResult parseResult)
{
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;
diff --git a/src/Discord.Net.Commands/CommandServiceConfig.cs b/src/Discord.Net.Commands/CommandServiceConfig.cs
index 7fdbe368b..77c5b2262 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 @@
/// 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 479724ae3..3b641ec5f 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 @@
void BeforeExecute(CommandInfo command);
void AfterExecute(CommandInfo command);
+
+ void OnModuleBuilding(CommandService commandService, ModuleBuilder builder);
}
}
diff --git a/src/Discord.Net.Commands/Info/CommandInfo.cs b/src/Discord.Net.Commands/Info/CommandInfo.cs
index f0d406e8d..6e74c8abc 100644
--- a/src/Discord.Net.Commands/Info/CommandInfo.cs
+++ b/src/Discord.Net.Commands/Info/CommandInfo.cs
@@ -165,11 +165,11 @@ namespace Discord.Commands
switch (RunMode)
{
case RunMode.Sync: //Always sync
- return await ExecuteAsyncInternalAsync(context, args, services).ConfigureAwait(false);
+ return await ExecuteInternalAsync(context, args, services).ConfigureAwait(false);
case RunMode.Async: //Always async
var t2 = Task.Run(async () =>
{
- await ExecuteAsyncInternalAsync(context, args, services).ConfigureAwait(false);
+ await ExecuteInternalAsync(context, args, services).ConfigureAwait(false);
});
break;
}
@@ -181,7 +181,7 @@ namespace Discord.Commands
}
}
- private async Task 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
diff --git a/src/Discord.Net.Commands/Info/ModuleInfo.cs b/src/Discord.Net.Commands/Info/ModuleInfo.cs
index 97b90bf4e..5a7f9208e 100644
--- a/src/Discord.Net.Commands/Info/ModuleInfo.cs
+++ b/src/Discord.Net.Commands/Info/ModuleInfo.cs
@@ -2,7 +2,7 @@ using System;
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 @@ namespace Discord.Commands
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 @@ namespace Discord.Commands
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 @@ namespace Discord.Commands
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 f51656e40..3e6fbbd9b 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
{
@@ -10,7 +11,13 @@ namespace Discord.Commands
{
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);
}
@@ -23,15 +30,18 @@ namespace Discord.Commands
{
}
+ 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);
}
}
diff --git a/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs b/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs
index ab88f66ae..30dd7c36b 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;
diff --git a/src/Discord.Net.Core/CDN.cs b/src/Discord.Net.Core/CDN.cs
index 070b965ee..52b9a2878 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
{
@@ -13,6 +13,10 @@ namespace Discord
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)
@@ -28,17 +32,25 @@ namespace Discord
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)
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/Activities/ActivityType.cs b/src/Discord.Net.Core/Entities/Activities/ActivityType.cs
new file mode 100644
index 000000000..c7db7b247
--- /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 f2b7e8eb6..179ad4eaa 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
{
@@ -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/GameAsset.cs b/src/Discord.Net.Core/Entities/Activities/GameAsset.cs
index 385f37214..02c29ba41 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/GameParty.cs b/src/Discord.Net.Core/Entities/Activities/GameParty.cs
index dbfe5b6ce..54e6deef4 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.Core/Entities/Activities/IActivity.cs b/src/Discord.Net.Core/Entities/Activities/IActivity.cs
index 0dcf34273..1f158217d 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/RichGame.cs b/src/Discord.Net.Core/Entities/Activities/RichGame.cs
index e66eac1d2..fc3f68cf0 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 @@ namespace Discord
{
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 @@ namespace Discord
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 000000000..a20384242
--- /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 IEnumerable 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.Core/Entities/Activities/StreamingGame.cs b/src/Discord.Net.Core/Entities/Activities/StreamingGame.cs
index 140024272..afbc24cd9 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/Channels/IMessageChannel.cs b/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs
index a1df5b4c7..5a6e5df59 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 @@ namespace Discord
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.Rest/Entities/Messages/EmbedBuilder.cs b/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs
similarity index 80%
rename from src/Discord.Net.Rest/Entities/Messages/EmbedBuilder.cs
rename to src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs
index f5663cea3..62834ebf3 100644
--- a/src/Discord.Net.Rest/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 @@ namespace Discord
AddField(field);
return this;
}
-
public EmbedBuilder AddField(EmbedFieldBuilder field)
{
if (Fields.Count >= MaxFieldCount)
@@ -195,63 +210,54 @@ namespace Discord
{
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 @@ namespace Discord
}
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 @@ namespace Discord
}
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 @@ namespace Discord
}
public EmbedFooter Build()
- => _footer;
+ => new EmbedFooter(Text, IconUrl, null);
}
}
diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedType.cs b/src/Discord.Net.Core/Entities/Messages/EmbedType.cs
index 469e968a5..5bb2653e2 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,
}
}
diff --git a/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs b/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs
index 1a8aad53c..ef10ee106 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 @@ namespace Discord
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 @@ namespace Discord
{
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 @@ namespace Discord
public override string ToString() => RawValue.ToString();
private string DebuggerDisplay => $"{string.Join(", ", ToList())}";
}
-}
\ No newline at end of file
+}
diff --git a/src/Discord.Net.Core/Entities/Users/IUser.cs b/src/Discord.Net.Core/Entities/Users/IUser.cs
index e3f270f6f..c5cce7a25 100644
--- a/src/Discord.Net.Core/Entities/Users/IUser.cs
+++ b/src/Discord.Net.Core/Entities/Users/IUser.cs
@@ -8,6 +8,8 @@ namespace Discord
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.Core/Entities/Users/StreamType.cs b/src/Discord.Net.Core/Entities/Users/StreamType.cs
deleted file mode 100644
index 7622e3d6e..000000000
--- 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.Core/Extensions/AsyncEnumerableExtensions.cs b/src/Discord.Net.Core/Extensions/AsyncEnumerableExtensions.cs
index 345154f1d..dd16d2943 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 @@ namespace Discord
public static 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());
}
}
}
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.Core/Extensions/UserExtensions.cs b/src/Discord.Net.Core/Extensions/UserExtensions.cs
index eac25391e..d3e968e39 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 @@ namespace Discord
///
/// 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,24 +25,29 @@ namespace Discord
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
+
+ public static Task BanAsync(this IGuildUser user, int pruneDays = 0, string reason = null, RequestOptions options = null)
+ => user.Guild.AddBanAsync(user, pruneDays, reason, options);
}
}
diff --git a/src/Discord.Net.Core/IDiscordClient.cs b/src/Discord.Net.Core/IDiscordClient.cs
index 9abb959b5..a383c37da 100644
--- a/src/Discord.Net.Core/IDiscordClient.cs
+++ b/src/Discord.Net.Core/IDiscordClient.cs
@@ -36,5 +36,7 @@ namespace Discord
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.Core/Net/HttpException.cs b/src/Discord.Net.Core/Net/HttpException.cs
index 1c872245c..d0ee65b23 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 @@ namespace Discord.Net
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 000000000..d3c708dd5
--- /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 e8572f911..2d34d7bc2 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.Core/TokenType.cs b/src/Discord.Net.Core/TokenType.cs
index c351b1c19..62181420a 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.Core/Utils/Comparers.cs b/src/Discord.Net.Core/Utils/Comparers.cs
new file mode 100644
index 000000000..d7641e897
--- /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;
+ }
+ }
+ }
+}
diff --git a/src/Discord.Net.Core/Utils/Permissions.cs b/src/Discord.Net.Core/Utils/Permissions.cs
index 367926dd1..04e6784c3 100644
--- a/src/Discord.Net.Core/Utils/Permissions.cs
+++ b/src/Discord.Net.Core/Utils/Permissions.cs
@@ -80,7 +80,7 @@ namespace Discord
}
[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)]
@@ -133,9 +133,10 @@ namespace Discord
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;
@@ -160,10 +161,10 @@ namespace Discord
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)
@@ -172,4 +173,4 @@ namespace Discord
return resolvedPermissions;
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Discord.Net.Providers.WS4Net/Discord.Net.Providers.WS4Net.csproj b/src/Discord.Net.Providers.WS4Net/Discord.Net.Providers.WS4Net.csproj
index 78987e739..bfd0983ce 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 93d6a83d6..1894a8906 100644
--- a/src/Discord.Net.Providers.WS4Net/WS4NetClient.cs
+++ b/src/Discord.Net.Providers.WS4Net/WS4NetClient.cs
@@ -66,7 +66,7 @@ namespace Discord.Net.Providers.WS4Net
_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 @@ namespace Discord.Net.Providers.WS4Net
Closed(ex).GetAwaiter().GetResult();
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Discord.Net.Rest/API/Common/Game.cs b/src/Discord.Net.Rest/API/Common/Game.cs
index bfb861692..4cde8444a 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;
@@ -12,7 +12,7 @@ namespace Discord.API
[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")]
@@ -29,6 +29,10 @@ namespace Discord.API
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.Rest/API/Common/GameAssets.cs b/src/Discord.Net.Rest/API/Common/GameAssets.cs
index b5928a8ab..94a540769 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 @@ namespace Discord.API
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
+}
diff --git a/src/Discord.Net.Rest/API/Common/GameParty.cs b/src/Discord.Net.Rest/API/Common/GameParty.cs
index e0da4a098..4f8ce2654 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 @@ namespace Discord.API
[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.Rest/API/Rest/UploadFileParams.cs b/src/Discord.Net.Rest/API/Rest/UploadFileParams.cs
index 30bfc7f9a..9e909b50c 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