parent
8e1c343d33
commit
a57886b800
@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<configuration>
|
||||||
|
<startup>
|
||||||
|
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8" />
|
||||||
|
</startup>
|
||||||
|
<runtime>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Runtime.CompilerServices.Unsafe" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-4.0.6.0" newVersion="4.0.6.0" />
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
</runtime>
|
||||||
|
</configuration>
|
@ -0,0 +1,9 @@
|
|||||||
|
<Application x:Class="stream_sniper.App"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:local="clr-namespace:stream_sniper"
|
||||||
|
StartupUri="MainWindow.xaml">
|
||||||
|
<Application.Resources>
|
||||||
|
|
||||||
|
</Application.Resources>
|
||||||
|
</Application>
|
@ -0,0 +1,41 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Discord.Commands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Marks the aliases for a command.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This attribute allows a command to have one or multiple aliases. In other words, the base command can have
|
||||||
|
/// multiple aliases when triggering the command itself, giving the end-user more freedom of choices when giving
|
||||||
|
/// hot-words to trigger the desired command. See the example for a better illustration.
|
||||||
|
/// </remarks>
|
||||||
|
/// <example>
|
||||||
|
/// In the following example, the command can be triggered with the base name, "stats", or either "stat" or
|
||||||
|
/// "info".
|
||||||
|
/// <code language="cs">
|
||||||
|
/// [Command("stats")]
|
||||||
|
/// [Alias("stat", "info")]
|
||||||
|
/// public async Task GetStatsAsync(IUser user)
|
||||||
|
/// {
|
||||||
|
/// // ...pull stats
|
||||||
|
/// }
|
||||||
|
/// </code>
|
||||||
|
/// </example>
|
||||||
|
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
|
||||||
|
public class AliasAttribute : Attribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the aliases which have been defined for the command.
|
||||||
|
/// </summary>
|
||||||
|
public string[] Aliases { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new <see cref="AliasAttribute" /> with the given aliases.
|
||||||
|
/// </summary>
|
||||||
|
public AliasAttribute(params string[] aliases)
|
||||||
|
{
|
||||||
|
Aliases = aliases;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Discord.Commands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Marks the execution information for a command.
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
|
||||||
|
public class CommandAttribute : Attribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the text that has been set to be recognized as a command.
|
||||||
|
/// </summary>
|
||||||
|
public string Text { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Specifies the <see cref="RunMode" /> of the command. This affects how the command is executed.
|
||||||
|
/// </summary>
|
||||||
|
public RunMode RunMode { get; set; } = RunMode.Default;
|
||||||
|
public bool? IgnoreExtraArgs { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public CommandAttribute()
|
||||||
|
{
|
||||||
|
Text = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new <see cref="CommandAttribute" /> attribute with the specified name.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="text">The name of the command.</param>
|
||||||
|
public CommandAttribute(string text)
|
||||||
|
{
|
||||||
|
Text = text;
|
||||||
|
}
|
||||||
|
public CommandAttribute(string text, bool ignoreExtraArgs)
|
||||||
|
{
|
||||||
|
Text = text;
|
||||||
|
IgnoreExtraArgs = ignoreExtraArgs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Discord.Commands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Prevents the marked module from being loaded automatically.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This attribute tells <see cref="CommandService" /> to ignore the marked module from being loaded
|
||||||
|
/// automatically (e.g. the <see cref="CommandService.AddModulesAsync" /> method). If a non-public module marked
|
||||||
|
/// with this attribute is attempted to be loaded manually, the loading process will also fail.
|
||||||
|
/// </remarks>
|
||||||
|
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
|
||||||
|
public class DontAutoLoadAttribute : Attribute
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Discord.Commands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Prevents the marked property from being injected into a module.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This attribute prevents the marked member from being injected into its parent module. Useful when you have a
|
||||||
|
/// public property that you do not wish to invoke the library's dependency injection service.
|
||||||
|
/// </remarks>
|
||||||
|
/// <example>
|
||||||
|
/// In the following example, <c>DatabaseService</c> will not be automatically injected into the module and will
|
||||||
|
/// not throw an error message if the dependency fails to be resolved.
|
||||||
|
/// <code language="cs">
|
||||||
|
/// public class MyModule : ModuleBase
|
||||||
|
/// {
|
||||||
|
/// [DontInject]
|
||||||
|
/// public DatabaseService DatabaseService;
|
||||||
|
/// public MyModule()
|
||||||
|
/// {
|
||||||
|
/// DatabaseService = DatabaseFactory.Generate();
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// </code>
|
||||||
|
/// </example>
|
||||||
|
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
|
||||||
|
public class DontInjectAttribute : Attribute
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Discord.Commands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Marks the module as a command group.
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
|
||||||
|
public class GroupAttribute : Attribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the prefix set for the module.
|
||||||
|
/// </summary>
|
||||||
|
public string Prefix { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public GroupAttribute()
|
||||||
|
{
|
||||||
|
Prefix = null;
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new <see cref="GroupAttribute" /> with the provided prefix.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="prefix">The prefix of the module group.</param>
|
||||||
|
public GroupAttribute(string prefix)
|
||||||
|
{
|
||||||
|
Prefix = prefix;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Discord.Commands
|
||||||
|
{
|
||||||
|
// Override public name of command/module
|
||||||
|
/// <summary>
|
||||||
|
/// Marks the public name of a command, module, or parameter.
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
|
||||||
|
public class NameAttribute : Attribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the name of the command.
|
||||||
|
/// </summary>
|
||||||
|
public string Text { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Marks the public name of a command, module, or parameter with the provided name.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="text">The public name of the object.</param>
|
||||||
|
public NameAttribute(string text)
|
||||||
|
{
|
||||||
|
Text = text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Discord.Commands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Instructs the command system to treat command parameters of this type
|
||||||
|
/// as a collection of named arguments matching to its properties.
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
|
||||||
|
public sealed class NamedArgumentTypeAttribute : Attribute { }
|
||||||
|
}
|
@ -0,0 +1,50 @@
|
|||||||
|
using System;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace Discord.Commands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Marks the <see cref="Type"/> to be read by the specified <see cref="Discord.Commands.TypeReader"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This attribute will override the <see cref="Discord.Commands.TypeReader"/> to be used when parsing for the
|
||||||
|
/// desired type in the command. This is useful when one wishes to use a particular
|
||||||
|
/// <see cref="Discord.Commands.TypeReader"/> without affecting other commands that are using the same target
|
||||||
|
/// type.
|
||||||
|
/// <note type="warning">
|
||||||
|
/// If the given type reader does not inherit from <see cref="Discord.Commands.TypeReader"/>, an
|
||||||
|
/// <see cref="ArgumentException"/> will be thrown.
|
||||||
|
/// </note>
|
||||||
|
/// </remarks>
|
||||||
|
/// <example>
|
||||||
|
/// In this example, the <see cref="TimeSpan"/> will be read by a custom
|
||||||
|
/// <see cref="Discord.Commands.TypeReader"/>, <c>FriendlyTimeSpanTypeReader</c>, instead of the
|
||||||
|
/// <see cref="TimeSpanTypeReader"/> shipped by Discord.Net.
|
||||||
|
/// <code language="cs">
|
||||||
|
/// [Command("time")]
|
||||||
|
/// public Task GetTimeAsync([OverrideTypeReader(typeof(FriendlyTimeSpanTypeReader))]TimeSpan time)
|
||||||
|
/// => ReplyAsync(time);
|
||||||
|
/// </code>
|
||||||
|
/// </example>
|
||||||
|
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
|
||||||
|
public sealed class OverrideTypeReaderAttribute : Attribute
|
||||||
|
{
|
||||||
|
private static readonly TypeInfo TypeReaderTypeInfo = typeof(TypeReader).GetTypeInfo();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the specified <see cref="TypeReader"/> of the parameter.
|
||||||
|
/// </summary>
|
||||||
|
public Type TypeReader { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
/// <param name="overridenTypeReader">The <see cref="TypeReader"/> to be used with the parameter. </param>
|
||||||
|
/// <exception cref="ArgumentException">The given <paramref name="overridenTypeReader"/> does not inherit from <see cref="TypeReader"/>.</exception>
|
||||||
|
public OverrideTypeReaderAttribute(Type overridenTypeReader)
|
||||||
|
{
|
||||||
|
if (!TypeReaderTypeInfo.IsAssignableFrom(overridenTypeReader.GetTypeInfo()))
|
||||||
|
throw new ArgumentException($"{nameof(overridenTypeReader)} must inherit from {nameof(TypeReader)}.");
|
||||||
|
|
||||||
|
TypeReader = overridenTypeReader;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Discord.Commands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Requires the parameter to pass the specified precondition before execution can begin.
|
||||||
|
/// </summary>
|
||||||
|
/// <seealso cref="PreconditionAttribute"/>
|
||||||
|
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = true, Inherited = true)]
|
||||||
|
public abstract class ParameterPreconditionAttribute : Attribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Checks whether the condition is met before execution of the command.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">The context of the command.</param>
|
||||||
|
/// <param name="parameter">The parameter of the command being checked against.</param>
|
||||||
|
/// <param name="value">The raw value of the parameter.</param>
|
||||||
|
/// <param name="services">The service collection used for dependency injection.</param>
|
||||||
|
public abstract Task<PreconditionResult> CheckPermissionsAsync(ICommandContext context, ParameterInfo parameter, object value, IServiceProvider services);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Discord.Commands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Requires the module or class to pass the specified precondition before execution can begin.
|
||||||
|
/// </summary>
|
||||||
|
/// <seealso cref="ParameterPreconditionAttribute"/>
|
||||||
|
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
|
||||||
|
public abstract class PreconditionAttribute : Attribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Specifies a group that this precondition belongs to.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <see cref="Preconditions" /> of the same group require only one of the preconditions to pass in order to
|
||||||
|
/// be successful (A || B). Specifying <see cref="Group" /> = <c>null</c> or not at all will
|
||||||
|
/// require *all* preconditions to pass, just like normal (A && B).
|
||||||
|
/// </remarks>
|
||||||
|
public string Group { get; set; } = null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When overridden in a derived class, uses the supplied string
|
||||||
|
/// as the error message if the precondition doesn't pass.
|
||||||
|
/// Setting this for a class that doesn't override
|
||||||
|
/// this property is a no-op.
|
||||||
|
/// </summary>
|
||||||
|
public virtual string ErrorMessage { get { return null; } set { } }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if the <paramref name="command"/> has the sufficient permission to be executed.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">The context of the command.</param>
|
||||||
|
/// <param name="command">The command being executed.</param>
|
||||||
|
/// <param name="services">The service collection used for dependency injection.</param>
|
||||||
|
public abstract Task<PreconditionResult> CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,86 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Discord.Commands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Requires the bot to have a specific permission in the channel a command is invoked in.
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
|
||||||
|
public class RequireBotPermissionAttribute : PreconditionAttribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the specified <see cref="Discord.GuildPermission" /> of the precondition.
|
||||||
|
/// </summary>
|
||||||
|
public GuildPermission? GuildPermission { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the specified <see cref="Discord.ChannelPermission" /> of the precondition.
|
||||||
|
/// </summary>
|
||||||
|
public ChannelPermission? ChannelPermission { get; }
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override string ErrorMessage { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the error message if the precondition
|
||||||
|
/// fails due to being run outside of a Guild channel.
|
||||||
|
/// </summary>
|
||||||
|
public string NotAGuildErrorMessage { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Requires the bot account to have a specific <see cref="Discord.GuildPermission"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This precondition will always fail if the command is being invoked in a <see cref="IPrivateChannel"/>.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="permission">
|
||||||
|
/// The <see cref="Discord.GuildPermission"/> that the bot must have. Multiple permissions can be specified
|
||||||
|
/// by ORing the permissions together.
|
||||||
|
/// </param>
|
||||||
|
public RequireBotPermissionAttribute(GuildPermission permission)
|
||||||
|
{
|
||||||
|
GuildPermission = permission;
|
||||||
|
ChannelPermission = null;
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Requires that the bot account to have a specific <see cref="Discord.ChannelPermission"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="permission">
|
||||||
|
/// The <see cref="Discord.ChannelPermission"/> that the bot must have. Multiple permissions can be
|
||||||
|
/// specified by ORing the permissions together.
|
||||||
|
/// </param>
|
||||||
|
public RequireBotPermissionAttribute(ChannelPermission permission)
|
||||||
|
{
|
||||||
|
ChannelPermission = permission;
|
||||||
|
GuildPermission = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override async Task<PreconditionResult> CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services)
|
||||||
|
{
|
||||||
|
IGuildUser guildUser = null;
|
||||||
|
if (context.Guild != null)
|
||||||
|
guildUser = await context.Guild.GetCurrentUserAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (GuildPermission.HasValue)
|
||||||
|
{
|
||||||
|
if (guildUser == null)
|
||||||
|
return PreconditionResult.FromError(NotAGuildErrorMessage ?? "Command must be used in a guild channel.");
|
||||||
|
if (!guildUser.GuildPermissions.Has(GuildPermission.Value))
|
||||||
|
return PreconditionResult.FromError(ErrorMessage ?? $"Bot requires guild permission {GuildPermission.Value}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ChannelPermission.HasValue)
|
||||||
|
{
|
||||||
|
ChannelPermissions perms;
|
||||||
|
if (context.Channel is IGuildChannel guildChannel)
|
||||||
|
perms = guildUser.GetPermissions(guildChannel);
|
||||||
|
else
|
||||||
|
perms = ChannelPermissions.All(context.Channel);
|
||||||
|
|
||||||
|
if (!perms.Has(ChannelPermission.Value))
|
||||||
|
return PreconditionResult.FromError(ErrorMessage ?? $"Bot requires channel permission {ChannelPermission.Value}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return PreconditionResult.FromSuccess();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,74 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Discord.Commands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Defines the type of command context (i.e. where the command is being executed).
|
||||||
|
/// </summary>
|
||||||
|
[Flags]
|
||||||
|
public enum ContextType
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Specifies the command to be executed within a guild.
|
||||||
|
/// </summary>
|
||||||
|
Guild = 0x01,
|
||||||
|
/// <summary>
|
||||||
|
/// Specifies the command to be executed within a DM.
|
||||||
|
/// </summary>
|
||||||
|
DM = 0x02,
|
||||||
|
/// <summary>
|
||||||
|
/// Specifies the command to be executed within a group.
|
||||||
|
/// </summary>
|
||||||
|
Group = 0x04
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Requires the command to be invoked in a specified context (e.g. in guild, DM).
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
|
||||||
|
public class RequireContextAttribute : PreconditionAttribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the context required to execute the command.
|
||||||
|
/// </summary>
|
||||||
|
public ContextType Contexts { get; }
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override string ErrorMessage { get; set; }
|
||||||
|
|
||||||
|
/// <summary> Requires the command to be invoked in the specified context. </summary>
|
||||||
|
/// <param name="contexts">The type of context the command can be invoked in. Multiple contexts can be specified by ORing the contexts together.</param>
|
||||||
|
/// <example>
|
||||||
|
/// <code language="cs">
|
||||||
|
/// [Command("secret")]
|
||||||
|
/// [RequireContext(ContextType.DM | ContextType.Group)]
|
||||||
|
/// public Task PrivateOnlyAsync()
|
||||||
|
/// {
|
||||||
|
/// return ReplyAsync("shh, this command is a secret");
|
||||||
|
/// }
|
||||||
|
/// </code>
|
||||||
|
/// </example>
|
||||||
|
public RequireContextAttribute(ContextType contexts)
|
||||||
|
{
|
||||||
|
Contexts = contexts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override Task<PreconditionResult> CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services)
|
||||||
|
{
|
||||||
|
bool isValid = false;
|
||||||
|
|
||||||
|
if ((Contexts & ContextType.Guild) != 0)
|
||||||
|
isValid = context.Channel is IGuildChannel;
|
||||||
|
if ((Contexts & ContextType.DM) != 0)
|
||||||
|
isValid = isValid || context.Channel is IDMChannel;
|
||||||
|
if ((Contexts & ContextType.Group) != 0)
|
||||||
|
isValid = isValid || context.Channel is IGroupChannel;
|
||||||
|
|
||||||
|
if (isValid)
|
||||||
|
return Task.FromResult(PreconditionResult.FromSuccess());
|
||||||
|
else
|
||||||
|
return Task.FromResult(PreconditionResult.FromError(ErrorMessage ?? $"Invalid context for command; accepted contexts: {Contexts}."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Discord.Commands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Requires the command to be invoked in a channel marked NSFW.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The precondition will restrict the access of the command or module to be accessed within a guild channel
|
||||||
|
/// that has been marked as mature or NSFW. If the channel is not of type <see cref="ITextChannel"/> or the
|
||||||
|
/// channel is not marked as NSFW, the precondition will fail with an erroneous <see cref="PreconditionResult"/>.
|
||||||
|
/// </remarks>
|
||||||
|
/// <example>
|
||||||
|
/// The following example restricts the command <c>too-cool</c> to an NSFW-enabled channel only.
|
||||||
|
/// <code language="cs">
|
||||||
|
/// public class DankModule : ModuleBase
|
||||||
|
/// {
|
||||||
|
/// [Command("cool")]
|
||||||
|
/// public Task CoolAsync()
|
||||||
|
/// => ReplyAsync("I'm cool for everyone.");
|
||||||
|
///
|
||||||
|
/// [RequireNsfw]
|
||||||
|
/// [Command("too-cool")]
|
||||||
|
/// public Task TooCoolAsync()
|
||||||
|
/// => ReplyAsync("You can only see this if you're cool enough.");
|
||||||
|
/// }
|
||||||
|
/// </code>
|
||||||
|
/// </example>
|
||||||
|
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
|
||||||
|
public class RequireNsfwAttribute : PreconditionAttribute
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override string ErrorMessage { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override Task<PreconditionResult> CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services)
|
||||||
|
{
|
||||||
|
if (context.Channel is ITextChannel text && text.IsNsfw)
|
||||||
|
return Task.FromResult(PreconditionResult.FromSuccess());
|
||||||
|
else
|
||||||
|
return Task.FromResult(PreconditionResult.FromError(ErrorMessage ?? "This command may only be invoked in an NSFW channel."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,55 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Discord.Commands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Requires the command to be invoked by the owner of the bot.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This precondition will restrict the access of the command or module to the owner of the Discord application.
|
||||||
|
/// If the precondition fails to be met, an erroneous <see cref="PreconditionResult"/> will be returned with the
|
||||||
|
/// message "Command can only be run by the owner of the bot."
|
||||||
|
/// <note>
|
||||||
|
/// This precondition will only work if the account has a <see cref="TokenType"/> of <see cref="TokenType.Bot"/>
|
||||||
|
/// ;otherwise, this precondition will always fail.
|
||||||
|
/// </note>
|
||||||
|
/// </remarks>
|
||||||
|
/// <example>
|
||||||
|
/// The following example restricts the command to a set of sensitive commands that only the owner of the bot
|
||||||
|
/// application should be able to access.
|
||||||
|
/// <code language="cs">
|
||||||
|
/// [RequireOwner]
|
||||||
|
/// [Group("admin")]
|
||||||
|
/// public class AdminModule : ModuleBase
|
||||||
|
/// {
|
||||||
|
/// [Command("exit")]
|
||||||
|
/// public async Task ExitAsync()
|
||||||
|
/// {
|
||||||
|
/// Environment.Exit(0);
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// </code>
|
||||||
|
/// </example>
|
||||||
|
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
|
||||||
|
public class RequireOwnerAttribute : PreconditionAttribute
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override string ErrorMessage { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override async Task<PreconditionResult> CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services)
|
||||||
|
{
|
||||||
|
switch (context.Client.TokenType)
|
||||||
|
{
|
||||||
|
case TokenType.Bot:
|
||||||
|
var application = await context.Client.GetApplicationInfoAsync().ConfigureAwait(false);
|
||||||
|
if (context.User.Id != application.Owner.Id)
|
||||||
|
return PreconditionResult.FromError(ErrorMessage ?? "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)}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,84 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Discord.Commands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Requires the user invoking the command to have a specified permission.
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
|
||||||
|
public class RequireUserPermissionAttribute : PreconditionAttribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the specified <see cref="Discord.GuildPermission" /> of the precondition.
|
||||||
|
/// </summary>
|
||||||
|
public GuildPermission? GuildPermission { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the specified <see cref="Discord.ChannelPermission" /> of the precondition.
|
||||||
|
/// </summary>
|
||||||
|
public ChannelPermission? ChannelPermission { get; }
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override string ErrorMessage { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the error message if the precondition
|
||||||
|
/// fails due to being run outside of a Guild channel.
|
||||||
|
/// </summary>
|
||||||
|
public string NotAGuildErrorMessage { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Requires that the user invoking the command to have a specific <see cref="Discord.GuildPermission"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This precondition will always fail if the command is being invoked in a <see cref="IPrivateChannel"/>.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="permission">
|
||||||
|
/// The <see cref="Discord.GuildPermission" /> that the user must have. Multiple permissions can be
|
||||||
|
/// specified by ORing the permissions together.
|
||||||
|
/// </param>
|
||||||
|
public RequireUserPermissionAttribute(GuildPermission permission)
|
||||||
|
{
|
||||||
|
GuildPermission = permission;
|
||||||
|
ChannelPermission = null;
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Requires that the user invoking the command to have a specific <see cref="Discord.ChannelPermission"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="permission">
|
||||||
|
/// The <see cref="Discord.ChannelPermission"/> that the user must have. Multiple permissions can be
|
||||||
|
/// specified by ORing the permissions together.
|
||||||
|
/// </param>
|
||||||
|
public RequireUserPermissionAttribute(ChannelPermission permission)
|
||||||
|
{
|
||||||
|
ChannelPermission = permission;
|
||||||
|
GuildPermission = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override Task<PreconditionResult> CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services)
|
||||||
|
{
|
||||||
|
var guildUser = context.User as IGuildUser;
|
||||||
|
|
||||||
|
if (GuildPermission.HasValue)
|
||||||
|
{
|
||||||
|
if (guildUser == null)
|
||||||
|
return Task.FromResult(PreconditionResult.FromError(NotAGuildErrorMessage ?? "Command must be used in a guild channel."));
|
||||||
|
if (!guildUser.GuildPermissions.Has(GuildPermission.Value))
|
||||||
|
return Task.FromResult(PreconditionResult.FromError(ErrorMessage ?? $"User requires guild permission {GuildPermission.Value}."));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ChannelPermission.HasValue)
|
||||||
|
{
|
||||||
|
ChannelPermissions perms;
|
||||||
|
if (context.Channel is IGuildChannel guildChannel)
|
||||||
|
perms = guildUser.GetPermissions(guildChannel);
|
||||||
|
else
|
||||||
|
perms = ChannelPermissions.All(context.Channel);
|
||||||
|
|
||||||
|
if (!perms.Has(ChannelPermission.Value))
|
||||||
|
return Task.FromResult(PreconditionResult.FromError(ErrorMessage ?? $"User requires channel permission {ChannelPermission.Value}."));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult(PreconditionResult.FromSuccess());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Discord.Commands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Sets priority of commands.
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
|
||||||
|
public class PriorityAttribute : Attribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the priority which has been set for the command.
|
||||||
|
/// </summary>
|
||||||
|
public int Priority { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new <see cref="PriorityAttribute" /> attribute with the given priority.
|
||||||
|
/// </summary>
|
||||||
|
public PriorityAttribute(int priority)
|
||||||
|
{
|
||||||
|
Priority = priority;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Discord.Commands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Marks the input to not be parsed by the parser.
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
|
||||||
|
public class RemainderAttribute : Attribute
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Discord.Commands
|
||||||
|
{
|
||||||
|
// Extension of the Cosmetic Summary, for Groups, Commands, and Parameters
|
||||||
|
/// <summary>
|
||||||
|
/// Attaches remarks to your commands.
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
|
||||||
|
public class RemarksAttribute : Attribute
|
||||||
|
{
|
||||||
|
public string Text { get; }
|
||||||
|
|
||||||
|
public RemarksAttribute(string text)
|
||||||
|
{
|
||||||
|
Text = text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Discord.Commands
|
||||||
|
{
|
||||||
|
// Cosmetic Summary, for Groups and Commands
|
||||||
|
/// <summary>
|
||||||
|
/// Attaches a summary to your command.
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
|
||||||
|
public class SummaryAttribute : Attribute
|
||||||
|
{
|
||||||
|
public string Text { get; }
|
||||||
|
|
||||||
|
public SummaryAttribute(string text)
|
||||||
|
{
|
||||||
|
Text = text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,144 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Discord.Commands.Builders
|
||||||
|
{
|
||||||
|
public class CommandBuilder
|
||||||
|
{
|
||||||
|
private readonly List<PreconditionAttribute> _preconditions;
|
||||||
|
private readonly List<ParameterBuilder> _parameters;
|
||||||
|
private readonly List<Attribute> _attributes;
|
||||||
|
private readonly List<string> _aliases;
|
||||||
|
|
||||||
|
public ModuleBuilder Module { get; }
|
||||||
|
internal Func<ICommandContext, object[], IServiceProvider, CommandInfo, Task> Callback { get; set; }
|
||||||
|
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string Summary { get; set; }
|
||||||
|
public string Remarks { get; set; }
|
||||||
|
public string PrimaryAlias { get; set; }
|
||||||
|
public RunMode RunMode { get; set; }
|
||||||
|
public int Priority { get; set; }
|
||||||
|
public bool IgnoreExtraArgs { get; set; }
|
||||||
|
|
||||||
|
public IReadOnlyList<PreconditionAttribute> Preconditions => _preconditions;
|
||||||
|
public IReadOnlyList<ParameterBuilder> Parameters => _parameters;
|
||||||
|
public IReadOnlyList<Attribute> Attributes => _attributes;
|
||||||
|
public IReadOnlyList<string> Aliases => _aliases;
|
||||||
|
|
||||||
|
//Automatic
|
||||||
|
internal CommandBuilder(ModuleBuilder module)
|
||||||
|
{
|
||||||
|
Module = module;
|
||||||
|
|
||||||
|
_preconditions = new List<PreconditionAttribute>();
|
||||||
|
_parameters = new List<ParameterBuilder>();
|
||||||
|
_attributes = new List<Attribute>();
|
||||||
|
_aliases = new List<string>();
|
||||||
|
}
|
||||||
|
//User-defined
|
||||||
|
internal CommandBuilder(ModuleBuilder module, string primaryAlias, Func<ICommandContext, object[], IServiceProvider, CommandInfo, Task> callback)
|
||||||
|
: this(module)
|
||||||
|
{
|
||||||
|
Discord.Preconditions.NotNull(primaryAlias, nameof(primaryAlias));
|
||||||
|
Discord.Preconditions.NotNull(callback, nameof(callback));
|
||||||
|
|
||||||
|
Callback = callback;
|
||||||
|
PrimaryAlias = primaryAlias;
|
||||||
|
_aliases.Add(primaryAlias);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandBuilder WithName(string name)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
public CommandBuilder WithSummary(string summary)
|
||||||
|
{
|
||||||
|
Summary = summary;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
public CommandBuilder WithRemarks(string remarks)
|
||||||
|
{
|
||||||
|
Remarks = remarks;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
public CommandBuilder WithRunMode(RunMode runMode)
|
||||||
|
{
|
||||||
|
RunMode = runMode;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
public CommandBuilder WithPriority(int priority)
|
||||||
|
{
|
||||||
|
Priority = priority;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandBuilder AddAliases(params string[] aliases)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < aliases.Length; i++)
|
||||||
|
{
|
||||||
|
string alias = aliases[i] ?? "";
|
||||||
|
if (!_aliases.Contains(alias))
|
||||||
|
_aliases.Add(alias);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
public CommandBuilder AddAttributes(params Attribute[] attributes)
|
||||||
|
{
|
||||||
|
_attributes.AddRange(attributes);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
public CommandBuilder AddPrecondition(PreconditionAttribute precondition)
|
||||||
|
{
|
||||||
|
_preconditions.Add(precondition);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
public CommandBuilder AddParameter<T>(string name, Action<ParameterBuilder> createFunc)
|
||||||
|
{
|
||||||
|
var param = new ParameterBuilder(this, name, typeof(T));
|
||||||
|
createFunc(param);
|
||||||
|
_parameters.Add(param);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
public CommandBuilder AddParameter(string name, Type type, Action<ParameterBuilder> createFunc)
|
||||||
|
{
|
||||||
|
var param = new ParameterBuilder(this, name, type);
|
||||||
|
createFunc(param);
|
||||||
|
_parameters.Add(param);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
internal CommandBuilder AddParameter(Action<ParameterBuilder> createFunc)
|
||||||
|
{
|
||||||
|
var param = new ParameterBuilder(this);
|
||||||
|
createFunc(param);
|
||||||
|
_parameters.Add(param);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <exception cref="InvalidOperationException">Only the last parameter in a command may have the Remainder or Multiple flag.</exception>
|
||||||
|
internal CommandInfo Build(ModuleInfo info, CommandService service)
|
||||||
|
{
|
||||||
|
//Default name to primary alias
|
||||||
|
if (Name == null)
|
||||||
|
Name = PrimaryAlias;
|
||||||
|
|
||||||
|
if (_parameters.Count > 0)
|
||||||
|
{
|
||||||
|
var lastParam = _parameters[_parameters.Count - 1];
|
||||||
|
|
||||||
|
var firstMultipleParam = _parameters.FirstOrDefault(x => x.IsMultiple);
|
||||||
|
if ((firstMultipleParam != null) && (firstMultipleParam != lastParam))
|
||||||
|
throw new InvalidOperationException($"Only the last parameter in a command may have the Multiple flag. Parameter: {firstMultipleParam.Name} in {PrimaryAlias}");
|
||||||
|
|
||||||
|
var firstRemainderParam = _parameters.FirstOrDefault(x => x.IsRemainder);
|
||||||
|
if ((firstRemainderParam != null) && (firstRemainderParam != lastParam))
|
||||||
|
throw new InvalidOperationException($"Only the last parameter in a command may have the Remainder flag. Parameter: {firstRemainderParam.Name} in {PrimaryAlias}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CommandInfo(this, info, service);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,136 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Discord.Commands.Builders
|
||||||
|
{
|
||||||
|
public class ModuleBuilder
|
||||||
|
{
|
||||||
|
private readonly List<CommandBuilder> _commands;
|
||||||
|
private readonly List<ModuleBuilder> _submodules;
|
||||||
|
private readonly List<PreconditionAttribute> _preconditions;
|
||||||
|
private readonly List<Attribute> _attributes;
|
||||||
|
private readonly List<string> _aliases;
|
||||||
|
|
||||||
|
public CommandService Service { get; }
|
||||||
|
public ModuleBuilder Parent { get; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string Summary { get; set; }
|
||||||
|
public string Remarks { get; set; }
|
||||||
|
public string Group { get; set; }
|
||||||
|
|
||||||
|
public IReadOnlyList<CommandBuilder> Commands => _commands;
|
||||||
|
public IReadOnlyList<ModuleBuilder> Modules => _submodules;
|
||||||
|
public IReadOnlyList<PreconditionAttribute> Preconditions => _preconditions;
|
||||||
|
public IReadOnlyList<Attribute> Attributes => _attributes;
|
||||||
|
public IReadOnlyList<string> Aliases => _aliases;
|
||||||
|
|
||||||
|
internal TypeInfo TypeInfo { get; set; }
|
||||||
|
|
||||||
|
//Automatic
|
||||||
|
internal ModuleBuilder(CommandService service, ModuleBuilder parent)
|
||||||
|
{
|
||||||
|
Service = service;
|
||||||
|
Parent = parent;
|
||||||
|
|
||||||
|
_commands = new List<CommandBuilder>();
|
||||||
|
_submodules = new List<ModuleBuilder>();
|
||||||
|
_preconditions = new List<PreconditionAttribute>();
|
||||||
|
_attributes = new List<Attribute>();
|
||||||
|
_aliases = new List<string>();
|
||||||
|
}
|
||||||
|
//User-defined
|
||||||
|
internal ModuleBuilder(CommandService service, ModuleBuilder parent, string primaryAlias)
|
||||||
|
: this(service, parent)
|
||||||
|
{
|
||||||
|
Discord.Preconditions.NotNull(primaryAlias, nameof(primaryAlias));
|
||||||
|
|
||||||
|
_aliases = new List<string> { primaryAlias };
|
||||||
|
}
|
||||||
|
|
||||||
|
public ModuleBuilder WithName(string name)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
public ModuleBuilder WithSummary(string summary)
|
||||||
|
{
|
||||||
|
Summary = summary;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
public ModuleBuilder WithRemarks(string remarks)
|
||||||
|
{
|
||||||
|
Remarks = remarks;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ModuleBuilder AddAliases(params string[] aliases)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < aliases.Length; i++)
|
||||||
|
{
|
||||||
|
string alias = aliases[i] ?? "";
|
||||||
|
if (!_aliases.Contains(alias))
|
||||||
|
_aliases.Add(alias);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
public ModuleBuilder AddAttributes(params Attribute[] attributes)
|
||||||
|
{
|
||||||
|
_attributes.AddRange(attributes);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
public ModuleBuilder AddPrecondition(PreconditionAttribute precondition)
|
||||||
|
{
|
||||||
|
_preconditions.Add(precondition);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
public ModuleBuilder AddCommand(string primaryAlias, Func<ICommandContext, object[], IServiceProvider, CommandInfo, Task> callback, Action<CommandBuilder> createFunc)
|
||||||
|
{
|
||||||
|
var builder = new CommandBuilder(this, primaryAlias, callback);
|
||||||
|
createFunc(builder);
|
||||||
|
_commands.Add(builder);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
internal ModuleBuilder AddCommand(Action<CommandBuilder> createFunc)
|
||||||
|
{
|
||||||
|
var builder = new CommandBuilder(this);
|
||||||
|
createFunc(builder);
|
||||||
|
_commands.Add(builder);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
public ModuleBuilder AddModule(string primaryAlias, Action<ModuleBuilder> createFunc)
|
||||||
|
{
|
||||||
|
var builder = new ModuleBuilder(Service, this, primaryAlias);
|
||||||
|
createFunc(builder);
|
||||||
|
_submodules.Add(builder);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
internal ModuleBuilder AddModule(Action<ModuleBuilder> createFunc)
|
||||||
|
{
|
||||||
|
var builder = new ModuleBuilder(Service, this);
|
||||||
|
createFunc(builder);
|
||||||
|
_submodules.Add(builder);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ModuleInfo BuildImpl(CommandService service, IServiceProvider services, ModuleInfo parent = null)
|
||||||
|
{
|
||||||
|
//Default name to first alias
|
||||||
|
if (Name == null)
|
||||||
|
Name = _aliases[0];
|
||||||
|
|
||||||
|
if (TypeInfo != null && !TypeInfo.IsAbstract)
|
||||||
|
{
|
||||||
|
var moduleInstance = ReflectionUtils.CreateObject<IModuleBase>(TypeInfo, service, services);
|
||||||
|
moduleInstance.OnModuleBuilding(service, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ModuleInfo(this, service, services, parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ModuleInfo Build(CommandService service, IServiceProvider services) => BuildImpl(service, services);
|
||||||
|
|
||||||
|
internal ModuleInfo Build(CommandService service, IServiceProvider services, ModuleInfo parent) => BuildImpl(service, services, parent);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,315 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
using Discord.Commands.Builders;
|
||||||
|
|
||||||
|
namespace Discord.Commands
|
||||||
|
{
|
||||||
|
internal static class ModuleClassBuilder
|
||||||
|
{
|
||||||
|
private static readonly TypeInfo ModuleTypeInfo = typeof(IModuleBase).GetTypeInfo();
|
||||||
|
|
||||||
|
public static async Task<IReadOnlyList<TypeInfo>> SearchAsync(Assembly assembly, CommandService service)
|
||||||
|
{
|
||||||
|
bool IsLoadableModule(TypeInfo info)
|
||||||
|
{
|
||||||
|
return info.DeclaredMethods.Any(x => x.GetCustomAttribute<CommandAttribute>() != null) &&
|
||||||
|
info.GetCustomAttribute<DontAutoLoadAttribute>() == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = new List<TypeInfo>();
|
||||||
|
|
||||||
|
foreach (var typeInfo in assembly.DefinedTypes)
|
||||||
|
{
|
||||||
|
if (typeInfo.IsPublic || typeInfo.IsNestedPublic)
|
||||||
|
{
|
||||||
|
if (IsValidModuleDefinition(typeInfo) &&
|
||||||
|
!typeInfo.IsDefined(typeof(DontAutoLoadAttribute)))
|
||||||
|
{
|
||||||
|
result.Add(typeInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (IsLoadableModule(typeInfo))
|
||||||
|
{
|
||||||
|
await service._cmdLogger.WarningAsync($"Class {typeInfo.FullName} is not public and cannot be loaded. To suppress this message, mark the class with {nameof(DontAutoLoadAttribute)}.").ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static Task<Dictionary<Type, ModuleInfo>> BuildAsync(CommandService service, IServiceProvider services, params TypeInfo[] validTypes) => BuildAsync(validTypes, service, services);
|
||||||
|
public static async Task<Dictionary<Type, ModuleInfo>> BuildAsync(IEnumerable<TypeInfo> 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 || !IsValidModuleDefinition(x.DeclaringType.GetTypeInfo()));
|
||||||
|
|
||||||
|
var builtTypes = new List<TypeInfo>();
|
||||||
|
|
||||||
|
var result = new Dictionary<Type, ModuleInfo>();
|
||||||
|
|
||||||
|
foreach (var typeInfo in topLevelGroups)
|
||||||
|
{
|
||||||
|
// TODO: This shouldn't be the case; may be safe to remove?
|
||||||
|
if (result.ContainsKey(typeInfo.AsType()))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var module = new ModuleBuilder(service, null);
|
||||||
|
|
||||||
|
BuildModule(module, typeInfo, service, services);
|
||||||
|
BuildSubTypes(module, typeInfo.DeclaredNestedTypes, builtTypes, service, services);
|
||||||
|
builtTypes.Add(typeInfo);
|
||||||
|
|
||||||
|
result[typeInfo.AsType()] = module.Build(service, services);
|
||||||
|
}
|
||||||
|
|
||||||
|
await service._cmdLogger.DebugAsync($"Successfully built {builtTypes.Count} modules.").ConfigureAwait(false);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void BuildSubTypes(ModuleBuilder builder, IEnumerable<TypeInfo> subTypes, List<TypeInfo> builtTypes, CommandService service, IServiceProvider services)
|
||||||
|
{
|
||||||
|
foreach (var typeInfo in subTypes)
|
||||||
|
{
|
||||||
|
if (!IsValidModuleDefinition(typeInfo))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (builtTypes.Contains(typeInfo))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
builder.AddModule((module) =>
|
||||||
|
{
|
||||||
|
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, IServiceProvider services)
|
||||||
|
{
|
||||||
|
var attributes = typeInfo.GetCustomAttributes();
|
||||||
|
builder.TypeInfo = typeInfo;
|
||||||
|
|
||||||
|
foreach (var attribute in attributes)
|
||||||
|
{
|
||||||
|
switch (attribute)
|
||||||
|
{
|
||||||
|
case NameAttribute name:
|
||||||
|
builder.Name = name.Text;
|
||||||
|
break;
|
||||||
|
case SummaryAttribute summary:
|
||||||
|
builder.Summary = summary.Text;
|
||||||
|
break;
|
||||||
|
case RemarksAttribute remarks:
|
||||||
|
builder.Remarks = remarks.Text;
|
||||||
|
break;
|
||||||
|
case AliasAttribute alias:
|
||||||
|
builder.AddAliases(alias.Aliases);
|
||||||
|
break;
|
||||||
|
case GroupAttribute group:
|
||||||
|
builder.Name = builder.Name ?? group.Prefix;
|
||||||
|
builder.Group = group.Prefix;
|
||||||
|
builder.AddAliases(group.Prefix);
|
||||||
|
break;
|
||||||
|
case PreconditionAttribute precondition:
|
||||||
|
builder.AddPrecondition(precondition);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
builder.AddAttributes(attribute);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Check for unspecified info
|
||||||
|
if (builder.Aliases.Count == 0)
|
||||||
|
builder.AddAliases("");
|
||||||
|
if (builder.Name == null)
|
||||||
|
builder.Name = typeInfo.Name;
|
||||||
|
|
||||||
|
var validCommands = typeInfo.DeclaredMethods.Where(IsValidCommandDefinition);
|
||||||
|
|
||||||
|
foreach (var method in validCommands)
|
||||||
|
{
|
||||||
|
builder.AddCommand((command) =>
|
||||||
|
{
|
||||||
|
BuildCommand(command, typeInfo, method, service, services);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void BuildCommand(CommandBuilder builder, TypeInfo typeInfo, MethodInfo method, CommandService service, IServiceProvider serviceprovider)
|
||||||
|
{
|
||||||
|
var attributes = method.GetCustomAttributes();
|
||||||
|
|
||||||
|
foreach (var attribute in attributes)
|
||||||
|
{
|
||||||
|
switch (attribute)
|
||||||
|
{
|
||||||
|
case CommandAttribute command:
|
||||||
|
builder.AddAliases(command.Text);
|
||||||
|
builder.RunMode = command.RunMode;
|
||||||
|
builder.Name = builder.Name ?? command.Text;
|
||||||
|
builder.IgnoreExtraArgs = command.IgnoreExtraArgs ?? service._ignoreExtraArgs;
|
||||||
|
break;
|
||||||
|
case NameAttribute name:
|
||||||
|
builder.Name = name.Text;
|
||||||
|
break;
|
||||||
|
case PriorityAttribute priority:
|
||||||
|
builder.Priority = priority.Priority;
|
||||||
|
break;
|
||||||
|
case SummaryAttribute summary:
|
||||||
|
builder.Summary = summary.Text;
|
||||||
|
break;
|
||||||
|
case RemarksAttribute remarks:
|
||||||
|
builder.Remarks = remarks.Text;
|
||||||
|
break;
|
||||||
|
case AliasAttribute alias:
|
||||||
|
builder.AddAliases(alias.Aliases);
|
||||||
|
break;
|
||||||
|
case PreconditionAttribute precondition:
|
||||||
|
builder.AddPrecondition(precondition);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
builder.AddAttributes(attribute);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (builder.Name == null)
|
||||||
|
builder.Name = method.Name;
|
||||||
|
|
||||||
|
var parameters = method.GetParameters();
|
||||||
|
int pos = 0, count = parameters.Length;
|
||||||
|
foreach (var paramInfo in parameters)
|
||||||
|
{
|
||||||
|
builder.AddParameter((parameter) =>
|
||||||
|
{
|
||||||
|
BuildParameter(parameter, paramInfo, pos++, count, service, serviceprovider);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var createInstance = ReflectionUtils.CreateBuilder<IModuleBase>(typeInfo, service);
|
||||||
|
|
||||||
|
async Task<IResult> ExecuteCallback(ICommandContext context, object[] args, IServiceProvider services, CommandInfo cmd)
|
||||||
|
{
|
||||||
|
var instance = createInstance(services);
|
||||||
|
instance.SetContext(context);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
instance.BeforeExecute(cmd);
|
||||||
|
|
||||||
|
var task = method.Invoke(instance, args) as Task ?? Task.Delay(0);
|
||||||
|
if (task is Task<RuntimeResult> resultTask)
|
||||||
|
{
|
||||||
|
return await resultTask.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await task.ConfigureAwait(false);
|
||||||
|
return ExecuteResult.FromSuccess();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
instance.AfterExecute(cmd);
|
||||||
|
(instance as IDisposable)?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.Callback = ExecuteCallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void BuildParameter(ParameterBuilder builder, System.Reflection.ParameterInfo paramInfo, int position, int count, CommandService service, IServiceProvider services)
|
||||||
|
{
|
||||||
|
var attributes = paramInfo.GetCustomAttributes();
|
||||||
|
var paramType = paramInfo.ParameterType;
|
||||||
|
|
||||||
|
builder.Name = paramInfo.Name;
|
||||||
|
|
||||||
|
builder.IsOptional = paramInfo.IsOptional;
|
||||||
|
builder.DefaultValue = paramInfo.HasDefaultValue ? paramInfo.DefaultValue : null;
|
||||||
|
|
||||||
|
foreach (var attribute in attributes)
|
||||||
|
{
|
||||||
|
switch (attribute)
|
||||||
|
{
|
||||||
|
case SummaryAttribute summary:
|
||||||
|
builder.Summary = summary.Text;
|
||||||
|
break;
|
||||||
|
case OverrideTypeReaderAttribute typeReader:
|
||||||
|
builder.TypeReader = GetTypeReader(service, paramType, typeReader.TypeReader, services);
|
||||||
|
break;
|
||||||
|
case ParamArrayAttribute _:
|
||||||
|
builder.IsMultiple = true;
|
||||||
|
paramType = paramType.GetElementType();
|
||||||
|
break;
|
||||||
|
case ParameterPreconditionAttribute precon:
|
||||||
|
builder.AddPrecondition(precon);
|
||||||
|
break;
|
||||||
|
case NameAttribute name:
|
||||||
|
builder.Name = name.Text;
|
||||||
|
break;
|
||||||
|
case RemainderAttribute _:
|
||||||
|
if (position != count - 1)
|
||||||
|
throw new InvalidOperationException($"Remainder parameters must be the last parameter in a command. Parameter: {paramInfo.Name} in {paramInfo.Member.DeclaringType.Name}.{paramInfo.Member.Name}");
|
||||||
|
|
||||||
|
builder.IsRemainder = true;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
builder.AddAttributes(attribute);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.ParameterType = paramType;
|
||||||
|
|
||||||
|
if (builder.TypeReader == null)
|
||||||
|
{
|
||||||
|
builder.TypeReader = service.GetDefaultTypeReader(paramType)
|
||||||
|
?? service.GetTypeReaders(paramType)?.FirstOrDefault().Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static TypeReader GetTypeReader(CommandService service, Type paramType, Type typeReaderType, IServiceProvider services)
|
||||||
|
{
|
||||||
|
var readers = service.GetTypeReaders(paramType);
|
||||||
|
TypeReader reader = null;
|
||||||
|
if (readers != null)
|
||||||
|
{
|
||||||
|
if (readers.TryGetValue(typeReaderType, out reader))
|
||||||
|
return reader;
|
||||||
|
}
|
||||||
|
|
||||||
|
//We dont have a cached type reader, create one
|
||||||
|
reader = ReflectionUtils.CreateObject<TypeReader>(typeReaderType.GetTypeInfo(), service, services);
|
||||||
|
service.AddTypeReader(paramType, reader, false);
|
||||||
|
|
||||||
|
return reader;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsValidModuleDefinition(TypeInfo typeInfo)
|
||||||
|
{
|
||||||
|
return ModuleTypeInfo.IsAssignableFrom(typeInfo) &&
|
||||||
|
!typeInfo.IsAbstract &&
|
||||||
|
!typeInfo.ContainsGenericParameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsValidCommandDefinition(MethodInfo methodInfo)
|
||||||
|
{
|
||||||
|
return methodInfo.IsDefined(typeof(CommandAttribute)) &&
|
||||||
|
(methodInfo.ReturnType == typeof(Task) || methodInfo.ReturnType == typeof(Task<RuntimeResult>)) &&
|
||||||
|
!methodInfo.IsStatic &&
|
||||||
|
!methodInfo.IsGenericMethod;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,136 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Discord.Commands.Builders
|
||||||
|
{
|
||||||
|
public class ParameterBuilder
|
||||||
|
{
|
||||||
|
private readonly List<ParameterPreconditionAttribute> _preconditions;
|
||||||
|
private readonly List<Attribute> _attributes;
|
||||||
|
|
||||||
|
public CommandBuilder Command { get; }
|
||||||
|
public string Name { get; internal set; }
|
||||||
|
public Type ParameterType { get; internal set; }
|
||||||
|
|
||||||
|
public TypeReader TypeReader { get; set; }
|
||||||
|
public bool IsOptional { get; set; }
|
||||||
|
public bool IsRemainder { get; set; }
|
||||||
|
public bool IsMultiple { get; set; }
|
||||||
|
public object DefaultValue { get; set; }
|
||||||
|
public string Summary { get; set; }
|
||||||
|
|
||||||
|
public IReadOnlyList<ParameterPreconditionAttribute> Preconditions => _preconditions;
|
||||||
|
public IReadOnlyList<Attribute> Attributes => _attributes;
|
||||||
|
|
||||||
|
//Automatic
|
||||||
|
internal ParameterBuilder(CommandBuilder command)
|
||||||
|
{
|
||||||
|
_preconditions = new List<ParameterPreconditionAttribute>();
|
||||||
|
_attributes = new List<Attribute>();
|
||||||
|
|
||||||
|
Command = command;
|
||||||
|
}
|
||||||
|
//User-defined
|
||||||
|
internal ParameterBuilder(CommandBuilder command, string name, Type type)
|
||||||
|
: this(command)
|
||||||
|
{
|
||||||
|
Discord.Preconditions.NotNull(name, nameof(name));
|
||||||
|
|
||||||
|
Name = name;
|
||||||
|
SetType(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void SetType(Type type)
|
||||||
|
{
|
||||||
|
TypeReader = GetReader(type);
|
||||||
|
|
||||||
|
if (type.GetTypeInfo().IsValueType)
|
||||||
|
DefaultValue = Activator.CreateInstance(type);
|
||||||
|
else if (type.IsArray)
|
||||||
|
type = ParameterType.GetElementType();
|
||||||
|
ParameterType = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TypeReader GetReader(Type type)
|
||||||
|
{
|
||||||
|
var commands = Command.Module.Service;
|
||||||
|
if (type.GetTypeInfo().GetCustomAttribute<NamedArgumentTypeAttribute>() != null)
|
||||||
|
{
|
||||||
|
IsRemainder = true;
|
||||||
|
var reader = commands.GetTypeReaders(type)?.FirstOrDefault().Value;
|
||||||
|
if (reader == null)
|
||||||
|
{
|
||||||
|
Type readerType;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
readerType = typeof(NamedArgumentTypeReader<>).MakeGenericType(new[] { type });
|
||||||
|
}
|
||||||
|
catch (ArgumentException ex)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Parameter type '{type.Name}' for command '{Command.Name}' must be a class with a public parameterless constructor to use as a NamedArgumentType.", ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
reader = (TypeReader)Activator.CreateInstance(readerType, new[] { commands });
|
||||||
|
commands.AddTypeReader(type, reader);
|
||||||
|
}
|
||||||
|
|
||||||
|
return reader;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var readers = commands.GetTypeReaders(type);
|
||||||
|
if (readers != null)
|
||||||
|
return readers.FirstOrDefault().Value;
|
||||||
|
else
|
||||||
|
return commands.GetDefaultTypeReader(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ParameterBuilder WithSummary(string summary)
|
||||||
|
{
|
||||||
|
Summary = summary;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
public ParameterBuilder WithDefault(object defaultValue)
|
||||||
|
{
|
||||||
|
DefaultValue = defaultValue;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
public ParameterBuilder WithIsOptional(bool isOptional)
|
||||||
|
{
|
||||||
|
IsOptional = isOptional;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
public ParameterBuilder WithIsRemainder(bool isRemainder)
|
||||||
|
{
|
||||||
|
IsRemainder = isRemainder;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
public ParameterBuilder WithIsMultiple(bool isMultiple)
|
||||||
|
{
|
||||||
|
IsMultiple = isMultiple;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ParameterBuilder AddAttributes(params Attribute[] attributes)
|
||||||
|
{
|
||||||
|
_attributes.AddRange(attributes);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
public ParameterBuilder AddPrecondition(ParameterPreconditionAttribute precondition)
|
||||||
|
{
|
||||||
|
_preconditions.Add(precondition);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal ParameterInfo Build(CommandInfo info)
|
||||||
|
{
|
||||||
|
if ((TypeReader ?? (TypeReader = GetReader(ParameterType))) == null)
|
||||||
|
throw new InvalidOperationException($"No type reader found for type {ParameterType.Name}, one must be specified");
|
||||||
|
|
||||||
|
return new ParameterInfo(this, info, Command.Module.Service);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
namespace Discord.Commands
|
||||||
|
{
|
||||||
|
/// <summary> The context of a command which may contain the client, user, guild, channel, and message. </summary>
|
||||||
|
public class CommandContext : ICommandContext
|
||||||
|
{
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public IDiscordClient Client { get; }
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public IGuild Guild { get; }
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public IMessageChannel Channel { get; }
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public IUser User { get; }
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public IUserMessage Message { get; }
|
||||||
|
|
||||||
|
/// <summary> Indicates whether the channel that the command is executed in is a private channel. </summary>
|
||||||
|
public bool IsPrivate => Channel is IPrivateChannel;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new <see cref="CommandContext" /> class with the provided client and message.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="client">The underlying client.</param>
|
||||||
|
/// <param name="msg">The underlying message.</param>
|
||||||
|
public CommandContext(IDiscordClient client, IUserMessage msg)
|
||||||
|
{
|
||||||
|
Client = client;
|
||||||
|
Guild = (msg.Channel as IGuildChannel)?.Guild;
|
||||||
|
Channel = msg.Channel;
|
||||||
|
User = msg.Author;
|
||||||
|
Message = msg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
namespace Discord.Commands
|
||||||
|
{
|
||||||
|
/// <summary> Defines the type of error a command can throw. </summary>
|
||||||
|
public enum CommandError
|
||||||
|
{
|
||||||
|
//Search
|
||||||
|
/// <summary>
|
||||||
|
/// Thrown when the command is unknown.
|
||||||
|
/// </summary>
|
||||||
|
UnknownCommand = 1,
|
||||||
|
|
||||||
|
//Parse
|
||||||
|
/// <summary>
|
||||||
|
/// Thrown when the command fails to be parsed.
|
||||||
|
/// </summary>
|
||||||
|
ParseFailed,
|
||||||
|
/// <summary>
|
||||||
|
/// Thrown when the input text has too few or too many arguments.
|
||||||
|
/// </summary>
|
||||||
|
BadArgCount,
|
||||||
|
|
||||||
|
//Parse (Type Reader)
|
||||||
|
//CastFailed,
|
||||||
|
/// <summary>
|
||||||
|
/// Thrown when the object cannot be found by the <see cref="TypeReader"/>.
|
||||||
|
/// </summary>
|
||||||
|
ObjectNotFound,
|
||||||
|
/// <summary>
|
||||||
|
/// Thrown when more than one object is matched by <see cref="TypeReader"/>.
|
||||||
|
/// </summary>
|
||||||
|
MultipleMatches,
|
||||||
|
|
||||||
|
//Preconditions
|
||||||
|
/// <summary>
|
||||||
|
/// Thrown when the command fails to meet a <see cref="PreconditionAttribute"/>'s conditions.
|
||||||
|
/// </summary>
|
||||||
|
UnmetPrecondition,
|
||||||
|
|
||||||
|
//Execute
|
||||||
|
/// <summary>
|
||||||
|
/// Thrown when an exception occurs mid-command execution.
|
||||||
|
/// </summary>
|
||||||
|
Exception,
|
||||||
|
|
||||||
|
//Runtime
|
||||||
|
/// <summary>
|
||||||
|
/// Thrown when the command is not successfully executed on runtime.
|
||||||
|
/// </summary>
|
||||||
|
Unsuccessful
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Discord.Commands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The exception that is thrown if another exception occurs during a command execution.
|
||||||
|
/// </summary>
|
||||||
|
public class CommandException : Exception
|
||||||
|
{
|
||||||
|
/// <summary> Gets the command that caused the exception. </summary>
|
||||||
|
public CommandInfo Command { get; }
|
||||||
|
/// <summary> Gets the command context of the exception. </summary>
|
||||||
|
public ICommandContext Context { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="CommandException" /> class using a
|
||||||
|
/// <paramref name="command"/> information, a <paramref name="command"/> context, and the exception that
|
||||||
|
/// interrupted the execution.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="command">The command information.</param>
|
||||||
|
/// <param name="context">The context of the command.</param>
|
||||||
|
/// <param name="ex">The exception that interrupted the command execution.</param>
|
||||||
|
public CommandException(CommandInfo command, ICommandContext context, Exception ex)
|
||||||
|
: base($"Error occurred executing {command.GetLogText(context)}.", ex)
|
||||||
|
{
|
||||||
|
Command = command;
|
||||||
|
Context = context;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Discord.Commands
|
||||||
|
{
|
||||||
|
public struct CommandMatch
|
||||||
|
{
|
||||||
|
/// <summary> The command that matches the search result. </summary>
|
||||||
|
public CommandInfo Command { get; }
|
||||||
|
/// <summary> The alias of the command. </summary>
|
||||||
|
public string Alias { get; }
|
||||||
|
|
||||||
|
public CommandMatch(CommandInfo command, string alias)
|
||||||
|
{
|
||||||
|
Command = command;
|
||||||
|
Alias = alias;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<PreconditionResult> CheckPreconditionsAsync(ICommandContext context, IServiceProvider services = null)
|
||||||
|
=> Command.CheckPreconditionsAsync(context, services);
|
||||||
|
public Task<ParseResult> ParseAsync(ICommandContext context, SearchResult searchResult, PreconditionResult preconditionResult = null, IServiceProvider services = null)
|
||||||
|
=> Command.ParseAsync(context, Alias.Length, searchResult, preconditionResult, services);
|
||||||
|
public Task<IResult> ExecuteAsync(ICommandContext context, IEnumerable<object> argList, IEnumerable<object> paramList, IServiceProvider services)
|
||||||
|
=> Command.ExecuteAsync(context, argList, paramList, services);
|
||||||
|
public Task<IResult> ExecuteAsync(ICommandContext context, ParseResult parseResult, IServiceProvider services)
|
||||||
|
=> Command.ExecuteAsync(context, parseResult, services);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,198 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Discord.Commands
|
||||||
|
{
|
||||||
|
internal static class CommandParser
|
||||||
|
{
|
||||||
|
private enum ParserPart
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
Parameter,
|
||||||
|
QuotedParameter
|
||||||
|
}
|
||||||
|
public static async Task<ParseResult> ParseArgsAsync(CommandInfo command, ICommandContext context, bool ignoreExtraArgs, IServiceProvider services, string input, int startPos, IReadOnlyDictionary<char, char> aliasMap)
|
||||||
|
{
|
||||||
|
ParameterInfo curParam = null;
|
||||||
|
StringBuilder argBuilder = new StringBuilder(input.Length);
|
||||||
|
int endPos = input.Length;
|
||||||
|
var curPart = ParserPart.None;
|
||||||
|
int lastArgEndPos = int.MinValue;
|
||||||
|
var argList = ImmutableArray.CreateBuilder<TypeReaderResult>();
|
||||||
|
var paramList = ImmutableArray.CreateBuilder<TypeReaderResult>();
|
||||||
|
bool isEscaping = false;
|
||||||
|
char c, matchQuote = '\0';
|
||||||
|
|
||||||
|
// local helper functions
|
||||||
|
bool IsOpenQuote(IReadOnlyDictionary<char, char> dict, char ch)
|
||||||
|
{
|
||||||
|
// return if the key is contained in the dictionary if it is populated
|
||||||
|
if (dict.Count != 0)
|
||||||
|
return dict.ContainsKey(ch);
|
||||||
|
// or otherwise if it is the default double quote
|
||||||
|
return c == '\"';
|
||||||
|
}
|
||||||
|
|
||||||
|
char GetMatch(IReadOnlyDictionary<char, char> dict, char ch)
|
||||||
|
{
|
||||||
|
// get the corresponding value for the key, if it exists
|
||||||
|
// and if the dictionary is populated
|
||||||
|
if (dict.Count != 0 && dict.TryGetValue(c, out var value))
|
||||||
|
return value;
|
||||||
|
// or get the default pair of the default double quote
|
||||||
|
return '\"';
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int curPos = startPos; curPos <= endPos; curPos++)
|
||||||
|
{
|
||||||
|
if (curPos < endPos)
|
||||||
|
c = input[curPos];
|
||||||
|
else
|
||||||
|
c = '\0';
|
||||||
|
|
||||||
|
//If we're processing an remainder parameter, ignore all other logic
|
||||||
|
if (curParam != null && curParam.IsRemainder && curPos != endPos)
|
||||||
|
{
|
||||||
|
argBuilder.Append(c);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
//If this character is escaped, skip it
|
||||||
|
if (isEscaping)
|
||||||
|
{
|
||||||
|
if (curPos != endPos)
|
||||||
|
{
|
||||||
|
// if this character matches the quotation mark of the end of the string
|
||||||
|
// means that it should be escaped
|
||||||
|
// but if is not, then there is no reason to escape it then
|
||||||
|
if (c != matchQuote)
|
||||||
|
{
|
||||||
|
// if no reason to escape the next character, then re-add \ to the arg
|
||||||
|
argBuilder.Append('\\');
|
||||||
|
}
|
||||||
|
|
||||||
|
argBuilder.Append(c);
|
||||||
|
isEscaping = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//Are we escaping the next character?
|
||||||
|
if (c == '\\' && (curParam == null || !curParam.IsRemainder))
|
||||||
|
{
|
||||||
|
isEscaping = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
//If we're not currently processing one, are we starting the next argument yet?
|
||||||
|
if (curPart == ParserPart.None)
|
||||||
|
{
|
||||||
|
if (char.IsWhiteSpace(c) || curPos == endPos)
|
||||||
|
continue; //Skip whitespace between arguments
|
||||||
|
else if (curPos == lastArgEndPos)
|
||||||
|
return ParseResult.FromError(CommandError.ParseFailed, "There must be at least one character of whitespace between arguments.");
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (curParam == null)
|
||||||
|
curParam = command.Parameters.Count > argList.Count ? command.Parameters[argList.Count] : null;
|
||||||
|
|
||||||
|
if (curParam != null && curParam.IsRemainder)
|
||||||
|
{
|
||||||
|
argBuilder.Append(c);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsOpenQuote(aliasMap, c))
|
||||||
|
{
|
||||||
|
curPart = ParserPart.QuotedParameter;
|
||||||
|
matchQuote = GetMatch(aliasMap, c);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
curPart = ParserPart.Parameter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Has this parameter ended yet?
|
||||||
|
string argString = null;
|
||||||
|
if (curPart == ParserPart.Parameter)
|
||||||
|
{
|
||||||
|
if (curPos == endPos || char.IsWhiteSpace(c))
|
||||||
|
{
|
||||||
|
argString = argBuilder.ToString();
|
||||||
|
lastArgEndPos = curPos;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
argBuilder.Append(c);
|
||||||
|
}
|
||||||
|
else if (curPart == ParserPart.QuotedParameter)
|
||||||
|
{
|
||||||
|
if (c == matchQuote)
|
||||||
|
{
|
||||||
|
argString = argBuilder.ToString(); //Remove quotes
|
||||||
|
lastArgEndPos = curPos + 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
argBuilder.Append(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (argString != null)
|
||||||
|
{
|
||||||
|
if (curParam == null)
|
||||||
|
{
|
||||||
|
if (command.IgnoreExtraArgs)
|
||||||
|
break;
|
||||||
|
else
|
||||||
|
return ParseResult.FromError(CommandError.BadArgCount, "The input text has too many parameters.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var typeReaderResult = await curParam.ParseAsync(context, argString, services).ConfigureAwait(false);
|
||||||
|
if (!typeReaderResult.IsSuccess && typeReaderResult.Error != CommandError.MultipleMatches)
|
||||||
|
return ParseResult.FromError(typeReaderResult, curParam);
|
||||||
|
|
||||||
|
if (curParam.IsMultiple)
|
||||||
|
{
|
||||||
|
paramList.Add(typeReaderResult);
|
||||||
|
|
||||||
|
curPart = ParserPart.None;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
argList.Add(typeReaderResult);
|
||||||
|
|
||||||
|
curParam = null;
|
||||||
|
curPart = ParserPart.None;
|
||||||
|
}
|
||||||
|
argBuilder.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (curParam != null && curParam.IsRemainder)
|
||||||
|
{
|
||||||
|
var typeReaderResult = await curParam.ParseAsync(context, argBuilder.ToString(), services).ConfigureAwait(false);
|
||||||
|
if (!typeReaderResult.IsSuccess)
|
||||||
|
return ParseResult.FromError(typeReaderResult, curParam);
|
||||||
|
argList.Add(typeReaderResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEscaping)
|
||||||
|
return ParseResult.FromError(CommandError.ParseFailed, "Input text may not end on an incomplete escape.");
|
||||||
|
if (curPart == ParserPart.QuotedParameter)
|
||||||
|
return ParseResult.FromError(CommandError.ParseFailed, "A quoted parameter is incomplete.");
|
||||||
|
|
||||||
|
//Add missing optionals
|
||||||
|
for (int i = argList.Count; i < command.Parameters.Count; i++)
|
||||||
|
{
|
||||||
|
var param = command.Parameters[i];
|
||||||
|
if (param.IsMultiple)
|
||||||
|
continue;
|
||||||
|
if (!param.IsOptional)
|
||||||
|
return ParseResult.FromError(CommandError.BadArgCount, "The input text has too few parameters.");
|
||||||
|
argList.Add(TypeReaderResult.FromSuccess(param.DefaultValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ParseResult.FromSuccess(argList.ToImmutable(), paramList.ToImmutable());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,624 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Discord.Commands.Builders;
|
||||||
|
using Discord.Logging;
|
||||||
|
|
||||||
|
namespace Discord.Commands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Provides a framework for building Discord commands.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// The service provides a framework for building Discord commands both dynamically via runtime builders or
|
||||||
|
/// statically via compile-time modules. To create a command module at compile-time, see
|
||||||
|
/// <see cref="ModuleBase" /> (most common); otherwise, see <see cref="ModuleBuilder" />.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// This service also provides several events for monitoring command usages; such as
|
||||||
|
/// <see cref="Discord.Commands.CommandService.Log" /> for any command-related log events, and
|
||||||
|
/// <see cref="Discord.Commands.CommandService.CommandExecuted" /> for information about commands that have
|
||||||
|
/// been successfully executed.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
public class CommandService : IDisposable
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Occurs when a command-related information is received.
|
||||||
|
/// </summary>
|
||||||
|
public event Func<LogMessage, Task> Log { add { _logEvent.Add(value); } remove { _logEvent.Remove(value); } }
|
||||||
|
internal readonly AsyncEvent<Func<LogMessage, Task>> _logEvent = new AsyncEvent<Func<LogMessage, Task>>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Occurs when a command is successfully executed without any error.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This event is fired when a command has been executed, successfully or not. When a command fails to
|
||||||
|
/// execute during parsing or precondition stage, the CommandInfo may not be returned.
|
||||||
|
/// </remarks>
|
||||||
|
public event Func<Optional<CommandInfo>, ICommandContext, IResult, Task> CommandExecuted { add { _commandExecutedEvent.Add(value); } remove { _commandExecutedEvent.Remove(value); } }
|
||||||
|
internal readonly AsyncEvent<Func<Optional<CommandInfo>, ICommandContext, IResult, Task>> _commandExecutedEvent = new AsyncEvent<Func<Optional<CommandInfo>, ICommandContext, IResult, Task>>();
|
||||||
|
|
||||||
|
private readonly SemaphoreSlim _moduleLock;
|
||||||
|
private readonly ConcurrentDictionary<Type, ModuleInfo> _typedModuleDefs;
|
||||||
|
private readonly ConcurrentDictionary<Type, ConcurrentDictionary<Type, TypeReader>> _typeReaders;
|
||||||
|
private readonly ConcurrentDictionary<Type, TypeReader> _defaultTypeReaders;
|
||||||
|
private readonly ImmutableList<(Type EntityType, Type TypeReaderType)> _entityTypeReaders;
|
||||||
|
private readonly HashSet<ModuleInfo> _moduleDefs;
|
||||||
|
private readonly CommandMap _map;
|
||||||
|
|
||||||
|
internal readonly bool _caseSensitive, _throwOnError, _ignoreExtraArgs;
|
||||||
|
internal readonly char _separatorChar;
|
||||||
|
internal readonly RunMode _defaultRunMode;
|
||||||
|
internal readonly Logger _cmdLogger;
|
||||||
|
internal readonly LogManager _logManager;
|
||||||
|
internal readonly IReadOnlyDictionary<char, char> _quotationMarkAliasMap;
|
||||||
|
|
||||||
|
internal bool _isDisposed;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents all modules loaded within <see cref="CommandService"/>.
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<ModuleInfo> Modules => _moduleDefs.Select(x => x);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents all commands loaded within <see cref="CommandService"/>.
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<CommandInfo> Commands => _moduleDefs.SelectMany(x => x.Commands);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents all <see cref="TypeReader" /> loaded within <see cref="CommandService"/>.
|
||||||
|
/// </summary>
|
||||||
|
public ILookup<Type, TypeReader> TypeReaders => _typeReaders.SelectMany(x => x.Value.Select(y => new { y.Key, y.Value })).ToLookup(x => x.Key, x => x.Value);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new <see cref="CommandService"/> class.
|
||||||
|
/// </summary>
|
||||||
|
public CommandService() : this(new CommandServiceConfig()) { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new <see cref="CommandService"/> class with the provided configuration.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="config">The configuration class.</param>
|
||||||
|
/// <exception cref="InvalidOperationException">
|
||||||
|
/// The <see cref="RunMode"/> cannot be set to <see cref="RunMode.Default"/>.
|
||||||
|
/// </exception>
|
||||||
|
public CommandService(CommandServiceConfig config)
|
||||||
|
{
|
||||||
|
_caseSensitive = config.CaseSensitiveCommands;
|
||||||
|
_throwOnError = config.ThrowOnError;
|
||||||
|
_ignoreExtraArgs = config.IgnoreExtraArgs;
|
||||||
|
_separatorChar = config.SeparatorChar;
|
||||||
|
_defaultRunMode = config.DefaultRunMode;
|
||||||
|
_quotationMarkAliasMap = (config.QuotationMarkAliasMap ?? new Dictionary<char, char>()).ToImmutableDictionary();
|
||||||
|
if (_defaultRunMode == RunMode.Default)
|
||||||
|
throw new InvalidOperationException("The default run mode cannot be set to Default.");
|
||||||
|
|
||||||
|
_logManager = new LogManager(config.LogLevel);
|
||||||
|
_logManager.Message += async msg => await _logEvent.InvokeAsync(msg).ConfigureAwait(false);
|
||||||
|
_cmdLogger = _logManager.CreateLogger("Command");
|
||||||
|
|
||||||
|
_moduleLock = new SemaphoreSlim(1, 1);
|
||||||
|
_typedModuleDefs = new ConcurrentDictionary<Type, ModuleInfo>();
|
||||||
|
_moduleDefs = new HashSet<ModuleInfo>();
|
||||||
|
_map = new CommandMap(this);
|
||||||
|
_typeReaders = new ConcurrentDictionary<Type, ConcurrentDictionary<Type, TypeReader>>();
|
||||||
|
|
||||||
|
_defaultTypeReaders = new ConcurrentDictionary<Type, TypeReader>();
|
||||||
|
foreach (var type in PrimitiveParsers.SupportedTypes)
|
||||||
|
{
|
||||||
|
_defaultTypeReaders[type] = PrimitiveTypeReader.Create(type);
|
||||||
|
_defaultTypeReaders[typeof(Nullable<>).MakeGenericType(type)] = NullableTypeReader.Create(type, _defaultTypeReaders[type]);
|
||||||
|
}
|
||||||
|
|
||||||
|
var tsreader = new TimeSpanTypeReader();
|
||||||
|
_defaultTypeReaders[typeof(TimeSpan)] = tsreader;
|
||||||
|
_defaultTypeReaders[typeof(TimeSpan?)] = NullableTypeReader.Create(typeof(TimeSpan), tsreader);
|
||||||
|
|
||||||
|
_defaultTypeReaders[typeof(string)] =
|
||||||
|
new PrimitiveTypeReader<string>((string x, out string y) => { y = x; return true; }, 0);
|
||||||
|
|
||||||
|
var entityTypeReaders = ImmutableList.CreateBuilder<(Type, Type)>();
|
||||||
|
entityTypeReaders.Add((typeof(IMessage), typeof(MessageTypeReader<>)));
|
||||||
|
entityTypeReaders.Add((typeof(IChannel), typeof(ChannelTypeReader<>)));
|
||||||
|
entityTypeReaders.Add((typeof(IRole), typeof(RoleTypeReader<>)));
|
||||||
|
entityTypeReaders.Add((typeof(IUser), typeof(UserTypeReader<>)));
|
||||||
|
_entityTypeReaders = entityTypeReaders.ToImmutable();
|
||||||
|
}
|
||||||
|
|
||||||
|
//Modules
|
||||||
|
public async Task<ModuleInfo> CreateModuleAsync(string primaryAlias, Action<ModuleBuilder> buildFunc)
|
||||||
|
{
|
||||||
|
await _moduleLock.WaitAsync().ConfigureAwait(false);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var builder = new ModuleBuilder(this, null, primaryAlias);
|
||||||
|
buildFunc(builder);
|
||||||
|
|
||||||
|
var module = builder.Build(this, null);
|
||||||
|
|
||||||
|
return LoadModuleInternal(module);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_moduleLock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add a command module from a <see cref="Type" />.
|
||||||
|
/// </summary>
|
||||||
|
/// <example>
|
||||||
|
/// <para>The following example registers the module <c>MyModule</c> to <c>commandService</c>.</para>
|
||||||
|
/// <code language="cs">
|
||||||
|
/// await commandService.AddModuleAsync<MyModule>(serviceProvider);
|
||||||
|
/// </code>
|
||||||
|
/// </example>
|
||||||
|
/// <typeparam name="T">The type of module.</typeparam>
|
||||||
|
/// <param name="services">The <see cref="IServiceProvider"/> for your dependency injection solution if using one; otherwise, pass <c>null</c>.</param>
|
||||||
|
/// <exception cref="ArgumentException">This module has already been added.</exception>
|
||||||
|
/// <exception cref="InvalidOperationException">
|
||||||
|
/// The <see cref="ModuleInfo"/> fails to be built; an invalid type may have been provided.
|
||||||
|
/// </exception>
|
||||||
|
/// <returns>
|
||||||
|
/// A task that represents the asynchronous operation for adding the module. The task result contains the
|
||||||
|
/// built module.
|
||||||
|
/// </returns>
|
||||||
|
public Task<ModuleInfo> AddModuleAsync<T>(IServiceProvider services) => AddModuleAsync(typeof(T), services);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a command module from a <see cref="Type" />.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type">The type of module.</param>
|
||||||
|
/// <param name="services">The <see cref="IServiceProvider" /> for your dependency injection solution if using one; otherwise, pass <c>null</c> .</param>
|
||||||
|
/// <exception cref="ArgumentException">This module has already been added.</exception>
|
||||||
|
/// <exception cref="InvalidOperationException">
|
||||||
|
/// The <see cref="ModuleInfo"/> fails to be built; an invalid type may have been provided.
|
||||||
|
/// </exception>
|
||||||
|
/// <returns>
|
||||||
|
/// A task that represents the asynchronous operation for adding the module. The task result contains the
|
||||||
|
/// built module.
|
||||||
|
/// </returns>
|
||||||
|
public async Task<ModuleInfo> AddModuleAsync(Type type, IServiceProvider services)
|
||||||
|
{
|
||||||
|
services = services ?? EmptyServiceProvider.Instance;
|
||||||
|
|
||||||
|
await _moduleLock.WaitAsync().ConfigureAwait(false);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var typeInfo = type.GetTypeInfo();
|
||||||
|
|
||||||
|
if (_typedModuleDefs.ContainsKey(type))
|
||||||
|
throw new ArgumentException("This module has already been added.");
|
||||||
|
|
||||||
|
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?");
|
||||||
|
|
||||||
|
_typedModuleDefs[module.Key] = module.Value;
|
||||||
|
|
||||||
|
return LoadModuleInternal(module.Value);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_moduleLock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Add command modules from an <see cref="Assembly"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="assembly">The <see cref="Assembly"/> containing command modules.</param>
|
||||||
|
/// <param name="services">The <see cref="IServiceProvider"/> for your dependency injection solution if using one; otherwise, pass <c>null</c>.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// A task that represents the asynchronous operation for adding the command modules. The task result
|
||||||
|
/// contains an enumerable collection of modules added.
|
||||||
|
/// </returns>
|
||||||
|
public async Task<IEnumerable<ModuleInfo>> 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, services).ConfigureAwait(false);
|
||||||
|
|
||||||
|
foreach (var info in moduleDefs)
|
||||||
|
{
|
||||||
|
_typedModuleDefs[info.Key] = info.Value;
|
||||||
|
LoadModuleInternal(info.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return moduleDefs.Select(x => x.Value).ToImmutableArray();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_moduleLock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private ModuleInfo LoadModuleInternal(ModuleInfo module)
|
||||||
|
{
|
||||||
|
_moduleDefs.Add(module);
|
||||||
|
|
||||||
|
foreach (var command in module.Commands)
|
||||||
|
_map.AddCommand(command);
|
||||||
|
|
||||||
|
foreach (var submodule in module.Submodules)
|
||||||
|
LoadModuleInternal(submodule);
|
||||||
|
|
||||||
|
return module;
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Removes the command module.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="module">The <see cref="ModuleInfo" /> to be removed from the service.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// A task that represents the asynchronous removal operation. The task result contains a value that
|
||||||
|
/// indicates whether the <paramref name="module"/> is successfully removed.
|
||||||
|
/// </returns>
|
||||||
|
public async Task<bool> RemoveModuleAsync(ModuleInfo module)
|
||||||
|
{
|
||||||
|
await _moduleLock.WaitAsync().ConfigureAwait(false);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return RemoveModuleInternal(module);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_moduleLock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Removes the command module.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The <see cref="Type"/> of the module.</typeparam>
|
||||||
|
/// <returns>
|
||||||
|
/// A task that represents the asynchronous removal operation. The task result contains a value that
|
||||||
|
/// indicates whether the module is successfully removed.
|
||||||
|
/// </returns>
|
||||||
|
public Task<bool> RemoveModuleAsync<T>() => RemoveModuleAsync(typeof(T));
|
||||||
|
/// <summary>
|
||||||
|
/// Removes the command module.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type">The <see cref="Type"/> of the module.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// A task that represents the asynchronous removal operation. The task result contains a value that
|
||||||
|
/// indicates whether the module is successfully removed.
|
||||||
|
/// </returns>
|
||||||
|
public async Task<bool> RemoveModuleAsync(Type type)
|
||||||
|
{
|
||||||
|
await _moduleLock.WaitAsync().ConfigureAwait(false);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!_typedModuleDefs.TryRemove(type, out var module))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return RemoveModuleInternal(module);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_moduleLock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private bool RemoveModuleInternal(ModuleInfo module)
|
||||||
|
{
|
||||||
|
if (!_moduleDefs.Remove(module))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
foreach (var cmd in module.Commands)
|
||||||
|
_map.RemoveCommand(cmd);
|
||||||
|
|
||||||
|
foreach (var submodule in module.Submodules)
|
||||||
|
{
|
||||||
|
RemoveModuleInternal(submodule);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Type Readers
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a custom <see cref="TypeReader" /> to this <see cref="CommandService" /> for the supplied object
|
||||||
|
/// type.
|
||||||
|
/// If <typeparamref name="T" /> is a <see cref="ValueType" />, a nullable <see cref="TypeReader" /> will
|
||||||
|
/// also be added.
|
||||||
|
/// If a default <see cref="TypeReader" /> exists for <typeparamref name="T" />, a warning will be logged
|
||||||
|
/// and the default <see cref="TypeReader" /> will be replaced.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The object type to be read by the <see cref="TypeReader"/>.</typeparam>
|
||||||
|
/// <param name="reader">An instance of the <see cref="TypeReader" /> to be added.</param>
|
||||||
|
public void AddTypeReader<T>(TypeReader reader)
|
||||||
|
=> AddTypeReader(typeof(T), reader);
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a custom <see cref="TypeReader" /> to this <see cref="CommandService" /> for the supplied object
|
||||||
|
/// type.
|
||||||
|
/// If <paramref name="type" /> is a <see cref="ValueType" />, a nullable <see cref="TypeReader" /> for the
|
||||||
|
/// value type will also be added.
|
||||||
|
/// If a default <see cref="TypeReader" /> exists for <paramref name="type" />, a warning will be logged and
|
||||||
|
/// the default <see cref="TypeReader" /> will be replaced.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type">A <see cref="Type" /> instance for the type to be read.</param>
|
||||||
|
/// <param name="reader">An instance of the <see cref="TypeReader" /> to be added.</param>
|
||||||
|
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 AddTypeReader<T>(reader, true).");
|
||||||
|
AddTypeReader(type, reader, true);
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a custom <see cref="TypeReader" /> to this <see cref="CommandService" /> for the supplied object
|
||||||
|
/// type.
|
||||||
|
/// If <typeparamref name="T" /> is a <see cref="ValueType" />, a nullable <see cref="TypeReader" /> will
|
||||||
|
/// also be added.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The object type to be read by the <see cref="TypeReader"/>.</typeparam>
|
||||||
|
/// <param name="reader">An instance of the <see cref="TypeReader" /> to be added.</param>
|
||||||
|
/// <param name="replaceDefault">
|
||||||
|
/// Defines whether the <see cref="TypeReader"/> should replace the default one for
|
||||||
|
/// <see cref="Type" /> if it exists.
|
||||||
|
/// </param>
|
||||||
|
public void AddTypeReader<T>(TypeReader reader, bool replaceDefault)
|
||||||
|
=> AddTypeReader(typeof(T), reader, replaceDefault);
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a custom <see cref="TypeReader" /> to this <see cref="CommandService" /> for the supplied object
|
||||||
|
/// type.
|
||||||
|
/// If <paramref name="type" /> is a <see cref="ValueType" />, a nullable <see cref="TypeReader" /> for the
|
||||||
|
/// value type will also be added.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type">A <see cref="Type" /> instance for the type to be read.</param>
|
||||||
|
/// <param name="reader">An instance of the <see cref="TypeReader" /> to be added.</param>
|
||||||
|
/// <param name="replaceDefault">
|
||||||
|
/// Defines whether the <see cref="TypeReader"/> should replace the default one for <see cref="Type" /> if
|
||||||
|
/// it exists.
|
||||||
|
/// </param>
|
||||||
|
public void AddTypeReader(Type type, TypeReader reader, bool replaceDefault)
|
||||||
|
{
|
||||||
|
if (replaceDefault && HasDefaultTypeReader(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<Type, TypeReader>());
|
||||||
|
readers[reader.GetType()] = reader;
|
||||||
|
|
||||||
|
if (type.GetTypeInfo().IsValueType)
|
||||||
|
AddNullableTypeReader(type, reader);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
internal bool HasDefaultTypeReader(Type type)
|
||||||
|
{
|
||||||
|
if (_defaultTypeReaders.ContainsKey(type))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
var typeInfo = type.GetTypeInfo();
|
||||||
|
if (typeInfo.IsEnum)
|
||||||
|
return true;
|
||||||
|
return _entityTypeReaders.Any(x => type == x.EntityType || typeInfo.ImplementedInterfaces.Contains(x.TypeReaderType));
|
||||||
|
}
|
||||||
|
internal void AddNullableTypeReader(Type valueType, TypeReader valueTypeReader)
|
||||||
|
{
|
||||||
|
var readers = _typeReaders.GetOrAdd(typeof(Nullable<>).MakeGenericType(valueType), x => new ConcurrentDictionary<Type, TypeReader>());
|
||||||
|
var nullableReader = NullableTypeReader.Create(valueType, valueTypeReader);
|
||||||
|
readers[nullableReader.GetType()] = nullableReader;
|
||||||
|
}
|
||||||
|
internal IDictionary<Type, TypeReader> GetTypeReaders(Type type)
|
||||||
|
{
|
||||||
|
if (_typeReaders.TryGetValue(type, out var definedTypeReaders))
|
||||||
|
return definedTypeReaders;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
internal TypeReader GetDefaultTypeReader(Type type)
|
||||||
|
{
|
||||||
|
if (_defaultTypeReaders.TryGetValue(type, out var reader))
|
||||||
|
return reader;
|
||||||
|
var typeInfo = type.GetTypeInfo();
|
||||||
|
|
||||||
|
//Is this an enum?
|
||||||
|
if (typeInfo.IsEnum)
|
||||||
|
{
|
||||||
|
reader = EnumTypeReader.GetReader(type);
|
||||||
|
_defaultTypeReaders[type] = reader;
|
||||||
|
return reader;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Is this an entity?
|
||||||
|
for (int i = 0; i < _entityTypeReaders.Count; i++)
|
||||||
|
{
|
||||||
|
if (type == _entityTypeReaders[i].EntityType || typeInfo.ImplementedInterfaces.Contains(_entityTypeReaders[i].EntityType))
|
||||||
|
{
|
||||||
|
reader = Activator.CreateInstance(_entityTypeReaders[i].TypeReaderType.MakeGenericType(type)) as TypeReader;
|
||||||
|
_defaultTypeReaders[type] = reader;
|
||||||
|
return reader;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Execution
|
||||||
|
/// <summary>
|
||||||
|
/// Searches for the command.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">The context of the command.</param>
|
||||||
|
/// <param name="argPos">The position of which the command starts at.</param>
|
||||||
|
/// <returns>The result containing the matching commands.</returns>
|
||||||
|
public SearchResult Search(ICommandContext context, int argPos)
|
||||||
|
=> Search(context.Message.Content.Substring(argPos));
|
||||||
|
/// <summary>
|
||||||
|
/// Searches for the command.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">The context of the command.</param>
|
||||||
|
/// <param name="input">The command string.</param>
|
||||||
|
/// <returns>The result containing the matching commands.</returns>
|
||||||
|
public SearchResult Search(ICommandContext context, string input)
|
||||||
|
=> Search(input);
|
||||||
|
public SearchResult Search(string input)
|
||||||
|
{
|
||||||
|
string searchInput = _caseSensitive ? input : input.ToLowerInvariant();
|
||||||
|
var matches = _map.GetCommands(searchInput).OrderByDescending(x => x.Command.Priority).ToImmutableArray();
|
||||||
|
|
||||||
|
if (matches.Length > 0)
|
||||||
|
return SearchResult.FromSuccess(input, matches);
|
||||||
|
else
|
||||||
|
return SearchResult.FromError(CommandError.UnknownCommand, "Unknown command.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the command.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">The context of the command.</param>
|
||||||
|
/// <param name="argPos">The position of which the command starts at.</param>
|
||||||
|
/// <param name="services">The service to be used in the command's dependency injection.</param>
|
||||||
|
/// <param name="multiMatchHandling">The handling mode when multiple command matches are found.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// A task that represents the asynchronous execution operation. The task result contains the result of the
|
||||||
|
/// command execution.
|
||||||
|
/// </returns>
|
||||||
|
public Task<IResult> ExecuteAsync(ICommandContext context, int argPos, IServiceProvider services, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception)
|
||||||
|
=> ExecuteAsync(context, context.Message.Content.Substring(argPos), services, multiMatchHandling);
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the command.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">The context of the command.</param>
|
||||||
|
/// <param name="input">The command string.</param>
|
||||||
|
/// <param name="services">The service to be used in the command's dependency injection.</param>
|
||||||
|
/// <param name="multiMatchHandling">The handling mode when multiple command matches are found.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// A task that represents the asynchronous execution operation. The task result contains the result of the
|
||||||
|
/// command execution.
|
||||||
|
/// </returns>
|
||||||
|
public async Task<IResult> ExecuteAsync(ICommandContext context, string input, IServiceProvider services, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception)
|
||||||
|
{
|
||||||
|
services = services ?? EmptyServiceProvider.Instance;
|
||||||
|
|
||||||
|
var searchResult = Search(input);
|
||||||
|
if (!searchResult.IsSuccess)
|
||||||
|
{
|
||||||
|
await _commandExecutedEvent.InvokeAsync(Optional.Create<CommandInfo>(), context, searchResult).ConfigureAwait(false);
|
||||||
|
return searchResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var commands = searchResult.Commands;
|
||||||
|
var preconditionResults = new Dictionary<CommandMatch, PreconditionResult>();
|
||||||
|
|
||||||
|
foreach (var match in commands)
|
||||||
|
{
|
||||||
|
preconditionResults[match] = await match.Command.CheckPreconditionsAsync(context, services).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
var successfulPreconditions = preconditionResults
|
||||||
|
.Where(x => x.Value.IsSuccess)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
if (successfulPreconditions.Length == 0)
|
||||||
|
{
|
||||||
|
//All preconditions failed, return the one from the highest priority command
|
||||||
|
var bestCandidate = preconditionResults
|
||||||
|
.OrderByDescending(x => x.Key.Command.Priority)
|
||||||
|
.FirstOrDefault(x => !x.Value.IsSuccess);
|
||||||
|
|
||||||
|
await _commandExecutedEvent.InvokeAsync(bestCandidate.Key.Command, context, bestCandidate.Value).ConfigureAwait(false);
|
||||||
|
return bestCandidate.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
//If we get this far, at least one precondition was successful.
|
||||||
|
|
||||||
|
var parseResultsDict = new Dictionary<CommandMatch, ParseResult>();
|
||||||
|
foreach (var pair in successfulPreconditions)
|
||||||
|
{
|
||||||
|
var parseResult = await pair.Key.ParseAsync(context, searchResult, pair.Value, services).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (parseResult.Error == CommandError.MultipleMatches)
|
||||||
|
{
|
||||||
|
IReadOnlyList<TypeReaderValue> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
{
|
||||||
|
var argValuesSum = parseResult.ArgValues?.Sum(x => x.Values.OrderByDescending(y => y.Score).FirstOrDefault().Score) ?? 0;
|
||||||
|
var paramValuesSum = parseResult.ParamValues?.Sum(x => x.Values.OrderByDescending(y => y.Score).FirstOrDefault().Score) ?? 0;
|
||||||
|
|
||||||
|
argValuesScore = argValuesSum / match.Command.Parameters.Count;
|
||||||
|
paramValuesScore = paramValuesSum / match.Command.Parameters.Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalArgsScore = (argValuesScore + paramValuesScore) / 2;
|
||||||
|
return match.Command.Priority + totalArgsScore * 0.99f;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Order the parse results by their score so that we choose the most likely result to execute
|
||||||
|
var parseResults = parseResultsDict
|
||||||
|
.OrderByDescending(x => CalculateScore(x.Key, x.Value));
|
||||||
|
|
||||||
|
var successfulParses = parseResults
|
||||||
|
.Where(x => x.Value.IsSuccess)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
if (successfulParses.Length == 0)
|
||||||
|
{
|
||||||
|
//All parses failed, return the one from the highest priority command, using score as a tie breaker
|
||||||
|
var bestMatch = parseResults
|
||||||
|
.FirstOrDefault(x => !x.Value.IsSuccess);
|
||||||
|
|
||||||
|
await _commandExecutedEvent.InvokeAsync(bestMatch.Key.Command, context, bestMatch.Value).ConfigureAwait(false);
|
||||||
|
return bestMatch.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
//If we get this far, at least one parse was successful. Execute the most likely overload.
|
||||||
|
var chosenOverload = successfulParses[0];
|
||||||
|
var result = await chosenOverload.Key.ExecuteAsync(context, chosenOverload.Value, services).ConfigureAwait(false);
|
||||||
|
if (!result.IsSuccess && !(result is RuntimeResult || result is ExecuteResult)) // succesful results raise the event in CommandInfo#ExecuteInternalAsync (have to raise it there b/c deffered execution)
|
||||||
|
await _commandExecutedEvent.InvokeAsync(chosenOverload.Key.Command, context, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (!_isDisposed)
|
||||||
|
{
|
||||||
|
if (disposing)
|
||||||
|
{
|
||||||
|
_moduleLock?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
_isDisposed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void IDisposable.Dispose()
|
||||||
|
{
|
||||||
|
Dispose(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,62 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Discord.Commands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a configuration class for <see cref="CommandService"/>.
|
||||||
|
/// </summary>
|
||||||
|
public class CommandServiceConfig
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the default <see cref="RunMode" /> commands should have, if one is not specified on the
|
||||||
|
/// Command attribute or builder.
|
||||||
|
/// </summary>
|
||||||
|
public RunMode DefaultRunMode { get; set; } = RunMode.Sync;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the <see cref="char"/> that separates an argument with another.
|
||||||
|
/// </summary>
|
||||||
|
public char SeparatorChar { get; set; } = ' ';
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets whether commands should be case-sensitive.
|
||||||
|
/// </summary>
|
||||||
|
public bool CaseSensitiveCommands { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the minimum log level severity that will be sent to the <see cref="CommandService.Log"/> event.
|
||||||
|
/// </summary>
|
||||||
|
public LogSeverity LogLevel { get; set; } = LogSeverity.Info;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets whether <see cref="RunMode.Sync"/> commands should push exceptions up to the caller.
|
||||||
|
/// </summary>
|
||||||
|
public bool ThrowOnError { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Collection of aliases for matching pairs of string delimiters.
|
||||||
|
/// The dictionary stores the opening delimiter as a key, and the matching closing delimiter as the value.
|
||||||
|
/// If no value is supplied <see cref="QuotationAliasUtils.GetDefaultAliasMap"/> will be used, which contains
|
||||||
|
/// many regional equivalents.
|
||||||
|
/// Only values that are specified in this map will be used as string delimiters, so if " is removed then
|
||||||
|
/// it won't be used.
|
||||||
|
/// If this map is set to null or empty, the default delimiter of " will be used.
|
||||||
|
/// </summary>
|
||||||
|
/// <example>
|
||||||
|
/// <code language="cs">
|
||||||
|
/// QuotationMarkAliasMap = new Dictionary<char, char%gt;()
|
||||||
|
/// {
|
||||||
|
/// {'\"', '\"' },
|
||||||
|
/// {'“', '”' },
|
||||||
|
/// {'「', '」' },
|
||||||
|
/// }
|
||||||
|
/// </code>
|
||||||
|
/// </example>
|
||||||
|
public Dictionary<char, char> QuotationMarkAliasMap { get; set; } = QuotationAliasUtils.GetDefaultAliasMap;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value that indicates whether extra parameters should be ignored.
|
||||||
|
/// </summary>
|
||||||
|
public bool IgnoreExtraArgs { get; set; } = false;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Discord.Commands
|
||||||
|
{
|
||||||
|
internal class EmptyServiceProvider : IServiceProvider
|
||||||
|
{
|
||||||
|
public static readonly EmptyServiceProvider Instance = new EmptyServiceProvider();
|
||||||
|
|
||||||
|
public object GetService(Type serviceType) => null;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,76 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Discord.Commands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Provides extension methods for the <see cref="CommandService"/> class.
|
||||||
|
/// </summary>
|
||||||
|
public static class CommandServiceExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Returns commands that can be executed under the current context.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="commands">The set of commands to be checked against.</param>
|
||||||
|
/// <param name="context">The current command context.</param>
|
||||||
|
/// <param name="provider">The service provider used for dependency injection upon precondition check.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// A read-only collection of commands that can be executed under the current context.
|
||||||
|
/// </returns>
|
||||||
|
public static async Task<IReadOnlyCollection<CommandInfo>> GetExecutableCommandsAsync(this ICollection<CommandInfo> commands, ICommandContext context, IServiceProvider provider)
|
||||||
|
{
|
||||||
|
var executableCommands = new List<CommandInfo>();
|
||||||
|
|
||||||
|
var tasks = commands.Select(async c =>
|
||||||
|
{
|
||||||
|
var result = await c.CheckPreconditionsAsync(context, provider).ConfigureAwait(false);
|
||||||
|
return new { Command = c, PreconditionResult = result };
|
||||||
|
});
|
||||||
|
|
||||||
|
var results = await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||||
|
|
||||||
|
foreach (var result in results)
|
||||||
|
{
|
||||||
|
if (result.PreconditionResult.IsSuccess)
|
||||||
|
executableCommands.Add(result.Command);
|
||||||
|
}
|
||||||
|
|
||||||
|
return executableCommands;
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Returns commands that can be executed under the current context.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="commandService">The desired command service class to check against.</param>
|
||||||
|
/// <param name="context">The current command context.</param>
|
||||||
|
/// <param name="provider">The service provider used for dependency injection upon precondition check.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// A read-only collection of commands that can be executed under the current context.
|
||||||
|
/// </returns>
|
||||||
|
public static Task<IReadOnlyCollection<CommandInfo>> GetExecutableCommandsAsync(this CommandService commandService, ICommandContext context, IServiceProvider provider)
|
||||||
|
=> GetExecutableCommandsAsync(commandService.Commands.ToArray(), context, provider);
|
||||||
|
/// <summary>
|
||||||
|
/// Returns commands that can be executed under the current context.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="module">The module to be checked against.</param>
|
||||||
|
/// <param name="context">The current command context.</param>
|
||||||
|
/// <param name="provider">The service provider used for dependency injection upon precondition check.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// A read-only collection of commands that can be executed under the current context.
|
||||||
|
/// </returns>
|
||||||
|
public static async Task<IReadOnlyCollection<CommandInfo>> GetExecutableCommandsAsync(this ModuleInfo module, ICommandContext context, IServiceProvider provider)
|
||||||
|
{
|
||||||
|
var executableCommands = new List<CommandInfo>();
|
||||||
|
|
||||||
|
executableCommands.AddRange(await module.Commands.ToArray().GetExecutableCommandsAsync(context, provider).ConfigureAwait(false));
|
||||||
|
|
||||||
|
var tasks = module.Submodules.Select(async s => await s.GetExecutableCommandsAsync(context, provider).ConfigureAwait(false));
|
||||||
|
var results = await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||||
|
|
||||||
|
executableCommands.AddRange(results.SelectMany(c => c));
|
||||||
|
|
||||||
|
return executableCommands;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Discord.Commands
|
||||||
|
{
|
||||||
|
public static class IEnumerableExtensions
|
||||||
|
{
|
||||||
|
public static IEnumerable<TResult> Permutate<TFirst, TSecond, TResult>(
|
||||||
|
this IEnumerable<TFirst> set,
|
||||||
|
IEnumerable<TSecond> others,
|
||||||
|
Func<TFirst, TSecond, TResult> func)
|
||||||
|
{
|
||||||
|
foreach (TFirst elem in set)
|
||||||
|
{
|
||||||
|
foreach (TSecond elem2 in others)
|
||||||
|
{
|
||||||
|
yield return func(elem, elem2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,64 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Discord.Commands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Provides extension methods for <see cref="IUserMessage" /> that relates to commands.
|
||||||
|
/// </summary>
|
||||||
|
public static class MessageExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets whether the message starts with the provided character.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="msg">The message to check against.</param>
|
||||||
|
/// <param name="c">The char prefix.</param>
|
||||||
|
/// <param name="argPos">References where the command starts.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// <c>true</c> if the message begins with the char <paramref name="c"/>; otherwise <c>false</c>.
|
||||||
|
/// </returns>
|
||||||
|
public static bool HasCharPrefix(this IUserMessage msg, char c, ref int argPos)
|
||||||
|
{
|
||||||
|
var text = msg.Content;
|
||||||
|
if (!string.IsNullOrEmpty(text) && text[0] == c)
|
||||||
|
{
|
||||||
|
argPos = 1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Gets whether the message starts with the provided string.
|
||||||
|
/// </summary>
|
||||||
|
public static bool HasStringPrefix(this IUserMessage msg, string str, ref int argPos, StringComparison comparisonType = StringComparison.Ordinal)
|
||||||
|
{
|
||||||
|
var text = msg.Content;
|
||||||
|
if (!string.IsNullOrEmpty(text) && text.StartsWith(str, comparisonType))
|
||||||
|
{
|
||||||
|
argPos = str.Length;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Gets whether the message starts with the user's mention string.
|
||||||
|
/// </summary>
|
||||||
|
public static bool HasMentionPrefix(this IUserMessage msg, IUser user, ref int argPos)
|
||||||
|
{
|
||||||
|
var text = msg.Content;
|
||||||
|
if (string.IsNullOrEmpty(text) || text.Length <= 3 || text[0] != '<' || text[1] != '@') return false;
|
||||||
|
|
||||||
|
int endPos = text.IndexOf('>');
|
||||||
|
if (endPos == -1) return false;
|
||||||
|
if (text.Length < endPos + 2 || text[endPos + 1] != ' ') return false; //Must end in "> "
|
||||||
|
|
||||||
|
ulong userId;
|
||||||
|
if (!MentionUtils.TryParseUser(text.Substring(0, endPos + 1), out userId)) return false;
|
||||||
|
if (userId == user.Id)
|
||||||
|
{
|
||||||
|
argPos = endPos + 2;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
using Discord.Commands.Builders;
|
||||||
|
|
||||||
|
namespace Discord.Commands
|
||||||
|
{
|
||||||
|
internal interface IModuleBase
|
||||||
|
{
|
||||||
|
void SetContext(ICommandContext context);
|
||||||
|
|
||||||
|
void BeforeExecute(CommandInfo command);
|
||||||
|
|
||||||
|
void AfterExecute(CommandInfo command);
|
||||||
|
|
||||||
|
void OnModuleBuilding(CommandService commandService, ModuleBuilder builder);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,339 @@
|
|||||||
|
using Discord.Commands.Builders;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.ExceptionServices;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Discord.Commands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Provides the information of a command.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This object contains the information of a command. This can include the module of the command, various
|
||||||
|
/// descriptions regarding the command, and its <see cref="RunMode"/>.
|
||||||
|
/// </remarks>
|
||||||
|
[DebuggerDisplay("{Name,nq}")]
|
||||||
|
public class CommandInfo
|
||||||
|
{
|
||||||
|
private static readonly System.Reflection.MethodInfo _convertParamsMethod = typeof(CommandInfo).GetTypeInfo().GetDeclaredMethod(nameof(ConvertParamsList));
|
||||||
|
private static readonly ConcurrentDictionary<Type, Func<IEnumerable<object>, object>> _arrayConverters = new ConcurrentDictionary<Type, Func<IEnumerable<object>, object>>();
|
||||||
|
|
||||||
|
private readonly CommandService _commandService;
|
||||||
|
private readonly Func<ICommandContext, object[], IServiceProvider, CommandInfo, Task> _action;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the module that the command belongs in.
|
||||||
|
/// </summary>
|
||||||
|
public ModuleInfo Module { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the name of the command. If none is set, the first alias is used.
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the summary of the command.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This field returns the summary of the command. <see cref="Summary"/> and <see cref="Remarks"/> can be
|
||||||
|
/// useful in help commands and various implementation that fetches details of the command for the user.
|
||||||
|
/// </remarks>
|
||||||
|
public string Summary { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the remarks of the command.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This field returns the summary of the command. <see cref="Summary"/> and <see cref="Remarks"/> can be
|
||||||
|
/// useful in help commands and various implementation that fetches details of the command for the user.
|
||||||
|
/// </remarks>
|
||||||
|
public string Remarks { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the priority of the command. This is used when there are multiple overloads of the command.
|
||||||
|
/// </summary>
|
||||||
|
public int Priority { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates whether the command accepts a <see langword="params"/> <see cref="Type"/>[] for its
|
||||||
|
/// parameter.
|
||||||
|
/// </summary>
|
||||||
|
public bool HasVarArgs { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates whether extra arguments should be ignored for this command.
|
||||||
|
/// </summary>
|
||||||
|
public bool IgnoreExtraArgs { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the <see cref="RunMode" /> that is being used for the command.
|
||||||
|
/// </summary>
|
||||||
|
public RunMode RunMode { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a list of aliases defined by the <see cref="AliasAttribute" /> of the command.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<string> Aliases { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a list of information about the parameters of the command.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<ParameterInfo> Parameters { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a list of preconditions defined by the <see cref="PreconditionAttribute" /> of the command.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<PreconditionAttribute> Preconditions { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a list of attributes of the command.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<Attribute> Attributes { get; }
|
||||||
|
|
||||||
|
internal CommandInfo(CommandBuilder builder, ModuleInfo module, CommandService service)
|
||||||
|
{
|
||||||
|
Module = module;
|
||||||
|
|
||||||
|
Name = builder.Name;
|
||||||
|
Summary = builder.Summary;
|
||||||
|
Remarks = builder.Remarks;
|
||||||
|
|
||||||
|
RunMode = (builder.RunMode == RunMode.Default ? service._defaultRunMode : builder.RunMode);
|
||||||
|
Priority = builder.Priority;
|
||||||
|
|
||||||
|
Aliases = module.Aliases
|
||||||
|
.Permutate(builder.Aliases, (first, second) =>
|
||||||
|
{
|
||||||
|
if (first == "")
|
||||||
|
return second;
|
||||||
|
else if (second == "")
|
||||||
|
return first;
|
||||||
|
else
|
||||||
|
return first + service._separatorChar + second;
|
||||||
|
})
|
||||||
|
.Select(x => service._caseSensitive ? x : x.ToLowerInvariant())
|
||||||
|
.ToImmutableArray();
|
||||||
|
|
||||||
|
Preconditions = builder.Preconditions.ToImmutableArray();
|
||||||
|
Attributes = builder.Attributes.ToImmutableArray();
|
||||||
|
|
||||||
|
Parameters = builder.Parameters.Select(x => x.Build(this)).ToImmutableArray();
|
||||||
|
HasVarArgs = builder.Parameters.Count > 0 && builder.Parameters[builder.Parameters.Count - 1].IsMultiple;
|
||||||
|
IgnoreExtraArgs = builder.IgnoreExtraArgs;
|
||||||
|
|
||||||
|
_action = builder.Callback;
|
||||||
|
_commandService = service;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<PreconditionResult> CheckPreconditionsAsync(ICommandContext context, IServiceProvider services = null)
|
||||||
|
{
|
||||||
|
services = services ?? EmptyServiceProvider.Instance;
|
||||||
|
|
||||||
|
async Task<PreconditionResult> CheckGroups(IEnumerable<PreconditionAttribute> preconditions, string type)
|
||||||
|
{
|
||||||
|
foreach (IGrouping<string, PreconditionAttribute> preconditionGroup in preconditions.GroupBy(p => p.Group, StringComparer.Ordinal))
|
||||||
|
{
|
||||||
|
if (preconditionGroup.Key == null)
|
||||||
|
{
|
||||||
|
foreach (PreconditionAttribute precondition in preconditionGroup)
|
||||||
|
{
|
||||||
|
var result = await precondition.CheckPermissionsAsync(context, this, services).ConfigureAwait(false);
|
||||||
|
if (!result.IsSuccess)
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var results = new List<PreconditionResult>();
|
||||||
|
foreach (PreconditionAttribute precondition in preconditionGroup)
|
||||||
|
results.Add(await precondition.CheckPermissionsAsync(context, this, services).ConfigureAwait(false));
|
||||||
|
|
||||||
|
if (!results.Any(p => p.IsSuccess))
|
||||||
|
return PreconditionGroupResult.FromError($"{type} precondition group {preconditionGroup.Key} failed.", results);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return PreconditionGroupResult.FromSuccess();
|
||||||
|
}
|
||||||
|
|
||||||
|
var moduleResult = await CheckGroups(Module.Preconditions, "Module").ConfigureAwait(false);
|
||||||
|
if (!moduleResult.IsSuccess)
|
||||||
|
return moduleResult;
|
||||||
|
|
||||||
|
var commandResult = await CheckGroups(Preconditions, "Command").ConfigureAwait(false);
|
||||||
|
if (!commandResult.IsSuccess)
|
||||||
|
return commandResult;
|
||||||
|
|
||||||
|
return PreconditionResult.FromSuccess();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ParseResult> ParseAsync(ICommandContext context, int startIndex, SearchResult searchResult, PreconditionResult preconditionResult = null, IServiceProvider services = null)
|
||||||
|
{
|
||||||
|
services = services ?? EmptyServiceProvider.Instance;
|
||||||
|
|
||||||
|
if (!searchResult.IsSuccess)
|
||||||
|
return ParseResult.FromError(searchResult);
|
||||||
|
if (preconditionResult != null && !preconditionResult.IsSuccess)
|
||||||
|
return ParseResult.FromError(preconditionResult);
|
||||||
|
|
||||||
|
string input = searchResult.Text.Substring(startIndex);
|
||||||
|
|
||||||
|
return await CommandParser.ParseArgsAsync(this, context, _commandService._ignoreExtraArgs, services, input, 0, _commandService._quotationMarkAliasMap).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<IResult> ExecuteAsync(ICommandContext context, ParseResult parseResult, IServiceProvider services)
|
||||||
|
{
|
||||||
|
if (!parseResult.IsSuccess)
|
||||||
|
return Task.FromResult((IResult)ExecuteResult.FromError(parseResult));
|
||||||
|
|
||||||
|
var argList = new object[parseResult.ArgValues.Count];
|
||||||
|
for (int i = 0; i < parseResult.ArgValues.Count; i++)
|
||||||
|
{
|
||||||
|
if (!parseResult.ArgValues[i].IsSuccess)
|
||||||
|
return Task.FromResult((IResult)ExecuteResult.FromError(parseResult.ArgValues[i]));
|
||||||
|
argList[i] = parseResult.ArgValues[i].Values.First().Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
var paramList = new object[parseResult.ParamValues.Count];
|
||||||
|
for (int i = 0; i < parseResult.ParamValues.Count; i++)
|
||||||
|
{
|
||||||
|
if (!parseResult.ParamValues[i].IsSuccess)
|
||||||
|
return Task.FromResult((IResult)ExecuteResult.FromError(parseResult.ParamValues[i]));
|
||||||
|
paramList[i] = parseResult.ParamValues[i].Values.First().Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ExecuteAsync(context, argList, paramList, services);
|
||||||
|
}
|
||||||
|
public async Task<IResult> ExecuteAsync(ICommandContext context, IEnumerable<object> argList, IEnumerable<object> paramList, IServiceProvider services)
|
||||||
|
{
|
||||||
|
services = services ?? EmptyServiceProvider.Instance;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
object[] args = GenerateArgs(argList, paramList);
|
||||||
|
|
||||||
|
for (int position = 0; position < Parameters.Count; position++)
|
||||||
|
{
|
||||||
|
var parameter = Parameters[position];
|
||||||
|
object argument = args[position];
|
||||||
|
var result = await parameter.CheckPreconditionsAsync(context, argument, services).ConfigureAwait(false);
|
||||||
|
if (!result.IsSuccess)
|
||||||
|
{
|
||||||
|
await Module.Service._commandExecutedEvent.InvokeAsync(this, context, result).ConfigureAwait(false);
|
||||||
|
return ExecuteResult.FromError(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (RunMode)
|
||||||
|
{
|
||||||
|
case RunMode.Sync: //Always sync
|
||||||
|
return await ExecuteInternalAsync(context, args, services).ConfigureAwait(false);
|
||||||
|
case RunMode.Async: //Always async
|
||||||
|
var t2 = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await ExecuteInternalAsync(context, args, services).ConfigureAwait(false);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return ExecuteResult.FromSuccess();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return ExecuteResult.FromError(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<IResult> ExecuteInternalAsync(ICommandContext context, object[] args, IServiceProvider services)
|
||||||
|
{
|
||||||
|
await Module.Service._cmdLogger.DebugAsync($"Executing {GetLogText(context)}").ConfigureAwait(false);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var task = _action(context, args, services, this);
|
||||||
|
if (task is Task<IResult> resultTask)
|
||||||
|
{
|
||||||
|
var result = await resultTask.ConfigureAwait(false);
|
||||||
|
await Module.Service._commandExecutedEvent.InvokeAsync(this, context, result).ConfigureAwait(false);
|
||||||
|
if (result is RuntimeResult execResult)
|
||||||
|
return execResult;
|
||||||
|
}
|
||||||
|
else if (task is Task<ExecuteResult> execTask)
|
||||||
|
{
|
||||||
|
var result = await execTask.ConfigureAwait(false);
|
||||||
|
await Module.Service._commandExecutedEvent.InvokeAsync(this, context, result).ConfigureAwait(false);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await task.ConfigureAwait(false);
|
||||||
|
var result = ExecuteResult.FromSuccess();
|
||||||
|
await Module.Service._commandExecutedEvent.InvokeAsync(this, context, result).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
var executeResult = ExecuteResult.FromSuccess();
|
||||||
|
return executeResult;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
var originalEx = ex;
|
||||||
|
while (ex is TargetInvocationException) //Happens with void-returning commands
|
||||||
|
ex = ex.InnerException;
|
||||||
|
|
||||||
|
var wrappedEx = new CommandException(this, context, ex);
|
||||||
|
await Module.Service._cmdLogger.ErrorAsync(wrappedEx).ConfigureAwait(false);
|
||||||
|
|
||||||
|
var result = ExecuteResult.FromError(ex);
|
||||||
|
await Module.Service._commandExecutedEvent.InvokeAsync(this, context, result).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (Module.Service._throwOnError)
|
||||||
|
{
|
||||||
|
if (ex == originalEx)
|
||||||
|
throw;
|
||||||
|
else
|
||||||
|
ExceptionDispatchInfo.Capture(ex).Throw();
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await Module.Service._cmdLogger.VerboseAsync($"Executed {GetLogText(context)}").ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private object[] GenerateArgs(IEnumerable<object> argList, IEnumerable<object> paramsList)
|
||||||
|
{
|
||||||
|
int argCount = Parameters.Count;
|
||||||
|
var array = new object[Parameters.Count];
|
||||||
|
if (HasVarArgs)
|
||||||
|
argCount--;
|
||||||
|
|
||||||
|
int i = 0;
|
||||||
|
foreach (object arg in argList)
|
||||||
|
{
|
||||||
|
if (i == argCount)
|
||||||
|
throw new InvalidOperationException("Command was invoked with too many parameters.");
|
||||||
|
array[i++] = arg;
|
||||||
|
}
|
||||||
|
if (i < argCount)
|
||||||
|
throw new InvalidOperationException("Command was invoked with too few parameters.");
|
||||||
|
|
||||||
|
if (HasVarArgs)
|
||||||
|
{
|
||||||
|
var func = _arrayConverters.GetOrAdd(Parameters[Parameters.Count - 1].Type, t =>
|
||||||
|
{
|
||||||
|
var method = _convertParamsMethod.MakeGenericMethod(t);
|
||||||
|
return (Func<IEnumerable<object>, object>)method.CreateDelegate(typeof(Func<IEnumerable<object>, object>));
|
||||||
|
});
|
||||||
|
array[i] = func(paramsList);
|
||||||
|
}
|
||||||
|
|
||||||
|
return array;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static T[] ConvertParamsList<T>(IEnumerable<object> paramsList)
|
||||||
|
=> paramsList.Cast<T>().ToArray();
|
||||||
|
|
||||||
|
internal string GetLogText(ICommandContext context)
|
||||||
|
{
|
||||||
|
if (context.Guild != null)
|
||||||
|
return $"\"{Name}\" for {context.User} in {context.Guild}/{context.Channel}";
|
||||||
|
else
|
||||||
|
return $"\"{Name}\" for {context.User} in {context.Channel}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,147 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using Discord.Commands.Builders;
|
||||||
|
|
||||||
|
namespace Discord.Commands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Provides the information of a module.
|
||||||
|
/// </summary>
|
||||||
|
public class ModuleInfo
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the command service associated with this module.
|
||||||
|
/// </summary>
|
||||||
|
public CommandService Service { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the name of this module.
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the summary of this module.
|
||||||
|
/// </summary>
|
||||||
|
public string Summary { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the remarks of this module.
|
||||||
|
/// </summary>
|
||||||
|
public string Remarks { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the group name (main prefix) of this module.
|
||||||
|
/// </summary>
|
||||||
|
public string Group { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a read-only list of aliases associated with this module.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<string> Aliases { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a read-only list of commands associated with this module.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<CommandInfo> Commands { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a read-only list of preconditions that apply to this module.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<PreconditionAttribute> Preconditions { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a read-only list of attributes that apply to this module.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<Attribute> Attributes { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a read-only list of submodules associated with this module.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<ModuleInfo> Submodules { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the parent module of this submodule if applicable.
|
||||||
|
/// </summary>
|
||||||
|
public ModuleInfo Parent { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a value that indicates whether this module is a submodule or not.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsSubmodule => Parent != null;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
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, services).ToImmutableArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<string> BuildAliases(ModuleBuilder builder, CommandService service)
|
||||||
|
{
|
||||||
|
var result = builder.Aliases.ToList();
|
||||||
|
var builderQueue = new Queue<ModuleBuilder>();
|
||||||
|
|
||||||
|
var parent = builder;
|
||||||
|
while ((parent = parent.Parent) != null)
|
||||||
|
builderQueue.Enqueue(parent);
|
||||||
|
|
||||||
|
while (builderQueue.Count > 0)
|
||||||
|
{
|
||||||
|
var level = builderQueue.Dequeue();
|
||||||
|
// permute in reverse because we want to *prefix* our aliases
|
||||||
|
result = level.Aliases.Permutate(result, (first, second) =>
|
||||||
|
{
|
||||||
|
if (first == "")
|
||||||
|
return second;
|
||||||
|
else if (second == "")
|
||||||
|
return first;
|
||||||
|
else
|
||||||
|
return first + service._separatorChar + second;
|
||||||
|
}).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ModuleInfo> BuildSubmodules(ModuleBuilder parent, CommandService service, IServiceProvider services)
|
||||||
|
{
|
||||||
|
var result = new List<ModuleInfo>();
|
||||||
|
|
||||||
|
foreach (var submodule in parent.Modules)
|
||||||
|
result.Add(submodule.Build(service, services, this));
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<PreconditionAttribute> BuildPreconditions(ModuleBuilder builder)
|
||||||
|
{
|
||||||
|
var result = new List<PreconditionAttribute>();
|
||||||
|
|
||||||
|
ModuleBuilder parent = builder;
|
||||||
|
while (parent != null)
|
||||||
|
{
|
||||||
|
result.AddRange(parent.Preconditions);
|
||||||
|
parent = parent.Parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Attribute> BuildAttributes(ModuleBuilder builder)
|
||||||
|
{
|
||||||
|
var result = new List<Attribute>();
|
||||||
|
|
||||||
|
ModuleBuilder parent = builder;
|
||||||
|
while (parent != null)
|
||||||
|
{
|
||||||
|
result.AddRange(parent.Attributes);
|
||||||
|
parent = parent.Parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,99 @@
|
|||||||
|
using Discord.Commands.Builders;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Discord.Commands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Provides the information of a parameter.
|
||||||
|
/// </summary>
|
||||||
|
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
|
||||||
|
public class ParameterInfo
|
||||||
|
{
|
||||||
|
private readonly TypeReader _reader;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the command that associates with this parameter.
|
||||||
|
/// </summary>
|
||||||
|
public CommandInfo Command { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the name of this parameter.
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the summary of this parameter.
|
||||||
|
/// </summary>
|
||||||
|
public string Summary { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a value that indicates whether this parameter is optional or not.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsOptional { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a value that indicates whether this parameter is a remainder parameter or not.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsRemainder { get; }
|
||||||
|
public bool IsMultiple { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the type of the parameter.
|
||||||
|
/// </summary>
|
||||||
|
public Type Type { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the default value for this optional parameter if applicable.
|
||||||
|
/// </summary>
|
||||||
|
public object DefaultValue { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a read-only list of precondition that apply to this parameter.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<ParameterPreconditionAttribute> Preconditions { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a read-only list of attributes that apply to this parameter.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<Attribute> Attributes { get; }
|
||||||
|
|
||||||
|
internal ParameterInfo(ParameterBuilder builder, CommandInfo command, CommandService service)
|
||||||
|
{
|
||||||
|
Command = command;
|
||||||
|
|
||||||
|
Name = builder.Name;
|
||||||
|
Summary = builder.Summary;
|
||||||
|
IsOptional = builder.IsOptional;
|
||||||
|
IsRemainder = builder.IsRemainder;
|
||||||
|
IsMultiple = builder.IsMultiple;
|
||||||
|
|
||||||
|
Type = builder.ParameterType;
|
||||||
|
DefaultValue = builder.DefaultValue;
|
||||||
|
|
||||||
|
Preconditions = builder.Preconditions.ToImmutableArray();
|
||||||
|
Attributes = builder.Attributes.ToImmutableArray();
|
||||||
|
|
||||||
|
_reader = builder.TypeReader;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<PreconditionResult> CheckPreconditionsAsync(ICommandContext context, object arg, IServiceProvider services = null)
|
||||||
|
{
|
||||||
|
services = services ?? EmptyServiceProvider.Instance;
|
||||||
|
|
||||||
|
foreach (var precondition in Preconditions)
|
||||||
|
{
|
||||||
|
var result = await precondition.CheckPermissionsAsync(context, this, arg, services).ConfigureAwait(false);
|
||||||
|
if (!result.IsSuccess)
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return PreconditionResult.FromSuccess();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<TypeReaderResult> ParseAsync(ICommandContext context, string input, IServiceProvider services = null)
|
||||||
|
{
|
||||||
|
services = services ?? EmptyServiceProvider.Instance;
|
||||||
|
return await _reader.ReadAsync(context, input, services).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString() => Name;
|
||||||
|
private string DebuggerDisplay => $"{Name}{(IsOptional ? " (Optional)" : "")}{(IsRemainder ? " (Remainder)" : "")}";
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Discord.Commands
|
||||||
|
{
|
||||||
|
internal class CommandMap
|
||||||
|
{
|
||||||
|
private readonly CommandService _service;
|
||||||
|
private readonly CommandMapNode _root;
|
||||||
|
private static readonly string[] BlankAliases = { "" };
|
||||||
|
|
||||||
|
public CommandMap(CommandService service)
|
||||||
|
{
|
||||||
|
_service = service;
|
||||||
|
_root = new CommandMapNode("");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddCommand(CommandInfo command)
|
||||||
|
{
|
||||||
|
foreach (string text in command.Aliases)
|
||||||
|
_root.AddCommand(_service, text, 0, command);
|
||||||
|
}
|
||||||
|
public void RemoveCommand(CommandInfo command)
|
||||||
|
{
|
||||||
|
foreach (string text in command.Aliases)
|
||||||
|
_root.RemoveCommand(_service, text, 0, command);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<CommandMatch> GetCommands(string text)
|
||||||
|
{
|
||||||
|
return _root.GetCommands(_service, text, 0, text != "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,135 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
|
||||||
|
namespace Discord.Commands
|
||||||
|
{
|
||||||
|
internal class CommandMapNode
|
||||||
|
{
|
||||||
|
private static readonly char[] WhitespaceChars = { ' ', '\r', '\n' };
|
||||||
|
|
||||||
|
private readonly ConcurrentDictionary<string, CommandMapNode> _nodes;
|
||||||
|
private readonly string _name;
|
||||||
|
private readonly object _lockObj = new object();
|
||||||
|
private ImmutableArray<CommandInfo> _commands;
|
||||||
|
|
||||||
|
public bool IsEmpty => _commands.Length == 0 && _nodes.Count == 0;
|
||||||
|
|
||||||
|
public CommandMapNode(string name)
|
||||||
|
{
|
||||||
|
_name = name;
|
||||||
|
_nodes = new ConcurrentDictionary<string, CommandMapNode>();
|
||||||
|
_commands = ImmutableArray.Create<CommandInfo>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <exception cref="InvalidOperationException">Cannot add commands to the root node.</exception>
|
||||||
|
public void AddCommand(CommandService service, string text, int index, CommandInfo command)
|
||||||
|
{
|
||||||
|
int nextSegment = NextSegment(text, index, service._separatorChar);
|
||||||
|
string name;
|
||||||
|
|
||||||
|
lock (_lockObj)
|
||||||
|
{
|
||||||
|
if (text == "")
|
||||||
|
{
|
||||||
|
if (_name == "")
|
||||||
|
throw new InvalidOperationException("Cannot add commands to the root node.");
|
||||||
|
_commands = _commands.Add(command);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (nextSegment == -1)
|
||||||
|
name = text.Substring(index);
|
||||||
|
else
|
||||||
|
name = text.Substring(index, nextSegment - index);
|
||||||
|
|
||||||
|
string fullName = _name == "" ? name : _name + service._separatorChar + name;
|
||||||
|
var nextNode = _nodes.GetOrAdd(name, x => new CommandMapNode(fullName));
|
||||||
|
nextNode.AddCommand(service, nextSegment == -1 ? "" : text, nextSegment + 1, command);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public void RemoveCommand(CommandService service, string text, int index, CommandInfo command)
|
||||||
|
{
|
||||||
|
int nextSegment = NextSegment(text, index, service._separatorChar);
|
||||||
|
|
||||||
|
lock (_lockObj)
|
||||||
|
{
|
||||||
|
if (text == "")
|
||||||
|
_commands = _commands.Remove(command);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
string name;
|
||||||
|
if (nextSegment == -1)
|
||||||
|
name = text.Substring(index);
|
||||||
|
else
|
||||||
|
name = text.Substring(index, nextSegment - index);
|
||||||
|
|
||||||
|
if (_nodes.TryGetValue(name, out var nextNode))
|
||||||
|
{
|
||||||
|
nextNode.RemoveCommand(service, nextSegment == -1 ? "" : text, nextSegment + 1, command);
|
||||||
|
if (nextNode.IsEmpty)
|
||||||
|
_nodes.TryRemove(name, out nextNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<CommandMatch> GetCommands(CommandService service, string text, int index, bool visitChildren = true)
|
||||||
|
{
|
||||||
|
var commands = _commands;
|
||||||
|
for (int i = 0; i < commands.Length; i++)
|
||||||
|
yield return new CommandMatch(_commands[i], _name);
|
||||||
|
|
||||||
|
if (visitChildren)
|
||||||
|
{
|
||||||
|
string name;
|
||||||
|
CommandMapNode nextNode;
|
||||||
|
|
||||||
|
//Search for next segment
|
||||||
|
int nextSegment = NextSegment(text, index, service._separatorChar);
|
||||||
|
if (nextSegment == -1)
|
||||||
|
name = text.Substring(index);
|
||||||
|
else
|
||||||
|
name = text.Substring(index, nextSegment - index);
|
||||||
|
if (_nodes.TryGetValue(name, out nextNode))
|
||||||
|
{
|
||||||
|
foreach (var cmd in nextNode.GetCommands(service, nextSegment == -1 ? "" : text, nextSegment + 1, true))
|
||||||
|
yield return cmd;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Check if this is the last command segment before args
|
||||||
|
nextSegment = NextSegment(text, index, WhitespaceChars, service._separatorChar);
|
||||||
|
if (nextSegment != -1)
|
||||||
|
{
|
||||||
|
name = text.Substring(index, nextSegment - index);
|
||||||
|
if (_nodes.TryGetValue(name, out nextNode))
|
||||||
|
{
|
||||||
|
foreach (var cmd in nextNode.GetCommands(service, nextSegment == -1 ? "" : text, nextSegment + 1, false))
|
||||||
|
yield return cmd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int NextSegment(string text, int startIndex, char separator)
|
||||||
|
{
|
||||||
|
return text.IndexOf(separator, startIndex);
|
||||||
|
}
|
||||||
|
private static int NextSegment(string text, int startIndex, char[] separators, char except)
|
||||||
|
{
|
||||||
|
int lowest = int.MaxValue;
|
||||||
|
for (int i = 0; i < separators.Length; i++)
|
||||||
|
{
|
||||||
|
if (separators[i] != except)
|
||||||
|
{
|
||||||
|
int index = text.IndexOf(separators[i], startIndex);
|
||||||
|
if (index != -1 && index < lowest)
|
||||||
|
lowest = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (lowest != int.MaxValue) ? lowest : -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,72 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Discord.Commands.Builders;
|
||||||
|
|
||||||
|
namespace Discord.Commands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Provides a base class for a command module to inherit from.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class ModuleBase : ModuleBase<ICommandContext> { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides a base class for a command module to inherit from.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">A class that implements <see cref="ICommandContext"/>.</typeparam>
|
||||||
|
public abstract class ModuleBase<T> : IModuleBase
|
||||||
|
where T : class, ICommandContext
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The underlying context of the command.
|
||||||
|
/// </summary>
|
||||||
|
/// <seealso cref="T:Discord.Commands.ICommandContext" />
|
||||||
|
/// <seealso cref="T:Discord.Commands.CommandContext" />
|
||||||
|
public T Context { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sends a message to the source channel.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">
|
||||||
|
/// Contents of the message; optional only if <paramref name="embed" /> is specified.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="isTTS">Specifies if Discord should read this <paramref name="message"/> aloud using text-to-speech.</param>
|
||||||
|
/// <param name="embed">An embed to be displayed alongside the <paramref name="message"/>.</param>
|
||||||
|
protected virtual async Task<IUserMessage> ReplyAsync(string message = null, bool isTTS = false, Embed embed = null, RequestOptions options = null)
|
||||||
|
{
|
||||||
|
return await Context.Channel.SendMessageAsync(message, isTTS, embed, options).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// The method to execute before executing the command.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="command">The <see cref="CommandInfo"/> of the command to be executed.</param>
|
||||||
|
protected virtual void BeforeExecute(CommandInfo command)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// The method to execute after executing the command.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="command">The <see cref="CommandInfo"/> of the command to be executed.</param>
|
||||||
|
protected virtual void AfterExecute(CommandInfo command)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The method to execute when building the module.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="commandService">The <see cref="CommandService"/> used to create the module.</param>
|
||||||
|
/// <param name="builder">The builder used to build the module.</param>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
namespace Discord.Commands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Specifies the behavior when multiple matches are found during the command parsing stage.
|
||||||
|
/// </summary>
|
||||||
|
public enum MultiMatchHandling
|
||||||
|
{
|
||||||
|
/// <summary> Indicates that when multiple results are found, an exception should be thrown. </summary>
|
||||||
|
Exception,
|
||||||
|
/// <summary> Indicates that when multiple results are found, the best result should be chosen. </summary>
|
||||||
|
Best
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
|
||||||
|
namespace Discord.Commands
|
||||||
|
{
|
||||||
|
internal delegate bool TryParseDelegate<T>(string str, out T value);
|
||||||
|
|
||||||
|
internal static class PrimitiveParsers
|
||||||
|
{
|
||||||
|
private static readonly Lazy<IReadOnlyDictionary<Type, Delegate>> Parsers = new Lazy<IReadOnlyDictionary<Type, Delegate>>(CreateParsers);
|
||||||
|
|
||||||
|
public static IEnumerable<Type> SupportedTypes = Parsers.Value.Keys;
|
||||||
|
|
||||||
|
static IReadOnlyDictionary<Type, Delegate> CreateParsers()
|
||||||
|
{
|
||||||
|
var parserBuilder = ImmutableDictionary.CreateBuilder<Type, Delegate>();
|
||||||
|
parserBuilder[typeof(bool)] = (TryParseDelegate<bool>)bool.TryParse;
|
||||||
|
parserBuilder[typeof(sbyte)] = (TryParseDelegate<sbyte>)sbyte.TryParse;
|
||||||
|
parserBuilder[typeof(byte)] = (TryParseDelegate<byte>)byte.TryParse;
|
||||||
|
parserBuilder[typeof(short)] = (TryParseDelegate<short>)short.TryParse;
|
||||||
|
parserBuilder[typeof(ushort)] = (TryParseDelegate<ushort>)ushort.TryParse;
|
||||||
|
parserBuilder[typeof(int)] = (TryParseDelegate<int>)int.TryParse;
|
||||||
|
parserBuilder[typeof(uint)] = (TryParseDelegate<uint>)uint.TryParse;
|
||||||
|
parserBuilder[typeof(long)] = (TryParseDelegate<long>)long.TryParse;
|
||||||
|
parserBuilder[typeof(ulong)] = (TryParseDelegate<ulong>)ulong.TryParse;
|
||||||
|
parserBuilder[typeof(float)] = (TryParseDelegate<float>)float.TryParse;
|
||||||
|
parserBuilder[typeof(double)] = (TryParseDelegate<double>)double.TryParse;
|
||||||
|
parserBuilder[typeof(decimal)] = (TryParseDelegate<decimal>)decimal.TryParse;
|
||||||
|
parserBuilder[typeof(DateTime)] = (TryParseDelegate<DateTime>)DateTime.TryParse;
|
||||||
|
parserBuilder[typeof(DateTimeOffset)] = (TryParseDelegate<DateTimeOffset>)DateTimeOffset.TryParse;
|
||||||
|
//parserBuilder[typeof(TimeSpan)] = (TryParseDelegate<TimeSpan>)TimeSpan.TryParse;
|
||||||
|
parserBuilder[typeof(char)] = (TryParseDelegate<char>)char.TryParse;
|
||||||
|
return parserBuilder.ToImmutable();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TryParseDelegate<T> Get<T>() => (TryParseDelegate<T>)Parsers.Value[typeof(T)];
|
||||||
|
public static Delegate Get(Type type) => Parsers.Value[type];
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,55 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Discord.Commands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A <see cref="TypeReader"/> for parsing objects implementing <see cref="IChannel"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This <see cref="TypeReader"/> is shipped with Discord.Net and is used by default to parse any
|
||||||
|
/// <see cref="IChannel"/> implemented object within a command. The TypeReader will attempt to first parse the
|
||||||
|
/// input by mention, then the snowflake identifier, then by name; the highest candidate will be chosen as the
|
||||||
|
/// final output; otherwise, an erroneous <see cref="TypeReaderResult"/> is returned.
|
||||||
|
/// </remarks>
|
||||||
|
/// <typeparam name="T">The type to be checked; must implement <see cref="IChannel"/>.</typeparam>
|
||||||
|
public class ChannelTypeReader<T> : TypeReader
|
||||||
|
where T : class, IChannel
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override async Task<TypeReaderResult> ReadAsync(ICommandContext context, string input, IServiceProvider services)
|
||||||
|
{
|
||||||
|
if (context.Guild != null)
|
||||||
|
{
|
||||||
|
var results = new Dictionary<ulong, TypeReaderValue>();
|
||||||
|
var channels = await context.Guild.GetChannelsAsync(CacheMode.CacheOnly).ConfigureAwait(false);
|
||||||
|
|
||||||
|
//By Mention (1.0)
|
||||||
|
if (MentionUtils.TryParseChannel(input, out ulong id))
|
||||||
|
AddResult(results, await context.Guild.GetChannelAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T, 1.00f);
|
||||||
|
|
||||||
|
//By Id (0.9)
|
||||||
|
if (ulong.TryParse(input, NumberStyles.None, CultureInfo.InvariantCulture, out id))
|
||||||
|
AddResult(results, await context.Guild.GetChannelAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T, 0.90f);
|
||||||
|
|
||||||
|
//By Name (0.7-0.8)
|
||||||
|
foreach (var channel in channels.Where(x => string.Equals(input, x.Name, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
AddResult(results, channel as T, channel.Name == input ? 0.80f : 0.70f);
|
||||||
|
|
||||||
|
if (results.Count > 0)
|
||||||
|
return TypeReaderResult.FromSuccess(results.Values.ToReadOnlyCollection());
|
||||||
|
}
|
||||||
|
|
||||||
|
return TypeReaderResult.FromError(CommandError.ObjectNotFound, "Channel not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddResult(Dictionary<ulong, TypeReaderValue> results, T channel, float score)
|
||||||
|
{
|
||||||
|
if (channel != null && !results.ContainsKey(channel.Id))
|
||||||
|
results.Add(channel.Id, new TypeReaderValue(channel, score));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,68 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Discord.Commands
|
||||||
|
{
|
||||||
|
internal static class EnumTypeReader
|
||||||
|
{
|
||||||
|
public static TypeReader GetReader(Type type)
|
||||||
|
{
|
||||||
|
Type baseType = Enum.GetUnderlyingType(type);
|
||||||
|
var constructor = typeof(EnumTypeReader<>).MakeGenericType(baseType).GetTypeInfo().DeclaredConstructors.First();
|
||||||
|
return (TypeReader)constructor.Invoke(new object[] { type, PrimitiveParsers.Get(baseType) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class EnumTypeReader<T> : TypeReader
|
||||||
|
{
|
||||||
|
private readonly IReadOnlyDictionary<string, object> _enumsByName;
|
||||||
|
private readonly IReadOnlyDictionary<T, object> _enumsByValue;
|
||||||
|
private readonly Type _enumType;
|
||||||
|
private readonly TryParseDelegate<T> _tryParse;
|
||||||
|
|
||||||
|
public EnumTypeReader(Type type, TryParseDelegate<T> parser)
|
||||||
|
{
|
||||||
|
_enumType = type;
|
||||||
|
_tryParse = parser;
|
||||||
|
|
||||||
|
var byNameBuilder = ImmutableDictionary.CreateBuilder<string, object>();
|
||||||
|
var byValueBuilder = ImmutableDictionary.CreateBuilder<T, object>();
|
||||||
|
|
||||||
|
foreach (var v in Enum.GetNames(_enumType))
|
||||||
|
{
|
||||||
|
var parsedValue = Enum.Parse(_enumType, v);
|
||||||
|
byNameBuilder.Add(v.ToLower(), parsedValue);
|
||||||
|
if (!byValueBuilder.ContainsKey((T)parsedValue))
|
||||||
|
byValueBuilder.Add((T)parsedValue, parsedValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
_enumsByName = byNameBuilder.ToImmutable();
|
||||||
|
_enumsByValue = byValueBuilder.ToImmutable();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override Task<TypeReaderResult> ReadAsync(ICommandContext context, string input, IServiceProvider services)
|
||||||
|
{
|
||||||
|
object enumValue;
|
||||||
|
|
||||||
|
if (_tryParse(input, out T baseValue))
|
||||||
|
{
|
||||||
|
if (_enumsByValue.TryGetValue(baseValue, out enumValue))
|
||||||
|
return Task.FromResult(TypeReaderResult.FromSuccess(enumValue));
|
||||||
|
else
|
||||||
|
return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, $"Value is not a {_enumType.Name}."));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (_enumsByName.TryGetValue(input.ToLower(), out enumValue))
|
||||||
|
return Task.FromResult(TypeReaderResult.FromSuccess(enumValue));
|
||||||
|
else
|
||||||
|
return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, $"Value is not a {_enumType.Name}."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Discord.Commands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A <see cref="TypeReader"/> for parsing objects implementing <see cref="IMessage"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type to be checked; must implement <see cref="IMessage"/>.</typeparam>
|
||||||
|
public class MessageTypeReader<T> : TypeReader
|
||||||
|
where T : class, IMessage
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override async Task<TypeReaderResult> ReadAsync(ICommandContext context, string input, IServiceProvider services)
|
||||||
|
{
|
||||||
|
//By Id (1.0)
|
||||||
|
if (ulong.TryParse(input, NumberStyles.None, CultureInfo.InvariantCulture, out ulong id))
|
||||||
|
{
|
||||||
|
if (await context.Channel.GetMessageAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) is T msg)
|
||||||
|
return TypeReaderResult.FromSuccess(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
return TypeReaderResult.FromError(CommandError.ObjectNotFound, "Message not found.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,190 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Discord.Commands
|
||||||
|
{
|
||||||
|
internal sealed class NamedArgumentTypeReader<T> : TypeReader
|
||||||
|
where T : class, new()
|
||||||
|
{
|
||||||
|
private static readonly IReadOnlyDictionary<string, PropertyInfo> _tProps = typeof(T).GetTypeInfo().DeclaredProperties
|
||||||
|
.Where(p => p.SetMethod != null && p.SetMethod.IsPublic && !p.SetMethod.IsStatic)
|
||||||
|
.ToImmutableDictionary(p => p.Name, StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
private readonly CommandService _commands;
|
||||||
|
|
||||||
|
public NamedArgumentTypeReader(CommandService commands)
|
||||||
|
{
|
||||||
|
_commands = commands;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<TypeReaderResult> ReadAsync(ICommandContext context, string input, IServiceProvider services)
|
||||||
|
{
|
||||||
|
var result = new T();
|
||||||
|
var state = ReadState.LookingForParameter;
|
||||||
|
int beginRead = 0, currentRead = 0;
|
||||||
|
|
||||||
|
while (state != ReadState.End)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var prop = Read(out var arg);
|
||||||
|
var propVal = await ReadArgumentAsync(prop, arg).ConfigureAwait(false);
|
||||||
|
if (propVal != null)
|
||||||
|
prop.SetMethod.Invoke(result, new[] { propVal });
|
||||||
|
else
|
||||||
|
return TypeReaderResult.FromError(CommandError.ParseFailed, $"Could not parse the argument for the parameter '{prop.Name}' as type '{prop.PropertyType}'.");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return TypeReaderResult.FromError(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return TypeReaderResult.FromSuccess(result);
|
||||||
|
|
||||||
|
PropertyInfo Read(out string arg)
|
||||||
|
{
|
||||||
|
string currentParam = null;
|
||||||
|
char match = '\0';
|
||||||
|
|
||||||
|
for (; currentRead < input.Length; currentRead++)
|
||||||
|
{
|
||||||
|
var currentChar = input[currentRead];
|
||||||
|
switch (state)
|
||||||
|
{
|
||||||
|
case ReadState.LookingForParameter:
|
||||||
|
if (Char.IsWhiteSpace(currentChar))
|
||||||
|
continue;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
beginRead = currentRead;
|
||||||
|
state = ReadState.InParameter;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ReadState.InParameter:
|
||||||
|
if (currentChar != ':')
|
||||||
|
continue;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
currentParam = input.Substring(beginRead, currentRead - beginRead);
|
||||||
|
state = ReadState.LookingForArgument;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ReadState.LookingForArgument:
|
||||||
|
if (Char.IsWhiteSpace(currentChar))
|
||||||
|
continue;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
beginRead = currentRead;
|
||||||
|
state = (QuotationAliasUtils.GetDefaultAliasMap.TryGetValue(currentChar, out match))
|
||||||
|
? ReadState.InQuotedArgument
|
||||||
|
: ReadState.InArgument;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ReadState.InArgument:
|
||||||
|
if (!Char.IsWhiteSpace(currentChar))
|
||||||
|
continue;
|
||||||
|
else
|
||||||
|
return GetPropAndValue(out arg);
|
||||||
|
case ReadState.InQuotedArgument:
|
||||||
|
if (currentChar != match)
|
||||||
|
continue;
|
||||||
|
else
|
||||||
|
return GetPropAndValue(out arg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentParam == null)
|
||||||
|
throw new InvalidOperationException("No parameter name was read.");
|
||||||
|
|
||||||
|
return GetPropAndValue(out arg);
|
||||||
|
|
||||||
|
PropertyInfo GetPropAndValue(out string argv)
|
||||||
|
{
|
||||||
|
bool quoted = state == ReadState.InQuotedArgument;
|
||||||
|
state = (currentRead == (quoted ? input.Length - 1 : input.Length))
|
||||||
|
? ReadState.End
|
||||||
|
: ReadState.LookingForParameter;
|
||||||
|
|
||||||
|
if (quoted)
|
||||||
|
{
|
||||||
|
argv = input.Substring(beginRead + 1, currentRead - beginRead - 1).Trim();
|
||||||
|
currentRead++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
argv = input.Substring(beginRead, currentRead - beginRead);
|
||||||
|
|
||||||
|
return _tProps[currentParam];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task<object> ReadArgumentAsync(PropertyInfo prop, string arg)
|
||||||
|
{
|
||||||
|
var elemType = prop.PropertyType;
|
||||||
|
bool isCollection = false;
|
||||||
|
if (elemType.GetTypeInfo().IsGenericType && elemType.GetGenericTypeDefinition() == typeof(IEnumerable<>))
|
||||||
|
{
|
||||||
|
elemType = prop.PropertyType.GenericTypeArguments[0];
|
||||||
|
isCollection = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var overridden = prop.GetCustomAttribute<OverrideTypeReaderAttribute>();
|
||||||
|
var reader = (overridden != null)
|
||||||
|
? ModuleClassBuilder.GetTypeReader(_commands, elemType, overridden.TypeReader, services)
|
||||||
|
: (_commands.GetDefaultTypeReader(elemType)
|
||||||
|
?? _commands.GetTypeReaders(elemType).FirstOrDefault().Value);
|
||||||
|
|
||||||
|
if (reader != null)
|
||||||
|
{
|
||||||
|
if (isCollection)
|
||||||
|
{
|
||||||
|
var method = _readMultipleMethod.MakeGenericMethod(elemType);
|
||||||
|
var task = (Task<IEnumerable>)method.Invoke(null, new object[] { reader, context, arg.Split(','), services });
|
||||||
|
return await task.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return await ReadSingle(reader, context, arg, services).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<object> ReadSingle(TypeReader reader, ICommandContext context, string arg, IServiceProvider services)
|
||||||
|
{
|
||||||
|
var readResult = await reader.ReadAsync(context, arg, services).ConfigureAwait(false);
|
||||||
|
return (readResult.IsSuccess)
|
||||||
|
? readResult.BestMatch
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
private static async Task<IEnumerable> ReadMultiple<TObj>(TypeReader reader, ICommandContext context, IEnumerable<string> args, IServiceProvider services)
|
||||||
|
{
|
||||||
|
var objs = new List<TObj>();
|
||||||
|
foreach (var arg in args)
|
||||||
|
{
|
||||||
|
var read = await ReadSingle(reader, context, arg.Trim(), services).ConfigureAwait(false);
|
||||||
|
if (read != null)
|
||||||
|
objs.Add((TObj)read);
|
||||||
|
}
|
||||||
|
return objs.ToImmutableArray();
|
||||||
|
}
|
||||||
|
private static readonly MethodInfo _readMultipleMethod = typeof(NamedArgumentTypeReader<T>)
|
||||||
|
.GetTypeInfo()
|
||||||
|
.DeclaredMethods
|
||||||
|
.Single(m => m.IsPrivate && m.IsStatic && m.Name == nameof(ReadMultiple));
|
||||||
|
|
||||||
|
private enum ReadState
|
||||||
|
{
|
||||||
|
LookingForParameter,
|
||||||
|
InParameter,
|
||||||
|
LookingForArgument,
|
||||||
|
InArgument,
|
||||||
|
InQuotedArgument,
|
||||||
|
End
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Discord.Commands
|
||||||
|
{
|
||||||
|
internal static class NullableTypeReader
|
||||||
|
{
|
||||||
|
public static TypeReader Create(Type type, TypeReader reader)
|
||||||
|
{
|
||||||
|
var constructor = typeof(NullableTypeReader<>).MakeGenericType(type).GetTypeInfo().DeclaredConstructors.First();
|
||||||
|
return (TypeReader)constructor.Invoke(new object[] { reader });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class NullableTypeReader<T> : TypeReader
|
||||||
|
where T : struct
|
||||||
|
{
|
||||||
|
private readonly TypeReader _baseTypeReader;
|
||||||
|
|
||||||
|
public NullableTypeReader(TypeReader baseTypeReader)
|
||||||
|
{
|
||||||
|
_baseTypeReader = baseTypeReader;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override async Task<TypeReaderResult> ReadAsync(ICommandContext context, string input, IServiceProvider services)
|
||||||
|
{
|
||||||
|
if (string.Equals(input, "null", StringComparison.OrdinalIgnoreCase) || string.Equals(input, "nothing", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return TypeReaderResult.FromSuccess(new T?());
|
||||||
|
return await _baseTypeReader.ReadAsync(context, input, services).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Discord.Commands
|
||||||
|
{
|
||||||
|
internal static class PrimitiveTypeReader
|
||||||
|
{
|
||||||
|
public static TypeReader Create(Type type)
|
||||||
|
{
|
||||||
|
type = typeof(PrimitiveTypeReader<>).MakeGenericType(type);
|
||||||
|
return Activator.CreateInstance(type) as TypeReader;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class PrimitiveTypeReader<T> : TypeReader
|
||||||
|
{
|
||||||
|
private readonly TryParseDelegate<T> _tryParse;
|
||||||
|
private readonly float _score;
|
||||||
|
|
||||||
|
/// <exception cref="ArgumentOutOfRangeException"><typeparamref name="T"/> must be within the range [0, 1].</exception>
|
||||||
|
public PrimitiveTypeReader()
|
||||||
|
: this(PrimitiveParsers.Get<T>(), 1)
|
||||||
|
{ }
|
||||||
|
|
||||||
|
/// <exception cref="ArgumentOutOfRangeException"><paramref name="score"/> must be within the range [0, 1].</exception>
|
||||||
|
public PrimitiveTypeReader(TryParseDelegate<T> tryParse, float score)
|
||||||
|
{
|
||||||
|
if (score < 0 || score > 1)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(score), score, "Scores must be within the range [0, 1].");
|
||||||
|
|
||||||
|
_tryParse = tryParse;
|
||||||
|
_score = score;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task<TypeReaderResult> ReadAsync(ICommandContext context, string input, IServiceProvider services)
|
||||||
|
{
|
||||||
|
if (_tryParse(input, out T value))
|
||||||
|
return Task.FromResult(TypeReaderResult.FromSuccess(new TypeReaderValue(value, _score)));
|
||||||
|
return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, $"Failed to parse {typeof(T).Name}."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,48 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Discord.Commands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A <see cref="TypeReader"/> for parsing objects implementing <see cref="IRole"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type to be checked; must implement <see cref="IRole"/>.</typeparam>
|
||||||
|
public class RoleTypeReader<T> : TypeReader
|
||||||
|
where T : class, IRole
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override Task<TypeReaderResult> ReadAsync(ICommandContext context, string input, IServiceProvider services)
|
||||||
|
{
|
||||||
|
if (context.Guild != null)
|
||||||
|
{
|
||||||
|
var results = new Dictionary<ulong, TypeReaderValue>();
|
||||||
|
var roles = context.Guild.Roles;
|
||||||
|
|
||||||
|
//By Mention (1.0)
|
||||||
|
if (MentionUtils.TryParseRole(input, out var id))
|
||||||
|
AddResult(results, context.Guild.GetRole(id) as T, 1.00f);
|
||||||
|
|
||||||
|
//By Id (0.9)
|
||||||
|
if (ulong.TryParse(input, NumberStyles.None, CultureInfo.InvariantCulture, out id))
|
||||||
|
AddResult(results, context.Guild.GetRole(id) as T, 0.90f);
|
||||||
|
|
||||||
|
//By Name (0.7-0.8)
|
||||||
|
foreach (var role in roles.Where(x => string.Equals(input, x.Name, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
AddResult(results, role as T, role.Name == input ? 0.80f : 0.70f);
|
||||||
|
|
||||||
|
if (results.Count > 0)
|
||||||
|
return Task.FromResult(TypeReaderResult.FromSuccess(results.Values.ToReadOnlyCollection()));
|
||||||
|
}
|
||||||
|
return Task.FromResult(TypeReaderResult.FromError(CommandError.ObjectNotFound, "Role not found."));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddResult(Dictionary<ulong, TypeReaderValue> results, T role, float score)
|
||||||
|
{
|
||||||
|
if (role != null && !results.ContainsKey(role.Id))
|
||||||
|
results.Add(role.Id, new TypeReaderValue(role, score));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Discord.Commands
|
||||||
|
{
|
||||||
|
internal class TimeSpanTypeReader : TypeReader
|
||||||
|
{
|
||||||
|
private static readonly string[] Formats = {
|
||||||
|
"%d'd'%h'h'%m'm'%s's'", //4d3h2m1s
|
||||||
|
"%d'd'%h'h'%m'm'", //4d3h2m
|
||||||
|
"%d'd'%h'h'%s's'", //4d3h 1s
|
||||||
|
"%d'd'%h'h'", //4d3h
|
||||||
|
"%d'd'%m'm'%s's'", //4d 2m1s
|
||||||
|
"%d'd'%m'm'", //4d 2m
|
||||||
|
"%d'd'%s's'", //4d 1s
|
||||||
|
"%d'd'", //4d
|
||||||
|
"%h'h'%m'm'%s's'", // 3h2m1s
|
||||||
|
"%h'h'%m'm'", // 3h2m
|
||||||
|
"%h'h'%s's'", // 3h 1s
|
||||||
|
"%h'h'", // 3h
|
||||||
|
"%m'm'%s's'", // 2m1s
|
||||||
|
"%m'm'", // 2m
|
||||||
|
"%s's'", // 1s
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override Task<TypeReaderResult> ReadAsync(ICommandContext context, string input, IServiceProvider services)
|
||||||
|
{
|
||||||
|
return (TimeSpan.TryParseExact(input.ToLowerInvariant(), Formats, CultureInfo.InvariantCulture, out var timeSpan))
|
||||||
|
? Task.FromResult(TypeReaderResult.FromSuccess(timeSpan))
|
||||||
|
: Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Failed to parse TimeSpan"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Discord.Commands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Defines a reader class that parses user input into a specified type.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class TypeReader
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Attempts to parse the <paramref name="input"/> into the desired type.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">The context of the command.</param>
|
||||||
|
/// <param name="input">The raw input of the command.</param>
|
||||||
|
/// <param name="services">The service collection used for dependency injection.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// A task that represents the asynchronous parsing operation. The task result contains the parsing result.
|
||||||
|
/// </returns>
|
||||||
|
public abstract Task<TypeReaderResult> ReadAsync(ICommandContext context, string input, IServiceProvider services);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,95 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Discord.Commands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A <see cref="TypeReader"/> for parsing objects implementing <see cref="IUser"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type to be checked; must implement <see cref="IUser"/>.</typeparam>
|
||||||
|
public class UserTypeReader<T> : TypeReader
|
||||||
|
where T : class, IUser
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override async Task<TypeReaderResult> ReadAsync(ICommandContext context, string input, IServiceProvider services)
|
||||||
|
{
|
||||||
|
var results = new Dictionary<ulong, TypeReaderValue>();
|
||||||
|
IAsyncEnumerable<IUser> channelUsers = context.Channel.GetUsersAsync(CacheMode.CacheOnly).Flatten(); // it's better
|
||||||
|
IReadOnlyCollection<IGuildUser> guildUsers = ImmutableArray.Create<IGuildUser>();
|
||||||
|
|
||||||
|
if (context.Guild != null)
|
||||||
|
guildUsers = await context.Guild.GetUsersAsync(CacheMode.CacheOnly).ConfigureAwait(false);
|
||||||
|
|
||||||
|
//By Mention (1.0)
|
||||||
|
if (MentionUtils.TryParseUser(input, out var id))
|
||||||
|
{
|
||||||
|
if (context.Guild != null)
|
||||||
|
AddResult(results, await context.Guild.GetUserAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T, 1.00f);
|
||||||
|
else
|
||||||
|
AddResult(results, await context.Channel.GetUserAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T, 1.00f);
|
||||||
|
}
|
||||||
|
|
||||||
|
//By Id (0.9)
|
||||||
|
if (ulong.TryParse(input, NumberStyles.None, CultureInfo.InvariantCulture, out id))
|
||||||
|
{
|
||||||
|
if (context.Guild != null)
|
||||||
|
AddResult(results, await context.Guild.GetUserAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T, 0.90f);
|
||||||
|
else
|
||||||
|
AddResult(results, await context.Channel.GetUserAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T, 0.90f);
|
||||||
|
}
|
||||||
|
|
||||||
|
//By Username + Discriminator (0.7-0.85)
|
||||||
|
int index = input.LastIndexOf('#');
|
||||||
|
if (index >= 0)
|
||||||
|
{
|
||||||
|
string username = input.Substring(0, index);
|
||||||
|
if (ushort.TryParse(input.Substring(index + 1), out ushort discriminator))
|
||||||
|
{
|
||||||
|
var channelUser = await channelUsers.FirstOrDefaultAsync(x => x.DiscriminatorValue == discriminator &&
|
||||||
|
string.Equals(username, x.Username, StringComparison.OrdinalIgnoreCase)).ConfigureAwait(false);
|
||||||
|
AddResult(results, channelUser as T, channelUser?.Username == username ? 0.85f : 0.75f);
|
||||||
|
|
||||||
|
var guildUser = guildUsers.FirstOrDefault(x => x.DiscriminatorValue == discriminator &&
|
||||||
|
string.Equals(username, x.Username, StringComparison.OrdinalIgnoreCase));
|
||||||
|
AddResult(results, guildUser as T, guildUser?.Username == username ? 0.80f : 0.70f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//By Username (0.5-0.6)
|
||||||
|
{
|
||||||
|
await channelUsers
|
||||||
|
.Where(x => string.Equals(input, x.Username, StringComparison.OrdinalIgnoreCase))
|
||||||
|
.ForEachAsync(channelUser => AddResult(results, channelUser as T, channelUser.Username == input ? 0.65f : 0.55f))
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
foreach (var guildUser in guildUsers.Where(x => string.Equals(input, x.Username, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
AddResult(results, guildUser as T, guildUser.Username == input ? 0.60f : 0.50f);
|
||||||
|
}
|
||||||
|
|
||||||
|
//By Nickname (0.5-0.6)
|
||||||
|
{
|
||||||
|
await channelUsers
|
||||||
|
.Where(x => string.Equals(input, (x as IGuildUser)?.Nickname, StringComparison.OrdinalIgnoreCase))
|
||||||
|
.ForEachAsync(channelUser => AddResult(results, channelUser as T, (channelUser as IGuildUser).Nickname == input ? 0.65f : 0.55f))
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
foreach (var guildUser in guildUsers.Where(x => string.Equals(input, x.Nickname, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
AddResult(results, guildUser as T, guildUser.Nickname == input ? 0.60f : 0.50f);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (results.Count > 0)
|
||||||
|
return TypeReaderResult.FromSuccess(results.Values.ToImmutableArray());
|
||||||
|
return TypeReaderResult.FromError(CommandError.ObjectNotFound, "User not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddResult(Dictionary<ulong, TypeReaderValue> results, T user, float score)
|
||||||
|
{
|
||||||
|
if (user != null && !results.ContainsKey(user.Id))
|
||||||
|
results.Add(user.Id, new TypeReaderValue(user, score));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,85 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace Discord.Commands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Contains information of the command's overall execution result.
|
||||||
|
/// </summary>
|
||||||
|
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
|
||||||
|
public struct ExecuteResult : IResult
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the exception that may have occurred during the command execution.
|
||||||
|
/// </summary>
|
||||||
|
public Exception Exception { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public CommandError? Error { get; }
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string ErrorReason { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool IsSuccess => !Error.HasValue;
|
||||||
|
|
||||||
|
private ExecuteResult(Exception exception, CommandError? error, string errorReason)
|
||||||
|
{
|
||||||
|
Exception = exception;
|
||||||
|
Error = error;
|
||||||
|
ErrorReason = errorReason;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new <see cref="ExecuteResult" /> with no error, indicating a successful execution.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// A <see cref="ExecuteResult" /> that does not contain any errors.
|
||||||
|
/// </returns>
|
||||||
|
public static ExecuteResult FromSuccess()
|
||||||
|
=> new ExecuteResult(null, null, null);
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new <see cref="ExecuteResult" /> with a specified <see cref="CommandError" /> and its
|
||||||
|
/// reason, indicating an unsuccessful execution.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="error">The type of error.</param>
|
||||||
|
/// <param name="reason">The reason behind the error.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// A <see cref="ExecuteResult" /> that contains a <see cref="CommandError" /> and reason.
|
||||||
|
/// </returns>
|
||||||
|
public static ExecuteResult FromError(CommandError error, string reason)
|
||||||
|
=> new ExecuteResult(null, error, reason);
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new <see cref="ExecuteResult" /> with a specified exception, indicating an unsuccessful
|
||||||
|
/// execution.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ex">The exception that caused the command execution to fail.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// A <see cref="ExecuteResult" /> that contains the exception that caused the unsuccessful execution, along
|
||||||
|
/// with a <see cref="CommandError" /> of type <c>Exception</c> as well as the exception message as the
|
||||||
|
/// reason.
|
||||||
|
/// </returns>
|
||||||
|
public static ExecuteResult FromError(Exception ex)
|
||||||
|
=> new ExecuteResult(ex, CommandError.Exception, ex.Message);
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new <see cref="ExecuteResult" /> with a specified result; this may or may not be an
|
||||||
|
/// successful execution depending on the <see cref="Discord.Commands.IResult.Error" /> and
|
||||||
|
/// <see cref="Discord.Commands.IResult.ErrorReason" /> specified.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="result">The result to inherit from.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// A <see cref="ExecuteResult"/> that inherits the <see cref="IResult"/> error type and reason.
|
||||||
|
/// </returns>
|
||||||
|
public static ExecuteResult FromError(IResult result)
|
||||||
|
=> new ExecuteResult(null, result.Error, result.ErrorReason);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a string that indicates the execution result.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// <c>Success</c> if <see cref="IsSuccess"/> is <c>true</c>; otherwise "<see cref="Error"/>:
|
||||||
|
/// <see cref="ErrorReason"/>".
|
||||||
|
/// </returns>
|
||||||
|
public override string ToString() => IsSuccess ? "Success" : $"{Error}: {ErrorReason}";
|
||||||
|
private string DebuggerDisplay => IsSuccess ? "Success" : $"{Error}: {ErrorReason}";
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
namespace Discord.Commands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Contains information of the result related to a command.
|
||||||
|
/// </summary>
|
||||||
|
public interface IResult
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Describes the error type that may have occurred during the operation.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// A <see cref="CommandError" /> indicating the type of error that may have occurred during the operation;
|
||||||
|
/// <c>null</c> if the operation was successful.
|
||||||
|
/// </returns>
|
||||||
|
CommandError? Error { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Describes the reason for the error.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// A string containing the error reason.
|
||||||
|
/// </returns>
|
||||||
|
string ErrorReason { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates whether the operation was successful or not.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// <c>true</c> if the result is positive; otherwise <c>false</c>.
|
||||||
|
/// </returns>
|
||||||
|
bool IsSuccess { get; }
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,85 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace Discord.Commands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Contains information for the parsing result from the command service's parser.
|
||||||
|
/// </summary>
|
||||||
|
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
|
||||||
|
public struct ParseResult : IResult
|
||||||
|
{
|
||||||
|
public IReadOnlyList<TypeReaderResult> ArgValues { get; }
|
||||||
|
public IReadOnlyList<TypeReaderResult> ParamValues { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public CommandError? Error { get; }
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public string ErrorReason { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides information about the parameter that caused the parsing error.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// A <see cref="ParameterInfo" /> indicating the parameter info of the error that may have occurred during parsing;
|
||||||
|
/// <c>null</c> if the parsing was successful or the parsing error is not specific to a single parameter.
|
||||||
|
/// </returns>
|
||||||
|
public ParameterInfo ErrorParameter { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public bool IsSuccess => !Error.HasValue;
|
||||||
|
|
||||||
|
private ParseResult(IReadOnlyList<TypeReaderResult> argValues, IReadOnlyList<TypeReaderResult> paramValues, CommandError? error, string errorReason, ParameterInfo errorParamInfo)
|
||||||
|
{
|
||||||
|
ArgValues = argValues;
|
||||||
|
ParamValues = paramValues;
|
||||||
|
Error = error;
|
||||||
|
ErrorReason = errorReason;
|
||||||
|
ErrorParameter = errorParamInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ParseResult FromSuccess(IReadOnlyList<TypeReaderResult> argValues, IReadOnlyList<TypeReaderResult> paramValues)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < argValues.Count; i++)
|
||||||
|
{
|
||||||
|
if (argValues[i].Values.Count > 1)
|
||||||
|
return new ParseResult(argValues, paramValues, CommandError.MultipleMatches, "Multiple matches found.", null);
|
||||||
|
}
|
||||||
|
for (int i = 0; i < paramValues.Count; i++)
|
||||||
|
{
|
||||||
|
if (paramValues[i].Values.Count > 1)
|
||||||
|
return new ParseResult(argValues, paramValues, CommandError.MultipleMatches, "Multiple matches found.", null);
|
||||||
|
}
|
||||||
|
return new ParseResult(argValues, paramValues, null, null, null);
|
||||||
|
}
|
||||||
|
public static ParseResult FromSuccess(IReadOnlyList<TypeReaderValue> argValues, IReadOnlyList<TypeReaderValue> paramValues)
|
||||||
|
{
|
||||||
|
var argList = new TypeReaderResult[argValues.Count];
|
||||||
|
for (int i = 0; i < argValues.Count; i++)
|
||||||
|
argList[i] = TypeReaderResult.FromSuccess(argValues[i]);
|
||||||
|
TypeReaderResult[] paramList = null;
|
||||||
|
if (paramValues != null)
|
||||||
|
{
|
||||||
|
paramList = new TypeReaderResult[paramValues.Count];
|
||||||
|
for (int i = 0; i < paramValues.Count; i++)
|
||||||
|
paramList[i] = TypeReaderResult.FromSuccess(paramValues[i]);
|
||||||
|
}
|
||||||
|
return new ParseResult(argList, paramList, null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ParseResult FromError(CommandError error, string reason)
|
||||||
|
=> new ParseResult(null, null, error, reason, null);
|
||||||
|
public static ParseResult FromError(CommandError error, string reason, ParameterInfo parameterInfo)
|
||||||
|
=> new ParseResult(null, null, error, reason, parameterInfo);
|
||||||
|
public static ParseResult FromError(Exception ex)
|
||||||
|
=> FromError(CommandError.Exception, ex.Message);
|
||||||
|
public static ParseResult FromError(IResult result)
|
||||||
|
=> new ParseResult(null, null, result.Error, result.ErrorReason, null);
|
||||||
|
public static ParseResult FromError(IResult result, ParameterInfo parameterInfo)
|
||||||
|
=> new ParseResult(null, null, result.Error, result.ErrorReason, parameterInfo);
|
||||||
|
|
||||||
|
public override string ToString() => IsSuccess ? "Success" : $"{Error}: {ErrorReason}";
|
||||||
|
private string DebuggerDisplay => IsSuccess ? $"Success ({ArgValues.Count}{(ParamValues.Count > 0 ? $" +{ParamValues.Count} Values" : "")})" : $"{Error}: {ErrorReason}";
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace Discord.Commands
|
||||||
|
{
|
||||||
|
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
|
||||||
|
public class PreconditionGroupResult : PreconditionResult
|
||||||
|
{
|
||||||
|
public IReadOnlyCollection<PreconditionResult> PreconditionResults { get; }
|
||||||
|
|
||||||
|
protected PreconditionGroupResult(CommandError? error, string errorReason, ICollection<PreconditionResult> preconditions)
|
||||||
|
: base(error, errorReason)
|
||||||
|
{
|
||||||
|
PreconditionResults = (preconditions ?? new List<PreconditionResult>(0)).ToReadOnlyCollection();
|
||||||
|
}
|
||||||
|
|
||||||
|
public new static PreconditionGroupResult FromSuccess()
|
||||||
|
=> new PreconditionGroupResult(null, null, null);
|
||||||
|
public static PreconditionGroupResult FromError(string reason, ICollection<PreconditionResult> preconditions)
|
||||||
|
=> new PreconditionGroupResult(CommandError.UnmetPrecondition, reason, preconditions);
|
||||||
|
public static new PreconditionGroupResult FromError(Exception ex)
|
||||||
|
=> new PreconditionGroupResult(CommandError.Exception, ex.Message, null);
|
||||||
|
public static new PreconditionGroupResult FromError(IResult result) //needed?
|
||||||
|
=> new PreconditionGroupResult(result.Error, result.ErrorReason, null);
|
||||||
|
|
||||||
|
public override string ToString() => IsSuccess ? "Success" : $"{Error}: {ErrorReason}";
|
||||||
|
private string DebuggerDisplay => IsSuccess ? "Success" : $"{Error}: {ErrorReason}";
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,59 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace Discord.Commands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a result type for command preconditions.
|
||||||
|
/// </summary>
|
||||||
|
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
|
||||||
|
public class PreconditionResult : IResult
|
||||||
|
{
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public CommandError? Error { get; }
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public string ErrorReason { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public bool IsSuccess => !Error.HasValue;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new <see cref="PreconditionResult" /> class with the command <paramref name="error"/> type
|
||||||
|
/// and reason.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="error">The type of failure.</param>
|
||||||
|
/// <param name="errorReason">The reason of failure.</param>
|
||||||
|
protected PreconditionResult(CommandError? error, string errorReason)
|
||||||
|
{
|
||||||
|
Error = error;
|
||||||
|
ErrorReason = errorReason;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a <see cref="PreconditionResult" /> with no errors.
|
||||||
|
/// </summary>
|
||||||
|
public static PreconditionResult FromSuccess()
|
||||||
|
=> new PreconditionResult(null, null);
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a <see cref="PreconditionResult" /> with <see cref="CommandError.UnmetPrecondition" /> and the
|
||||||
|
/// specified reason.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="reason">The reason of failure.</param>
|
||||||
|
public static PreconditionResult FromError(string reason)
|
||||||
|
=> new PreconditionResult(CommandError.UnmetPrecondition, reason);
|
||||||
|
public static PreconditionResult FromError(Exception ex)
|
||||||
|
=> new PreconditionResult(CommandError.Exception, ex.Message);
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a <see cref="PreconditionResult" /> with the specified <paramref name="result"/> type.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="result">The result of failure.</param>
|
||||||
|
public static PreconditionResult FromError(IResult result)
|
||||||
|
=> new PreconditionResult(result.Error, result.ErrorReason);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a string indicating whether the <see cref="PreconditionResult"/> is successful.
|
||||||
|
/// </summary>
|
||||||
|
public override string ToString() => IsSuccess ? "Success" : $"{Error}: {ErrorReason}";
|
||||||
|
private string DebuggerDisplay => IsSuccess ? "Success" : $"{Error}: {ErrorReason}";
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace Discord.Commands
|
||||||
|
{
|
||||||
|
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
|
||||||
|
public abstract class RuntimeResult : IResult
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new <see cref="RuntimeResult" /> class with the type of error and reason.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="error">The type of failure, or <c>null</c> if none.</param>
|
||||||
|
/// <param name="reason">The reason of failure.</param>
|
||||||
|
protected RuntimeResult(CommandError? error, string reason)
|
||||||
|
{
|
||||||
|
Error = error;
|
||||||
|
Reason = reason;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public CommandError? Error { get; }
|
||||||
|
/// <summary> Describes the execution reason or result. </summary>
|
||||||
|
public string Reason { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public bool IsSuccess => !Error.HasValue;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
string IResult.ErrorReason => Reason;
|
||||||
|
|
||||||
|
public override string ToString() => Reason ?? (IsSuccess ? "Successful" : "Unsuccessful");
|
||||||
|
private string DebuggerDisplay => IsSuccess ? $"Success: {Reason ?? "No Reason"}" : $"{Error}: {Reason}";
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace Discord.Commands
|
||||||
|
{
|
||||||
|
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
|
||||||
|
public struct SearchResult : IResult
|
||||||
|
{
|
||||||
|
public string Text { get; }
|
||||||
|
public IReadOnlyList<CommandMatch> Commands { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public CommandError? Error { get; }
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public string ErrorReason { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public bool IsSuccess => !Error.HasValue;
|
||||||
|
|
||||||
|
private SearchResult(string text, IReadOnlyList<CommandMatch> commands, CommandError? error, string errorReason)
|
||||||
|
{
|
||||||
|
Text = text;
|
||||||
|
Commands = commands;
|
||||||
|
Error = error;
|
||||||
|
ErrorReason = errorReason;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SearchResult FromSuccess(string text, IReadOnlyList<CommandMatch> commands)
|
||||||
|
=> new SearchResult(text, commands, null, null);
|
||||||
|
public static SearchResult FromError(CommandError error, string reason)
|
||||||
|
=> new SearchResult(null, null, error, reason);
|
||||||
|
public static SearchResult FromError(Exception ex)
|
||||||
|
=> FromError(CommandError.Exception, ex.Message);
|
||||||
|
public static SearchResult FromError(IResult result)
|
||||||
|
=> new SearchResult(null, null, result.Error, result.ErrorReason);
|
||||||
|
|
||||||
|
public override string ToString() => IsSuccess ? "Success" : $"{Error}: {ErrorReason}";
|
||||||
|
private string DebuggerDisplay => IsSuccess ? $"Success ({Commands.Count} Results)" : $"{Error}: {ErrorReason}";
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,66 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Discord.Commands
|
||||||
|
{
|
||||||
|
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
|
||||||
|
public struct TypeReaderValue
|
||||||
|
{
|
||||||
|
public object Value { get; }
|
||||||
|
public float Score { get; }
|
||||||
|
|
||||||
|
public TypeReaderValue(object value, float score)
|
||||||
|
{
|
||||||
|
Value = value;
|
||||||
|
Score = score;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString() => Value?.ToString();
|
||||||
|
private string DebuggerDisplay => $"[{Value}, {Math.Round(Score, 2)}]";
|
||||||
|
}
|
||||||
|
|
||||||
|
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
|
||||||
|
public struct TypeReaderResult : IResult
|
||||||
|
{
|
||||||
|
public IReadOnlyCollection<TypeReaderValue> Values { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public CommandError? Error { get; }
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public string ErrorReason { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public bool IsSuccess => !Error.HasValue;
|
||||||
|
|
||||||
|
/// <exception cref="InvalidOperationException">TypeReaderResult was not successful.</exception>
|
||||||
|
public object BestMatch => IsSuccess
|
||||||
|
? (Values.Count == 1 ? Values.Single().Value : Values.OrderByDescending(v => v.Score).First().Value)
|
||||||
|
: throw new InvalidOperationException("TypeReaderResult was not successful.");
|
||||||
|
|
||||||
|
private TypeReaderResult(IReadOnlyCollection<TypeReaderValue> values, CommandError? error, string errorReason)
|
||||||
|
{
|
||||||
|
Values = values;
|
||||||
|
Error = error;
|
||||||
|
ErrorReason = errorReason;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TypeReaderResult FromSuccess(object value)
|
||||||
|
=> new TypeReaderResult(ImmutableArray.Create(new TypeReaderValue(value, 1.0f)), null, null);
|
||||||
|
public static TypeReaderResult FromSuccess(TypeReaderValue value)
|
||||||
|
=> new TypeReaderResult(ImmutableArray.Create(value), null, null);
|
||||||
|
public static TypeReaderResult FromSuccess(IReadOnlyCollection<TypeReaderValue> values)
|
||||||
|
=> new TypeReaderResult(values, null, null);
|
||||||
|
public static TypeReaderResult FromError(CommandError error, string reason)
|
||||||
|
=> new TypeReaderResult(null, error, reason);
|
||||||
|
public static TypeReaderResult FromError(Exception ex)
|
||||||
|
=> FromError(CommandError.Exception, ex.Message);
|
||||||
|
public static TypeReaderResult FromError(IResult result)
|
||||||
|
=> new TypeReaderResult(null, result.Error, result.ErrorReason);
|
||||||
|
|
||||||
|
public override string ToString() => IsSuccess ? "Success" : $"{Error}: {ErrorReason}";
|
||||||
|
private string DebuggerDisplay => IsSuccess ? $"Success ({string.Join(", ", Values)})" : $"{Error}: {ErrorReason}";
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
namespace Discord.Commands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Specifies the behavior of the command execution workflow.
|
||||||
|
/// </summary>
|
||||||
|
/// <seealso cref="CommandServiceConfig"/>
|
||||||
|
/// <seealso cref="CommandAttribute"/>
|
||||||
|
public enum RunMode
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The default behaviour set in <see cref="CommandServiceConfig"/>.
|
||||||
|
/// </summary>
|
||||||
|
Default,
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the command on the same thread as gateway one.
|
||||||
|
/// </summary>
|
||||||
|
Sync,
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the command on a different thread from the gateway one.
|
||||||
|
/// </summary>
|
||||||
|
Async
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,79 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace Discord.Commands
|
||||||
|
{
|
||||||
|
internal static class ReflectionUtils
|
||||||
|
{
|
||||||
|
private static readonly TypeInfo ObjectTypeInfo = typeof(object).GetTypeInfo();
|
||||||
|
|
||||||
|
internal static T CreateObject<T>(TypeInfo typeInfo, CommandService commands, IServiceProvider services = null)
|
||||||
|
=> CreateBuilder<T>(typeInfo, commands)(services);
|
||||||
|
internal static Func<IServiceProvider, T> CreateBuilder<T>(TypeInfo typeInfo, CommandService commands)
|
||||||
|
{
|
||||||
|
var constructor = GetConstructor(typeInfo);
|
||||||
|
var parameters = constructor.GetParameters();
|
||||||
|
var properties = GetProperties(typeInfo);
|
||||||
|
|
||||||
|
return (services) =>
|
||||||
|
{
|
||||||
|
var args = new object[parameters.Length];
|
||||||
|
for (int i = 0; i < parameters.Length; i++)
|
||||||
|
args[i] = GetMember(commands, services, parameters[i].ParameterType, typeInfo);
|
||||||
|
var obj = InvokeConstructor<T>(constructor, args, typeInfo);
|
||||||
|
|
||||||
|
foreach(var property in properties)
|
||||||
|
property.SetValue(obj, GetMember(commands, services, property.PropertyType, typeInfo));
|
||||||
|
return obj;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
private static T InvokeConstructor<T>(ConstructorInfo constructor, object[] args, TypeInfo ownerType)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return (T)constructor.Invoke(args);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw new Exception($"Failed to create \"{ownerType.FullName}\".", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ConstructorInfo GetConstructor(TypeInfo ownerType)
|
||||||
|
{
|
||||||
|
var constructors = ownerType.DeclaredConstructors.Where(x => !x.IsStatic).ToArray();
|
||||||
|
if (constructors.Length == 0)
|
||||||
|
throw new InvalidOperationException($"No constructor found for \"{ownerType.FullName}\".");
|
||||||
|
else if (constructors.Length > 1)
|
||||||
|
throw new InvalidOperationException($"Multiple constructors found for \"{ownerType.FullName}\".");
|
||||||
|
return constructors[0];
|
||||||
|
}
|
||||||
|
private static PropertyInfo[] GetProperties(TypeInfo ownerType)
|
||||||
|
{
|
||||||
|
var result = new List<System.Reflection.PropertyInfo>();
|
||||||
|
while (ownerType != ObjectTypeInfo)
|
||||||
|
{
|
||||||
|
foreach (var prop in ownerType.DeclaredProperties)
|
||||||
|
{
|
||||||
|
if (prop.SetMethod?.IsStatic == false && prop.SetMethod?.IsPublic == true && prop.GetCustomAttribute<DontInjectAttribute>() == null)
|
||||||
|
result.Add(prop);
|
||||||
|
}
|
||||||
|
ownerType = ownerType.BaseType.GetTypeInfo();
|
||||||
|
}
|
||||||
|
return result.ToArray();
|
||||||
|
}
|
||||||
|
private static object GetMember(CommandService commands, IServiceProvider services, Type memberType, TypeInfo ownerType)
|
||||||
|
{
|
||||||
|
if (memberType == typeof(CommandService))
|
||||||
|
return commands;
|
||||||
|
if (memberType == typeof(IServiceProvider) || memberType == services.GetType())
|
||||||
|
return services;
|
||||||
|
var service = services.GetService(memberType);
|
||||||
|
if (service != null)
|
||||||
|
return service;
|
||||||
|
throw new InvalidOperationException($"Failed to create \"{ownerType.FullName}\", dependency \"{memberType.Name}\" was not found.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
[assembly: InternalsVisibleTo("Discord.Net.Relay")]
|
||||||
|
[assembly: InternalsVisibleTo("Discord.Net.Rest")]
|
||||||
|
[assembly: InternalsVisibleTo("Discord.Net.Rpc")]
|
||||||
|
[assembly: InternalsVisibleTo("Discord.Net.WebSocket")]
|
||||||
|
[assembly: InternalsVisibleTo("Discord.Net.Webhook")]
|
||||||
|
[assembly: InternalsVisibleTo("Discord.Net.Commands")]
|
||||||
|
[assembly: InternalsVisibleTo("Discord.Net.Tests")]
|
||||||
|
[assembly: InternalsVisibleTo("Discord.Net.Tests.Unit")]
|
@ -0,0 +1,9 @@
|
|||||||
|
namespace Discord.Audio
|
||||||
|
{
|
||||||
|
public enum AudioApplication : int
|
||||||
|
{
|
||||||
|
Voice,
|
||||||
|
Music,
|
||||||
|
Mixed
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace Discord.Audio
|
||||||
|
{
|
||||||
|
public abstract class AudioOutStream : AudioStream
|
||||||
|
{
|
||||||
|
public override bool CanWrite => true;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
/// <exception cref="NotSupportedException">Reading this stream is not supported.</exception>
|
||||||
|
public override int Read(byte[] buffer, int offset, int count) =>
|
||||||
|
throw new NotSupportedException();
|
||||||
|
/// <inheritdoc />
|
||||||
|
/// <exception cref="NotSupportedException">Setting the length to this stream is not supported.</exception>
|
||||||
|
public override void SetLength(long value) =>
|
||||||
|
throw new NotSupportedException();
|
||||||
|
/// <inheritdoc />
|
||||||
|
/// <exception cref="NotSupportedException">Seeking this stream is not supported..</exception>
|
||||||
|
public override long Seek(long offset, SeekOrigin origin) =>
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,58 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Discord.Audio
|
||||||
|
{
|
||||||
|
public abstract class AudioStream : Stream
|
||||||
|
{
|
||||||
|
public override bool CanRead => false;
|
||||||
|
public override bool CanSeek => false;
|
||||||
|
public override bool CanWrite => false;
|
||||||
|
|
||||||
|
/// <exception cref="InvalidOperationException">This stream does not accept headers.</exception>
|
||||||
|
public virtual void WriteHeader(ushort seq, uint timestamp, bool missed) =>
|
||||||
|
throw new InvalidOperationException("This stream does not accept headers.");
|
||||||
|
public override void Write(byte[] buffer, int offset, int count)
|
||||||
|
{
|
||||||
|
WriteAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
public override void Flush()
|
||||||
|
{
|
||||||
|
FlushAsync(CancellationToken.None).GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
ClearAsync(CancellationToken.None).GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual Task ClearAsync(CancellationToken cancellationToken) { return Task.Delay(0); }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
/// <exception cref="NotSupportedException">Reading stream length is not supported.</exception>
|
||||||
|
public override long Length =>
|
||||||
|
throw new NotSupportedException();
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
/// <exception cref="NotSupportedException">Getting or setting this stream position is not supported.</exception>
|
||||||
|
public override long Position
|
||||||
|
{
|
||||||
|
get => throw new NotSupportedException();
|
||||||
|
set => throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
/// <exception cref="NotSupportedException">Reading this stream is not supported.</exception>
|
||||||
|
public override int Read(byte[] buffer, int offset, int count) =>
|
||||||
|
throw new NotSupportedException();
|
||||||
|
/// <inheritdoc />
|
||||||
|
/// <exception cref="NotSupportedException">Setting the length to this stream is not supported.</exception>
|
||||||
|
public override void SetLength(long value) =>
|
||||||
|
throw new NotSupportedException();
|
||||||
|
/// <inheritdoc />
|
||||||
|
/// <exception cref="NotSupportedException">Seeking this stream is not supported..</exception>
|
||||||
|
public override long Seek(long offset, SeekOrigin origin) =>
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,18 @@
|
|||||||
|
namespace Discord.Audio
|
||||||
|
{
|
||||||
|
public struct RTPFrame
|
||||||
|
{
|
||||||
|
public readonly ushort Sequence;
|
||||||
|
public readonly uint Timestamp;
|
||||||
|
public readonly byte[] Payload;
|
||||||
|
public readonly bool Missed;
|
||||||
|
|
||||||
|
public RTPFrame(ushort sequence, uint timestamp, byte[] payload, bool missed)
|
||||||
|
{
|
||||||
|
Sequence = sequence;
|
||||||
|
Timestamp = timestamp;
|
||||||
|
Payload = payload;
|
||||||
|
Missed = missed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,160 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Discord
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a class containing the strings related to various Content Delivery Networks (CDNs).
|
||||||
|
/// </summary>
|
||||||
|
public static class CDN
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Returns an application icon URL.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="appId">The application identifier.</param>
|
||||||
|
/// <param name="iconId">The icon identifier.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// A URL pointing to the application's icon.
|
||||||
|
/// </returns>
|
||||||
|
public static string GetApplicationIconUrl(ulong appId, string iconId)
|
||||||
|
=> iconId != null ? $"{DiscordConfig.CDNUrl}app-icons/{appId}/{iconId}.jpg" : null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a user avatar URL.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userId">The user snowflake identifier.</param>
|
||||||
|
/// <param name="avatarId">The avatar identifier.</param>
|
||||||
|
/// <param name="size">The size of the image to return in horizontal pixels. This can be any power of two between 16 and 2048.</param>
|
||||||
|
/// <param name="format">The format to return.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// A URL pointing to the user's avatar in the specified size.
|
||||||
|
/// </returns>
|
||||||
|
public static string GetUserAvatarUrl(ulong userId, string avatarId, ushort size, ImageFormat format)
|
||||||
|
{
|
||||||
|
if (avatarId == null)
|
||||||
|
return null;
|
||||||
|
string extension = FormatToExtension(format, avatarId);
|
||||||
|
return $"{DiscordConfig.CDNUrl}avatars/{userId}/{avatarId}.{extension}?size={size}";
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the default user avatar URL.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="discriminator">The discriminator value of a user.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// A URL pointing to the user's default avatar when one isn't set.
|
||||||
|
/// </returns>
|
||||||
|
public static string GetDefaultUserAvatarUrl(ushort discriminator)
|
||||||
|
{
|
||||||
|
return $"{DiscordConfig.CDNUrl}embed/avatars/{discriminator % 5}.png";
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Returns an icon URL.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="guildId">The guild snowflake identifier.</param>
|
||||||
|
/// <param name="iconId">The icon identifier.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// A URL pointing to the guild's icon.
|
||||||
|
/// </returns>
|
||||||
|
public static string GetGuildIconUrl(ulong guildId, string iconId)
|
||||||
|
=> iconId != null ? $"{DiscordConfig.CDNUrl}icons/{guildId}/{iconId}.jpg" : null;
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a guild splash URL.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="guildId">The guild snowflake identifier.</param>
|
||||||
|
/// <param name="splashId">The splash icon identifier.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// A URL pointing to the guild's icon.
|
||||||
|
/// </returns>
|
||||||
|
public static string GetGuildSplashUrl(ulong guildId, string splashId)
|
||||||
|
=> splashId != null ? $"{DiscordConfig.CDNUrl}splashes/{guildId}/{splashId}.jpg" : null;
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a channel icon URL.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="channelId">The channel snowflake identifier.</param>
|
||||||
|
/// <param name="iconId">The icon identifier.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// A URL pointing to the channel's icon.
|
||||||
|
/// </returns>
|
||||||
|
public static string GetChannelIconUrl(ulong channelId, string iconId)
|
||||||
|
=> iconId != null ? $"{DiscordConfig.CDNUrl}channel-icons/{channelId}/{iconId}.jpg" : null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a guild banner URL.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="guildId">The guild snowflake identifier.</param>
|
||||||
|
/// <param name="bannerId">The banner image identifier.</param>
|
||||||
|
/// <param name="size">The size of the image to return in horizontal pixels. This can be any power of two between 16 and 2048 inclusive.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// A URL pointing to the guild's banner image.
|
||||||
|
/// </returns>
|
||||||
|
public static string GetGuildBannerUrl(ulong guildId, string bannerId, ushort? size = null)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(bannerId))
|
||||||
|
return $"{DiscordConfig.CDNUrl}banners/{guildId}/{bannerId}.jpg" + (size.HasValue ? $"?size={size}" : string.Empty);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Returns an emoji URL.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="emojiId">The emoji snowflake identifier.</param>
|
||||||
|
/// <param name="animated">Whether this emoji is animated.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// A URL pointing to the custom emote.
|
||||||
|
/// </returns>
|
||||||
|
public static string GetEmojiUrl(ulong emojiId, bool animated)
|
||||||
|
=> $"{DiscordConfig.CDNUrl}emojis/{emojiId}.{(animated ? "gif" : "png")}";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a Rich Presence asset URL.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="appId">The application identifier.</param>
|
||||||
|
/// <param name="assetId">The asset identifier.</param>
|
||||||
|
/// <param name="size">The size of the image to return in. This can be any power of two between 16 and 2048.</param>
|
||||||
|
/// <param name="format">The format to return.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// A URL pointing to the asset image in the specified size.
|
||||||
|
/// </returns>
|
||||||
|
public static string GetRichAssetUrl(ulong appId, string assetId, ushort size, ImageFormat format)
|
||||||
|
{
|
||||||
|
string extension = FormatToExtension(format, "");
|
||||||
|
return $"{DiscordConfig.CDNUrl}app-assets/{appId}/{assetId}.{extension}?size={size}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a Spotify album URL.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="albumArtId">The identifier for the album art (e.g. 6be8f4c8614ecf4f1dd3ebba8d8692d8ce4951ac).</param>
|
||||||
|
/// <returns>
|
||||||
|
/// A URL pointing to the Spotify album art.
|
||||||
|
/// </returns>
|
||||||
|
public static string GetSpotifyAlbumArtUrl(string albumArtId)
|
||||||
|
=> $"https://i.scdn.co/image/{albumArtId}";
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a Spotify direct URL for a track.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="trackId">The identifier for the track (e.g. 4uLU6hMCjMI75M1A2tKUQC).</param>
|
||||||
|
/// <returns>
|
||||||
|
/// A URL pointing to the Spotify track.
|
||||||
|
/// </returns>
|
||||||
|
public static string GetSpotifyDirectUrl(string trackId)
|
||||||
|
=> $"https://open.spotify.com/track/{trackId}";
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
namespace Discord.Commands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a context of a command. This may include the client, guild, channel, user, and message.
|
||||||
|
/// </summary>
|
||||||
|
public interface ICommandContext
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the <see cref="IDiscordClient" /> that the command is executed with.
|
||||||
|
/// </summary>
|
||||||
|
IDiscordClient Client { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the <see cref="IGuild" /> that the command is executed in.
|
||||||
|
/// </summary>
|
||||||
|
IGuild Guild { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the <see cref="IMessageChannel" /> that the command is executed in.
|
||||||
|
/// </summary>
|
||||||
|
IMessageChannel Channel { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the <see cref="IUser" /> who executed the command.
|
||||||
|
/// </summary>
|
||||||
|
IUser User { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the <see cref="IUserMessage" /> that the command is interpreted from.
|
||||||
|
/// </summary>
|
||||||
|
IUserMessage Message { get; }
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
namespace Discord
|
||||||
|
{
|
||||||
|
/// <summary> Specifies the connection state of a client. </summary>
|
||||||
|
public enum ConnectionState : byte
|
||||||
|
{
|
||||||
|
/// <summary> The client has disconnected from Discord. </summary>
|
||||||
|
Disconnected,
|
||||||
|
/// <summary> The client is connecting to Discord. </summary>
|
||||||
|
Connecting,
|
||||||
|
/// <summary> The client has established a connection to Discord. </summary>
|
||||||
|
Connected,
|
||||||
|
/// <summary> The client is disconnecting from Discord. </summary>
|
||||||
|
Disconnecting
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,174 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace Discord
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Defines various behaviors of Discord.Net.
|
||||||
|
/// </summary>
|
||||||
|
public class DiscordConfig
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the API version Discord.Net uses.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// An <see cref="int"/> representing the API version that Discord.Net uses to communicate with Discord.
|
||||||
|
/// <para>A list of available API version can be seen on the official
|
||||||
|
/// <see href="https://discordapp.com/developers/docs/reference#api-versioning">Discord API documentation</see>
|
||||||
|
/// .</para>
|
||||||
|
/// </returns>
|
||||||
|
public const int APIVersion = 6;
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the Voice API version Discord.Net uses.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// An <see cref="int"/> representing the API version that Discord.Net uses to communicate with Discord's
|
||||||
|
/// voice server.
|
||||||
|
/// </returns>
|
||||||
|
public const int VoiceAPIVersion = 3;
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the Discord.Net version, including the build number.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// A string containing the detailed version information, including its build number; <c>Unknown</c> when
|
||||||
|
/// the version fails to be fetched.
|
||||||
|
/// </returns>
|
||||||
|
public static string Version { get; } =
|
||||||
|
typeof(DiscordConfig).GetTypeInfo().Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion ??
|
||||||
|
typeof(DiscordConfig).GetTypeInfo().Assembly.GetName().Version.ToString(3) ??
|
||||||
|
"Unknown";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the user agent that Discord.Net uses in its clients.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// The user agent used in each Discord.Net request.
|
||||||
|
/// </returns>
|
||||||
|
public static string UserAgent { get; } = $"DiscordBot (https://github.com/RogueException/Discord.Net, v{Version})";
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the base Discord API URL.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// The Discord API URL using <see cref="APIVersion"/>.
|
||||||
|
/// </returns>
|
||||||
|
public static readonly string APIUrl = $"https://discordapp.com/api/v{APIVersion}/";
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the base Discord CDN URL.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// The base Discord Content Delivery Network (CDN) URL.
|
||||||
|
/// </returns>
|
||||||
|
public const string CDNUrl = "https://cdn.discordapp.com/";
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the base Discord invite URL.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// The base Discord invite URL.
|
||||||
|
/// </returns>
|
||||||
|
public const string InviteUrl = "https://discord.gg/";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the default timeout for requests.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// The amount of time it takes in milliseconds before a request is timed out.
|
||||||
|
/// </returns>
|
||||||
|
public const int DefaultRequestTimeout = 15000;
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the max length for a Discord message.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// The maximum length of a message allowed by Discord.
|
||||||
|
/// </returns>
|
||||||
|
public const int MaxMessageSize = 2000;
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the max messages allowed to be in a request.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// The maximum number of messages that can be gotten per-batch.
|
||||||
|
/// </returns>
|
||||||
|
public const int MaxMessagesPerBatch = 100;
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the max users allowed to be in a request.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// The maximum number of users that can be gotten per-batch.
|
||||||
|
/// </returns>
|
||||||
|
public const int MaxUsersPerBatch = 1000;
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the max guilds allowed to be in a request.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// The maximum number of guilds that can be gotten per-batch.
|
||||||
|
/// </returns>
|
||||||
|
public const int MaxGuildsPerBatch = 100;
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the max user reactions allowed to be in a request.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// The maximum number of user reactions that can be gotten per-batch.
|
||||||
|
/// </returns>
|
||||||
|
public const int MaxUserReactionsPerBatch = 100;
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the max audit log entries allowed to be in a request.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// The maximum number of audit log entries that can be gotten per-batch.
|
||||||
|
/// </returns>
|
||||||
|
public const int MaxAuditLogEntriesPerBatch = 100;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets how a request should act in the case of an error, by default.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// The currently set <see cref="RetryMode"/>.
|
||||||
|
/// </returns>
|
||||||
|
public RetryMode DefaultRetryMode { get; set; } = RetryMode.AlwaysRetry;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the minimum log level severity that will be sent to the Log event.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// The currently set <see cref="LogSeverity"/> for logging level.
|
||||||
|
/// </returns>
|
||||||
|
public LogSeverity LogLevel { get; set; } = LogSeverity.Info;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets whether the initial log entry should be printed.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// If set to <c>true</c>, the library will attempt to print the current version of the library, as well as
|
||||||
|
/// the API version it uses on startup.
|
||||||
|
/// </remarks>
|
||||||
|
internal bool DisplayInitialLog { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the level of precision of the rate limit reset response.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// If set to <see cref="RateLimitPrecision.Second"/>, this value will be rounded up to the
|
||||||
|
/// nearest second.
|
||||||
|
/// </remarks>
|
||||||
|
/// <returns>
|
||||||
|
/// The currently set <see cref="RateLimitPrecision"/>.
|
||||||
|
/// </returns>
|
||||||
|
public RateLimitPrecision RateLimitPrecision { get; set; } = RateLimitPrecision.Millisecond;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets whether or not rate-limits should use the system clock.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// If set to <c>false</c>, we will use the X-RateLimit-Reset-After header
|
||||||
|
/// to determine when a rate-limit expires, rather than comparing the
|
||||||
|
/// X-RateLimit-Reset timestamp to the system time.
|
||||||
|
///
|
||||||
|
/// This should only be changed to false if the system is known to have
|
||||||
|
/// a clock that is out of sync. Relying on the Reset-After header will
|
||||||
|
/// incur network lag.
|
||||||
|
///
|
||||||
|
/// Regardless of this property, we still rely on the system's wall-clock
|
||||||
|
/// to determine if a bucket is rate-limited; we do not use any monotonic
|
||||||
|
/// clock. Your system will still need a stable clock.
|
||||||
|
/// </remarks>
|
||||||
|
public bool UseSystemClock { get; set; } = true;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Discord
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Flags for the <see cref="IActivity.Flags"/> property, that are ORd together.
|
||||||
|
/// These describe what the activity payload includes.
|
||||||
|
/// </summary>
|
||||||
|
[Flags]
|
||||||
|
public enum ActivityProperties
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates that no actions on this activity can be taken.
|
||||||
|
/// </summary>
|
||||||
|
None = 0,
|
||||||
|
Instance = 1,
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates that this activity can be joined.
|
||||||
|
/// </summary>
|
||||||
|
Join = 0b10,
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates that this activity can be spectated.
|
||||||
|
/// </summary>
|
||||||
|
Spectate = 0b100,
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates that a user may request to join an activity.
|
||||||
|
/// </summary>
|
||||||
|
JoinRequest = 0b1000,
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates that a user can listen along in Spotify.
|
||||||
|
/// </summary>
|
||||||
|
Sync = 0b10000,
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates that a user can play this song.
|
||||||
|
/// </summary>
|
||||||
|
Play = 0b100000
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
namespace Discord
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Specifies a Discord user's activity type.
|
||||||
|
/// </summary>
|
||||||
|
public enum ActivityType
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The user is playing a game.
|
||||||
|
/// </summary>
|
||||||
|
Playing = 0,
|
||||||
|
/// <summary>
|
||||||
|
/// The user is streaming online.
|
||||||
|
/// </summary>
|
||||||
|
Streaming = 1,
|
||||||
|
/// <summary>
|
||||||
|
/// The user is listening to a song.
|
||||||
|
/// </summary>
|
||||||
|
Listening = 2,
|
||||||
|
/// <summary>
|
||||||
|
/// The user is watching some form of media.
|
||||||
|
/// </summary>
|
||||||
|
Watching = 3,
|
||||||
|
/// <summary>
|
||||||
|
/// The user has set a custom status.
|
||||||
|
/// </summary>
|
||||||
|
CustomStatus = 4,
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace Discord
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A user's activity for their custom status.
|
||||||
|
/// </summary>
|
||||||
|
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
|
||||||
|
public class CustomStatusGame : Game
|
||||||
|
{
|
||||||
|
internal CustomStatusGame() { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the emote, if it is set.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// An <see cref="IEmote"/> containing the <see cref="Emoji"/> or <see cref="GuildEmote"/> set by the user.
|
||||||
|
/// </returns>
|
||||||
|
public IEmote Emote { get; internal set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the timestamp of when this status was created.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// A <see cref="DateTimeOffset"/> containing the time when this status was created.
|
||||||
|
/// </returns>
|
||||||
|
public DateTimeOffset CreatedAt { get; internal set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the state of the status.
|
||||||
|
/// </summary>
|
||||||
|
public string State { get; internal set; }
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
=> $"{Emote} {State}";
|
||||||
|
|
||||||
|
private string DebuggerDisplay => $"{Name}";
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace Discord
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A user's game status.
|
||||||
|
/// </summary>
|
||||||
|
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
|
||||||
|
public class Game : IActivity
|
||||||
|
{
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public string Name { get; internal set; }
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public ActivityType Type { get; internal set; }
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public ActivityProperties Flags { get; internal set; }
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public string Details { get; internal set; }
|
||||||
|
|
||||||
|
internal Game() { }
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a <see cref="Game"/> with the provided name and <see cref="ActivityType"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">The name of the game.</param>
|
||||||
|
/// <param name="type">The type of activity.</param>
|
||||||
|
public Game(string name, ActivityType type = ActivityType.Playing, ActivityProperties flags = ActivityProperties.None, string details = null)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
Type = type;
|
||||||
|
Flags = flags;
|
||||||
|
Details = details;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary> Returns the name of the <see cref="Game"/>. </summary>
|
||||||
|
public override string ToString() => Name;
|
||||||
|
private string DebuggerDisplay => Name;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
namespace Discord
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// An asset for a <see cref="RichGame" /> object containing the text and image.
|
||||||
|
/// </summary>
|
||||||
|
public class GameAsset
|
||||||
|
{
|
||||||
|
internal GameAsset() { }
|
||||||
|
|
||||||
|
internal ulong? ApplicationId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the description of the asset.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// A string containing the description of the asset.
|
||||||
|
/// </returns>
|
||||||
|
public string Text { get; internal set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the image ID of the asset.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// A string containing the unique image identifier of the asset.
|
||||||
|
/// </returns>
|
||||||
|
public string ImageId { get; internal set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the image URL of the asset.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="size">The size of the image to return in. This can be any power of two between 16 and 2048.</param>
|
||||||
|
/// <param name="format">The format to return.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// A string pointing to the image URL of the asset; <c>null</c> when the application ID does not exist.
|
||||||
|
/// </returns>
|
||||||
|
public string GetImageUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128)
|
||||||
|
=> ApplicationId.HasValue ? CDN.GetRichAssetUrl(ApplicationId.Value, ImageId, size, format) : null;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
namespace Discord
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Party information for a <see cref="RichGame" /> object.
|
||||||
|
/// </summary>
|
||||||
|
public class GameParty
|
||||||
|
{
|
||||||
|
internal GameParty() { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the ID of the party.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// A string containing the unique identifier of the party.
|
||||||
|
/// </returns>
|
||||||
|
public string Id { get; internal set; }
|
||||||
|
public long Members { get; internal set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the party's current and maximum size.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// A <see cref="long"/> representing the capacity of the party.
|
||||||
|
/// </returns>
|
||||||
|
public long Capacity { get; internal set; }
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
namespace Discord
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Party secret for a <see cref="RichGame" /> object.
|
||||||
|
/// </summary>
|
||||||
|
public class GameSecrets
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the secret for a specific instanced match.
|
||||||
|
/// </summary>
|
||||||
|
public string Match { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the secret for joining a party.
|
||||||
|
/// </summary>
|
||||||
|
public string Join { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the secret for spectating a game.
|
||||||
|
/// </summary>
|
||||||
|
public string Spectate { get; }
|
||||||
|
|
||||||
|
internal GameSecrets(string match, string join, string spectate)
|
||||||
|
{
|
||||||
|
Match = match;
|
||||||
|
Join = join;
|
||||||
|
Spectate = spectate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Discord
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Timestamps for a <see cref="RichGame" /> object.
|
||||||
|
/// </summary>
|
||||||
|
public class GameTimestamps
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets when the activity started.
|
||||||
|
/// </summary>
|
||||||
|
public DateTimeOffset? Start { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets when the activity ends.
|
||||||
|
/// </summary>
|
||||||
|
public DateTimeOffset? End { get; }
|
||||||
|
|
||||||
|
internal GameTimestamps(DateTimeOffset? start, DateTimeOffset? end)
|
||||||
|
{
|
||||||
|
Start = start;
|
||||||
|
End = end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
namespace Discord
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A user's activity status, typically a <see cref="Game"/>.
|
||||||
|
/// </summary>
|
||||||
|
public interface IActivity
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the name of the activity.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// A string containing the name of the activity that the user is doing.
|
||||||
|
/// </returns>
|
||||||
|
string Name { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the type of the activity.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// The type of activity.
|
||||||
|
/// </returns>
|
||||||
|
ActivityType Type { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the flags that are relevant to this activity.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This value is determined by bitwise OR-ing <see cref="ActivityProperties"/> values together.
|
||||||
|
/// </remarks>
|
||||||
|
/// <returns>
|
||||||
|
/// The value of flags for this activity.
|
||||||
|
/// </returns>
|
||||||
|
ActivityProperties Flags { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the details on what the player is currently doing.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// A string describing what the player is doing.
|
||||||
|
/// </returns>
|
||||||
|
string Details { get; }
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,48 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace Discord
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A user's Rich Presence status.
|
||||||
|
/// </summary>
|
||||||
|
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
|
||||||
|
public class RichGame : Game
|
||||||
|
{
|
||||||
|
internal RichGame() { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the user's current party status.
|
||||||
|
/// </summary>
|
||||||
|
public string State { get; internal set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the application ID for the game.
|
||||||
|
/// </summary>
|
||||||
|
public ulong ApplicationId { get; internal set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the small image for the presence and their hover texts.
|
||||||
|
/// </summary>
|
||||||
|
public GameAsset SmallAsset { get; internal set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the large image for the presence and their hover texts.
|
||||||
|
/// </summary>
|
||||||
|
public GameAsset LargeAsset { get; internal set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the information for the current party of the player.
|
||||||
|
/// </summary>
|
||||||
|
public GameParty Party { get; internal set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the secrets for Rich Presence joining and spectating.
|
||||||
|
/// </summary>
|
||||||
|
public GameSecrets Secrets { get; internal set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the timestamps for start and/or end of the game.
|
||||||
|
/// </summary>
|
||||||
|
public GameTimestamps Timestamps { get; internal set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the name of the Rich Presence.
|
||||||
|
/// </summary>
|
||||||
|
public override string ToString() => Name;
|
||||||
|
private string DebuggerDisplay => $"{Name} (Rich)";
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,122 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace Discord
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A user's activity for listening to a song on Spotify.
|
||||||
|
/// </summary>
|
||||||
|
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
|
||||||
|
public class SpotifyGame : Game
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the song's artist(s).
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// A collection of string containing all artists featured in the track (e.g. <c>Avicii</c>; <c>Rita Ora</c>).
|
||||||
|
/// </returns>
|
||||||
|
public IReadOnlyCollection<string> Artists { get; internal set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the Spotify album title of the song.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// A string containing the name of the album (e.g. <c>AVĪCI (01)</c>).
|
||||||
|
/// </returns>
|
||||||
|
public string AlbumTitle { get; internal set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the track title of the song.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// A string containing the name of the song (e.g. <c>Lonely Together (feat. Rita Ora)</c>).
|
||||||
|
/// </returns>
|
||||||
|
public string TrackTitle { get; internal set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the date when the track started playing.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// A <see cref="DateTimeOffset"/> containing the start timestamp of the song.
|
||||||
|
/// </returns>
|
||||||
|
public DateTimeOffset? StartedAt { get; internal set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the date when the track ends.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// A <see cref="DateTimeOffset"/> containing the finish timestamp of the song.
|
||||||
|
/// </returns>
|
||||||
|
public DateTimeOffset? EndsAt { get; internal set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the duration of the song.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// A <see cref="TimeSpan"/> containing the duration of the song.
|
||||||
|
/// </returns>
|
||||||
|
public TimeSpan? Duration { get; internal set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the elapsed duration of the song.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// A <see cref="TimeSpan"/> containing the elapsed duration of the song.
|
||||||
|
/// </returns>
|
||||||
|
public TimeSpan? Elapsed => DateTimeOffset.UtcNow - StartedAt;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the remaining duration of the song.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// A <see cref="TimeSpan"/> containing the remaining duration of the song.
|
||||||
|
/// </returns>
|
||||||
|
public TimeSpan? Remaining => EndsAt - DateTimeOffset.UtcNow;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the track ID of the song.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// A string containing the Spotify ID of the track (e.g. <c>7DoN0sCGIT9IcLrtBDm4f0</c>).
|
||||||
|
/// </returns>
|
||||||
|
public string TrackId { get; internal set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the session ID of the song.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The purpose of this property is currently unknown.
|
||||||
|
/// </remarks>
|
||||||
|
/// <returns>
|
||||||
|
/// A string containing the session ID.
|
||||||
|
/// </returns>
|
||||||
|
public string SessionId { get; internal set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the URL of the album art.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// A URL pointing to the album art of the track (e.g.
|
||||||
|
/// <c>https://i.scdn.co/image/ba2fd8823d42802c2f8738db0b33a4597f2f39e7</c>).
|
||||||
|
/// </returns>
|
||||||
|
public string AlbumArtUrl { get; internal set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the direct Spotify URL of the track.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// A URL pointing directly to the track on Spotify. (e.g.
|
||||||
|
/// <c>https://open.spotify.com/track/7DoN0sCGIT9IcLrtBDm4f0</c>).
|
||||||
|
/// </returns>
|
||||||
|
public string TrackUrl { get; internal set; }
|
||||||
|
|
||||||
|
internal SpotifyGame() { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the full information of the song.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// A string containing the full information of the song (e.g.
|
||||||
|
/// <c>Avicii, Rita Ora - Lonely Together (feat. Rita Ora) (3:08)</c>
|
||||||
|
/// </returns>
|
||||||
|
public override string ToString() => $"{string.Join(", ", Artists)} - {TrackTitle} ({Duration})";
|
||||||
|
private string DebuggerDisplay => $"{Name} (Spotify)";
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace Discord
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A user's activity for streaming on services such as Twitch.
|
||||||
|
/// </summary>
|
||||||
|
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
|
||||||
|
public class StreamingGame : Game
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the URL of the stream.
|
||||||
|
/// </summary>
|
||||||
|
public string Url { get; internal set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new <see cref="StreamingGame" /> based on the <paramref name="name"/> on the stream URL.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">The name of the stream.</param>
|
||||||
|
/// <param name="url">The URL of the stream.</param>
|
||||||
|
public StreamingGame(string name, string url)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
Url = url;
|
||||||
|
Type = ActivityType.Streaming;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the name of the stream.
|
||||||
|
/// </summary>
|
||||||
|
public override string ToString() => Name;
|
||||||
|
private string DebuggerDisplay => $"{Name} ({Url})";
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,122 @@
|
|||||||
|
namespace Discord
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Representing a type of action within an <see cref="IAuditLogEntry"/>.
|
||||||
|
/// </summary>
|
||||||
|
public enum ActionType
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// this guild was updated.
|
||||||
|
/// </summary>
|
||||||
|
GuildUpdated = 1,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A channel was created.
|
||||||
|
/// </summary>
|
||||||
|
ChannelCreated = 10,
|
||||||
|
/// <summary>
|
||||||
|
/// A channel was updated.
|
||||||
|
/// </summary>
|
||||||
|
ChannelUpdated = 11,
|
||||||
|
/// <summary>
|
||||||
|
/// A channel was deleted.
|
||||||
|
/// </summary>
|
||||||
|
ChannelDeleted = 12,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A permission overwrite was created for a channel.
|
||||||
|
/// </summary>
|
||||||
|
OverwriteCreated = 13,
|
||||||
|
/// <summary>
|
||||||
|
/// A permission overwrite was updated for a channel.
|
||||||
|
/// </summary>
|
||||||
|
OverwriteUpdated = 14,
|
||||||
|
/// <summary>
|
||||||
|
/// A permission overwrite was deleted for a channel.
|
||||||
|
/// </summary>
|
||||||
|
OverwriteDeleted = 15,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A user was kicked from this guild.
|
||||||
|
/// </summary>
|
||||||
|
Kick = 20,
|
||||||
|
/// <summary>
|
||||||
|
/// A prune took place in this guild.
|
||||||
|
/// </summary>
|
||||||
|
Prune = 21,
|
||||||
|
/// <summary>
|
||||||
|
/// A user banned another user from this guild.
|
||||||
|
/// </summary>
|
||||||
|
Ban = 22,
|
||||||
|
/// <summary>
|
||||||
|
/// A user unbanned another user from this guild.
|
||||||
|
/// </summary>
|
||||||
|
Unban = 23,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A guild member whose information was updated.
|
||||||
|
/// </summary>
|
||||||
|
MemberUpdated = 24,
|
||||||
|
/// <summary>
|
||||||
|
/// A guild member's role collection was updated.
|
||||||
|
/// </summary>
|
||||||
|
MemberRoleUpdated = 25,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A role was created in this guild.
|
||||||
|
/// </summary>
|
||||||
|
RoleCreated = 30,
|
||||||
|
/// <summary>
|
||||||
|
/// A role was updated in this guild.
|
||||||
|
/// </summary>
|
||||||
|
RoleUpdated = 31,
|
||||||
|
/// <summary>
|
||||||
|
/// A role was deleted from this guild.
|
||||||
|
/// </summary>
|
||||||
|
RoleDeleted = 32,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An invite was created in this guild.
|
||||||
|
/// </summary>
|
||||||
|
InviteCreated = 40,
|
||||||
|
/// <summary>
|
||||||
|
/// An invite was updated in this guild.
|
||||||
|
/// </summary>
|
||||||
|
InviteUpdated = 41,
|
||||||
|
/// <summary>
|
||||||
|
/// An invite was deleted from this guild.
|
||||||
|
/// </summary>
|
||||||
|
InviteDeleted = 42,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A Webhook was created in this guild.
|
||||||
|
/// </summary>
|
||||||
|
WebhookCreated = 50,
|
||||||
|
/// <summary>
|
||||||
|
/// A Webhook was updated in this guild.
|
||||||
|
/// </summary>
|
||||||
|
WebhookUpdated = 51,
|
||||||
|
/// <summary>
|
||||||
|
/// A Webhook was deleted from this guild.
|
||||||
|
/// </summary>
|
||||||
|
WebhookDeleted = 52,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An emoji was created in this guild.
|
||||||
|
/// </summary>
|
||||||
|
EmojiCreated = 60,
|
||||||
|
/// <summary>
|
||||||
|
/// An emoji was updated in this guild.
|
||||||
|
/// </summary>
|
||||||
|
EmojiUpdated = 61,
|
||||||
|
/// <summary>
|
||||||
|
/// An emoji was deleted from this guild.
|
||||||
|
/// </summary>
|
||||||
|
EmojiDeleted = 62,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A message was deleted from this guild.
|
||||||
|
/// </summary>
|
||||||
|
MessageDeleted = 72
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
namespace Discord
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents data applied to an <see cref="IAuditLogEntry"/>.
|
||||||
|
/// </summary>
|
||||||
|
public interface IAuditLogData
|
||||||
|
{ }
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Discord
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a generic audit log entry.
|
||||||
|
/// </summary>
|
||||||
|
public interface IAuditLogEntry : ISnowflakeEntity
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the action which occurred to create this entry.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// The type of action for this audit log entry.
|
||||||
|
/// </returns>
|
||||||
|
ActionType Action { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the data for this entry.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// An <see cref="IAuditLogData" /> for this audit log entry; <c>null</c> if no data is available.
|
||||||
|
/// </returns>
|
||||||
|
IAuditLogData Data { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the user responsible for causing the changes.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// A user object.
|
||||||
|
/// </returns>
|
||||||
|
IUser User { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the reason behind the change.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// A string containing the reason for the change; <c>null</c> if none is provided.
|
||||||
|
/// </returns>
|
||||||
|
string Reason { get; }
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
namespace Discord
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Specifies the cache mode that should be used.
|
||||||
|
/// </summary>
|
||||||
|
public enum CacheMode
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Allows the object to be downloaded if it does not exist in the current cache.
|
||||||
|
/// </summary>
|
||||||
|
AllowDownload,
|
||||||
|
/// <summary>
|
||||||
|
/// Only allows the object to be pulled from the existing cache.
|
||||||
|
/// </summary>
|
||||||
|
CacheOnly
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
namespace Discord
|
||||||
|
{
|
||||||
|
/// <summary> Defines the types of channels. </summary>
|
||||||
|
public enum ChannelType
|
||||||
|
{
|
||||||
|
/// <summary> The channel is a text channel. </summary>
|
||||||
|
Text = 0,
|
||||||
|
/// <summary> The channel is a Direct Message channel. </summary>
|
||||||
|
DM = 1,
|
||||||
|
/// <summary> The channel is a voice channel. </summary>
|
||||||
|
Voice = 2,
|
||||||
|
/// <summary> The channel is a group channel. </summary>
|
||||||
|
Group = 3,
|
||||||
|
/// <summary> The channel is a category channel. </summary>
|
||||||
|
Category = 4,
|
||||||
|
/// <summary> The channel is a news channel. </summary>
|
||||||
|
News = 5
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
namespace Discord
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Specifies the direction of where message(s) should be retrieved from.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This enum is used to specify the direction for retrieving messages.
|
||||||
|
/// <note type="important">
|
||||||
|
/// At the time of writing, <see cref="Around"/> is not yet implemented into
|
||||||
|
/// <see cref="IMessageChannel.GetMessagesAsync(int, CacheMode, RequestOptions)"/>.
|
||||||
|
/// Attempting to use the method with <see cref="Around"/> will throw
|
||||||
|
/// a <see cref="System.NotImplementedException"/>.
|
||||||
|
/// </note>
|
||||||
|
/// </remarks>
|
||||||
|
public enum Direction
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The message(s) should be retrieved before a message.
|
||||||
|
/// </summary>
|
||||||
|
Before,
|
||||||
|
/// <summary>
|
||||||
|
/// The message(s) should be retrieved after a message.
|
||||||
|
/// </summary>
|
||||||
|
After,
|
||||||
|
/// <summary>
|
||||||
|
/// The message(s) should be retrieved around a message.
|
||||||
|
/// </summary>
|
||||||
|
Around
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
namespace Discord
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Properties that are used to modify an <see cref="IGuildChannel" /> with the specified changes.
|
||||||
|
/// </summary>
|
||||||
|
/// <seealso cref="IGuildChannel.ModifyAsync"/>
|
||||||
|
public class GuildChannelProperties
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the channel to this name.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This property defines the new name for this channel.
|
||||||
|
/// <note type="warning">
|
||||||
|
/// When modifying an <see cref="ITextChannel"/>, the <see cref="Name"/> must be alphanumeric with
|
||||||
|
/// dashes. It must match the RegEx <c>[a-z0-9-_]{2,100}</c>.
|
||||||
|
/// </note>
|
||||||
|
/// </remarks>
|
||||||
|
public Optional<string> Name { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Moves the channel to the following position. This property is zero-based.
|
||||||
|
/// </summary>
|
||||||
|
public Optional<int> Position { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the category ID for this channel.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Setting this value to a category's snowflake identifier will change or set this channel's parent to the
|
||||||
|
/// specified channel; setting this value to <c>0</c> will detach this channel from its parent if one
|
||||||
|
/// is set.
|
||||||
|
/// </remarks>
|
||||||
|
public Optional<ulong?> CategoryId { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
using Discord.Audio;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Discord
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a generic audio channel.
|
||||||
|
/// </summary>
|
||||||
|
public interface IAudioChannel : IChannel
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Connects to this audio channel.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="selfDeaf">Determines whether the client should deaf itself upon connection.</param>
|
||||||
|
/// <param name="selfMute">Determines whether the client should mute itself upon connection.</param>
|
||||||
|
/// <param name="external">Determines whether the audio client is an external one or not.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// A task representing the asynchronous connection operation. The task result contains the
|
||||||
|
/// <see cref="IAudioClient"/> responsible for the connection.
|
||||||
|
/// </returns>
|
||||||
|
Task<IAudioClient> ConnectAsync(bool selfDeaf = false, bool selfMute = false, bool external = false);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Disconnects from this audio channel.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// A task representing the asynchronous operation for disconnecting from the audio channel.
|
||||||
|
/// </returns>
|
||||||
|
Task DisconnectAsync();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
namespace Discord
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a generic category channel.
|
||||||
|
/// </summary>
|
||||||
|
public interface ICategoryChannel : IGuildChannel
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue