- Created project, added Discord.NET

master
VollRagm 4 years ago
parent 8e1c343d33
commit a57886b800

@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.30204.135
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "stream-sniper", "stream-sniper\stream-sniper.csproj", "{D6DCDFC5-4DB3-4340-94F7-A5EC846E9195}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{D6DCDFC5-4DB3-4340-94F7-A5EC846E9195}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D6DCDFC5-4DB3-4340-94F7-A5EC846E9195}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D6DCDFC5-4DB3-4340-94F7-A5EC846E9195}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D6DCDFC5-4DB3-4340-94F7-A5EC846E9195}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {02E9EA2E-1B01-4F93-8D52-142CA749AC0E}
EndGlobalSection
EndGlobal

@ -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,17 @@
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
namespace stream_sniper
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : 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 &amp;&amp; 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&lt;MyModule&gt;(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&lt;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,94 @@
using System.Collections.Generic;
namespace Discord.Commands
{
/// <summary>
/// Utility class which contains the default matching pairs of quotation marks for CommandServiceConfig
/// </summary>
internal static class QuotationAliasUtils
{
/// <summary>
/// A default map of open-close pairs of quotation marks.
/// Contains many regional and Unicode equivalents.
/// Used in the <see cref="CommandServiceConfig"/>.
/// </summary>
/// <seealso cref="CommandServiceConfig.QuotationMarkAliasMap"/>
internal static Dictionary<char, char> GetDefaultAliasMap
{
get
{
// Output of a gist provided by https://gist.github.com/ufcpp
// https://gist.github.com/ufcpp/5b2cf9a9bf7d0b8743714a0b88f7edc5
// This was not used for the implementation because of incompatibility with netstandard1.1
return new Dictionary<char, char> {
{'\"', '\"' },
{'«', '»' },
{'', '' },
{'“', '”' },
{'„', '‟' },
{'', '' },
{'', '' },
{'《', '》' },
{'〈', '〉' },
{'「', '」' },
{'『', '』' },
{'〝', '〞' },
{'﹁', '﹂' },
{'﹃', '﹄' },
{'', '' },
{'', '' },
{'「', '」' },
{'(', ')' },
{'༺', '༻' },
{'༼', '༽' },
{'᚛', '᚜' },
{'⁅', '⁆' },
{'⌈', '⌉' },
{'⌊', '⌋' },
{'', '' },
{'❪', '❫' },
{'❬', '❭' },
{'', '' },
{'❰', '❱' },
{'', '' },
{'', '' },
{'⟅', '⟆' },
{'⟦', '⟧' },
{'⟨', '⟩' },
{'⟪', '⟫' },
{'⟬', '⟭' },
{'⟮', '⟯' },
{'⦃', '⦄' },
{'⦅', '⦆' },
{'⦇', '⦈' },
{'⦉', '⦊' },
{'⦋', '⦌' },
{'⦍', '⦎' },
{'⦏', '⦐' },
{'⦑', '⦒' },
{'⦓', '⦔' },
{'⦕', '⦖' },
{'⦗', '⦘' },
{'⧘', '⧙' },
{'⧚', '⧛' },
{'⧼', '⧽' },
{'⸂', '⸃' },
{'⸄', '⸅' },
{'⸉', '⸊' },
{'⸌', '⸍' },
{'⸜', '⸝' },
{'⸠', '⸡' },
{'⸢', '⸣' },
{'⸤', '⸥' },
{'⸦', '⸧' },
{'⸨', '⸩' },
{'【', '】'},
{'', '' },
{'〖', '〗' },
{'〘', '〙' },
{'〚', '〛' }
};
}
}
}
}

@ -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,19 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Discord.Audio
{
public abstract class AudioInStream : AudioStream
{
public abstract int AvailableFrames { get; }
public override bool CanRead => true;
public override bool CanWrite => true;
public abstract Task<RTPFrame> ReadFrameAsync(CancellationToken cancelToken);
public abstract bool TryReadFrame(CancellationToken cancelToken, out RTPFrame frame);
public override Task FlushAsync(CancellationToken cancelToken) { throw new NotSupportedException(); }
}
}

@ -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,35 @@
using System;
using System.Threading.Tasks;
namespace Discord.Audio
{
public interface IAudioClient : IDisposable
{
event Func<Task> Connected;
event Func<Exception, Task> Disconnected;
event Func<int, int, Task> LatencyUpdated;
event Func<int, int, Task> UdpLatencyUpdated;
event Func<ulong, AudioInStream, Task> StreamCreated;
event Func<ulong, Task> StreamDestroyed;
event Func<ulong, bool, Task> SpeakingUpdated;
/// <summary> Gets the current connection state of this client. </summary>
ConnectionState ConnectionState { get; }
/// <summary> Gets the estimated round-trip latency, in milliseconds, to the voice WebSocket server. </summary>
int Latency { get; }
/// <summary> Gets the estimated round-trip latency, in milliseconds, to the voice UDP server. </summary>
int UdpLatency { get; }
Task StopAsync();
Task SetSpeakingAsync(bool value);
/// <summary>Creates a new outgoing stream accepting Opus-encoded data.</summary>
AudioOutStream CreateOpusStream(int bufferMillis = 1000);
/// <summary>Creates a new outgoing stream accepting Opus-encoded data. This is a direct stream with no internal timer.</summary>
AudioOutStream CreateDirectOpusStream();
/// <summary>Creates a new outgoing stream accepting PCM (raw) data.</summary>
AudioOutStream CreatePCMStream(AudioApplication application, int? bitrate = null, int bufferMillis = 1000, int packetLoss = 30);
/// <summary>Creates a new direct outgoing stream accepting PCM (raw) data. This is a direct stream with no internal timer.</summary>
AudioOutStream CreateDirectPCMStream(AudioApplication application, int? bitrate = null, int packetLoss = 30);
}
}

@ -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…
Cancel
Save