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