Documentation Overhaul (#1161)

* Add XML docs

* Clean up style switcher

* Squash commits on branch docs/faq-n-patches

* Fix broken theme selector

* Add local image embed instruction

* Add a bunch of XML docs

* Add a bunch of XML docs

* Fix broken search
+ DocFX by default ships with an older version of jQuery, switching to a newer version confuses parts of the DocFX Javascript.

* Minor fixes for CONTRIBUTING.md and README.md

* Clean up filterConfig.yml

+ New config exposes Discord.Net namespace since it has several common public exceptions that may be helpful to users

* Add XML docs

* Read token from Environment Variable instead of hardcode

* Add XMLDocs

* Compress some assets & add OAuth2 URL generator

* Fix sample link & add missing pictures

* Add tag examples

* Fix embed docs consistency

* Add details regarding userbot support

* Add XML Docs

* Add XML Docs

* Add XML Docs

* Minor fixes in documentations
+ Fix unescaped '<'
+ Fix typo

* Fix seealso for preconditions and add missing descriptions

* Add missing exceptions

* Document exposed TypeReaders

* Fix letter-casing for files

* Add 'last modified' plugin

Source: https://github.com/Still34/DocFx.Plugin.LastModified
Licensed under MIT License

* XML Docs

* Fix minor consistencies & redundant impl

* Add properties examples to overwrite

* Fix missing Username prop

* Add warning for bulk-delete endpoint

* Replace note block

* Add BaseSocketClient docs

* Add XML docs

* Replace langword null to code block null instead

- Because DocFX sucks at rendering langword

* Replace all langword placements with code block

* Add more IGuild docs

* Add details to SpotifyGame

* Initial proofread of the articles

* Add explanation for RunMode

* Add event docs

- MessageReceived
- ChannelUpdated/Destroyed/Created

* Fix light theme link color

* Fix xml docs error

* Add partial documentation for audit log impl

* Add documentation for some REST-based objects

* Add partial documentation for audit log objects

* Add more XML comments to quotation mark alias map stuff, including an example

* Add reference to CommandServiceConfig from the util docs'

* Add explanation that if " is removed then it wont work

* Fix missing service provider in example

* Add documentation for new INestedChannel

* Add documentation

* Add documentation for new API version & few events

* Revise guide paragraphs/samples

+ Fix various formatting.
+ Provide a more detailed walkthrough for dependency injection.
+ Add C# note at intro.

* Fix typos & formatting

* Improve group module example

* Small amount to see if I'm doing it right

* Remove/cleanup redundant variables

* Fix EnterTypingState impl for doc inheritance

* Fix Test to resolve changes made in 15b58e

* Improve precondition documentation

+ Add precondition usage sample
+ Add precondition group usage sample
+ Move precondition samples to its own sample folder

* Move samples to individual folders

* Clarify token source

* Cleanup styling of README.md for docs

* Replace InvalidPathChars for NS1.3

* InvalidPathChars does not exist in NS1.3; replaced with GetInvalidPathChars instead.

* Add a missing change for 2c7cc738

* Update LastModified to v1.1.0 & add license

* Rewrite installation page for Core 2.1

* Fix anchor link

* Bump post-processor to v1.1.1

* Add fixes to partial file & add license

* Moved theme-switcher code to scripts partial file
+ Add author's MIT license to featherlight javascript

* Remove unused bootstrap plugin

* Bump LastModified plugin

* Changed the path from 'lastmodified' to 'last-modified' for consistency

* Cleanup README & Contribution guide

* Changes to last pr

* Fix GetCategoryAsync docs

* Proofread and cleanup articles

* Change passive voice in "Get Started" to active
* Fix improper preposition in Commands Introduction page
* Fix minor grammar mistakes in "Your First Bot" (future tense -> present tense/subjunctive mood -> indicative mood/proper noun casing/incorrect noun/add missing article)
* Fix minor grammar mistakes in "Installation" (missing article)

* no hablo ingles

* Try try try again

* I'm sure you're having as much fun as I am

* Cleanup TOC & fix titles

* Improve styling

+ Change title font to Noto Sans
+ Add materialized design for commit message box

* Add DescriptionGenerator plugin

* Add nightly section for clarification

* Fix typos in Nightlies & Post-execution

* Bump DescriptionGenerator to v1.1.0

+ This build adds the functionality of generating managed references' summary into the description tag.

* Initial emoji article draft

* Add 'additional information' section for emoji article

* Add cosmetic changes to the master css

* Alter info box color
+ Add transition to article content

* Add clarification in the emoji article

* Emphasize that normal emoji string will not translate to its Unicode representation.
* Clean up or add some of the samples featured in the article.
+ Add emoji/emote declaration section for clarification.
+ Add WebSocket emote sample.
- Remove inconsistent styling ('wacky memes' proves to be too out of place).

* Improve readability for nightlies article

* Move 'Bundled Preconditions' section

* Bump LastModified to fix UTC DateTime parsing

* Add langwordMapping.yml

* Add XML docs

* Add VSC workspace rule

* The root workspace limits the ruler to 120 characters for member documentations and excludes folders such as 'samples' and 'docs'.
* The docs workspace limits the ruler to 70 characters for standard conceptual article to comply with documentation's CONTRIBUTING.md rule, and excludes temprorary folders created by DocFX.

* Update CONTRIBUTING.md

* Add documentation style rule

* Fix styling of several member documentation

* Fix ' />' caused by Agent Smith oddities
* Fix styling to be more specific about the mention of IDs

* Fix exception summary to comply with official Microsoft Docs style

* References
https://docs.microsoft.com/en-us/dotnet/api/system.argumentnullexception?view=netframework-4.7.2
https://docs.microsoft.com/en-us/dotnet/api/system.platformnotsupportedexception?view=netframework-4.7.2
https://docs.microsoft.com/en-us/dotnet/api/system.badimageformatexception?view=netframework-4.7.2

* Add XML documentations

* Shift color return docs

* Fix minor docs

* Added documentation for SocketDMChannel, SocketGuildChannel, and SocketTextChannel

* Add XML docs

* Corrections to SocketGuildChannel

* Corrections to SocketTextChannel

* Corrections to SocketDMChannel

* Swapped out 'id' for 'snowflake identifier

* Swapped out 'id' for 'snowflake identifier'

* SocketDMChannel amendments

* SocketGuildChannel amendments

* SocketTextChannel amendments

* Add XML docs & patch return types
+ Starting from this commit, all return types for tasks will use style similar to most documentations featured on docs.microsoft.com

References:
https://docs.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext.-ctor?view=efcore-2.1
https://docs.microsoft.com/en-us/dotnet/api/system.io.filestream.readasync?view=netcore-2.1
https://docs.microsoft.com/en-us/dotnet/api/system.io.textwriter.writelineasync?view=netcore-2.1#System_IO_TextWriter_WriteLineAsync_System_Char___
And many more other asynchronous method documentations featured in the latest BCL.

* Added documentation for many audit log data types, fixed vowel indefinite articles

* Change audit log data types to start with 'Contains' (verb) instead of an article

* Fix some documentation issues and document some more audit log data types

* Fix English posession

* Add XML doc

* Documented two more types

* Documented RoleCreateAuditLogData

* Document remaining audit log data types

* Added RestDMChannel documentation

* Added RestGuildChannel documentation

* Added RestTextChannel documentation

* Added RestVoiceChannel documentation

* Added RestUser documentation

* Added RestRole documentation

* Added RestMessage documentation

* Slightly better wording

* Contains -> Contains a piece of (describe article)

* [EN] Present perf. -> past perf.

* Add XML docs

* Fix arrow alignment

* Clarify supported nullable type

* Fixed a typo in ISnowflakeEntity

* Added RestUser Documentation

* Added RestInvite documentation

* Add XML docs & minor optimizations

* Minor optimization for doc rendering

* Rollback font optimization changes

* Amendments to RestUser

* Added SocketDMChannel documentation

* Added RestDMChannel documentation

* Added RestGuild documentation

* Adjustment to SocketDMChannel

* Added minimal descriptions from the API documentation for Integration types

* Added obsolete mention to the ReadMessages flag.

* Added remarks about 2FA requirement for guild permissions

* Added xmldoc for GuildPermission methods

* Added xml doc for ToAllowList and ToDenyList

* Added specification of how the bits of the color raw value are packed

* Added discord API documentation to IConnection interface

* I can spell :^)

* Fix whitespace in ChannelPermission

* fix spacing of values in guildpermission

* Made changes to get field descriptions from feedback, added returns tag to IConnection

* Added property get standard for IntegrationAccount

* Added property get pattern to xml docs and identical returns tag.

* Change all color class references to struct
...because it isn't a class.

* Add XML docs

* Rewrote the returns tags in IGuildIntegration, removed the ones I was unsure about.

* Rewrote the rest of the returns tags

* Amendments

* Cleanup doc for c1d78189

* Added types to <returns> tags where missing

* Added second sample for adding reactions

* Added some class summaries

* Missed a period

* Amendments

* restored the removed line break

* Removed unnecessary see tag

* Use consistent quotation marks around subscribers, the name for these users are dependant on the source of where they are integrated from (youtube or twitch), so we should not use a name that is specific to one platform

* Add <remarks> tag to the IGuildIntegration xmldocs

* Fix grammar issue

* Update DescriptionGenerator

* Cleanup of https://github.com/Still34/Discord.Net/pull/8

* Cleanup previous PR

* Fix for misleading behaviour in the emoji guide
+ Original lines stated that sending a emoji wrapped in colon will not be parsed, but that was incorrect; replaced with reactions instead of sending messages as the example

* Add strings for dictionary in DotSettings

* Add XML docs

* Fix lots of typos in comments
+ Geez, I didn't know there were so many.

* Add XML docs & rewrite GetMessagesAsync docs

This commit rewrites the remarks section of GetMessagesAsync, as well as adding examples to several methods.

* Update 'Your First Bot'
+ This commit reflects the new changes made to the Discord Application Developer Portal after its major update

* Initial optimization for DocFX render & add missing files

* Add examples in message methods

* Cleanup https://github.com/RogueException/Discord.Net/pull/1128

* Fix first bot note

* Cleanup FAQ structure

* Add XML docs

* Update docfx plugins

* Fix navbar collapsing issue

* Fix broken xref

* Cleanup FAQ section
+ Add introductory paragraphs to each FAQ section.
+ Add 'missing dependency' entry to commands FAQ.
* Split commands FAQ to 'General' and 'DI' sections.

* Cleanup https://github.com/RogueException/Discord.Net/pull/1139

* Fix missing namespace

* Add missing highlighting css for the light theme

* Add additional clarification for installing packages

* Add indentation to example for clarity

* Cleanup several articles to be more human-friendly and easier to read

* Remove RPC-related notes

* Cleanup slow-mode-related documentation strings

* Add an additional note about cross-guild emote usage

* Add CreateTextChannel sample

* Add XMLDocs
This commit is contained in:
Still Hsu
2018-10-01 05:44:33 +08:00
committed by Christopher F
parent 6b21b11f7d
commit ff0fea98a6
498 changed files with 16064 additions and 2633 deletions

View File

@@ -2,14 +2,37 @@ using System;
namespace Discord.Commands
{
/// <summary> Provides aliases for a command. </summary>
/// <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> The aliases which have been defined for the command. </summary>
/// <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>
/// <summary>
/// Creates a new <see cref="AliasAttribute" /> with the given aliases.
/// </summary>
public AliasAttribute(params string[] aliases)
{
Aliases = aliases;

View File

@@ -2,17 +2,32 @@ 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;

View File

@@ -2,6 +2,14 @@ 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
{

View File

@@ -1,9 +1,31 @@
using System;
namespace Discord.Commands {
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class DontInjectAttribute : Attribute {
}
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
{
}
}

View File

@@ -2,15 +2,26 @@ 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;

View File

@@ -3,11 +3,21 @@ 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;

View File

@@ -4,17 +4,46 @@ 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, AllowMultiple = false, Inherited = true)]
public 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)}");
throw new ArgumentException($"{nameof(overridenTypeReader)} must inherit from {nameof(TypeReader)}.");
TypeReader = overridenTypeReader;
}

View File

@@ -3,9 +3,20 @@ 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);
}
}

View File

@@ -1,18 +1,31 @@
using System;
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>
/// Specify a group that this precondition belongs to. Preconditions of the same group require only one
/// of the preconditions to pass in order to be successful (A || B). Specifying <see cref="Group"/> = <see langword="null"/>
/// or not at all will require *all* preconditions to pass, just like normal (A &amp;&amp; B).
/// 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>
/// 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);
}
}

View File

@@ -1,46 +1,52 @@
using System;
using System;
using System.Threading.Tasks;
namespace Discord.Commands
{
/// <summary>
/// This attribute requires that the bot has a specified permission in the channel a command is invoked in.
/// 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; }
/// <summary>
/// Require that the bot account has a specified GuildPermission
/// 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 private channel.</remarks>
/// <param name="permission">The GuildPermission that the bot must have. Multiple permissions can be specified by ORing the permissions together.</param>
/// <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>
/// Require that the bot account has a specified ChannelPermission.
/// Requires that the bot account to have a specific <see cref="Discord.ChannelPermission"/>.
/// </summary>
/// <param name="permission">The ChannelPermission that the bot must have. Multiple permissions can be specified by ORing the permissions together.</param>
/// <example>
/// <code language="c#">
/// [Command("permission")]
/// [RequireBotPermission(ChannelPermission.ManageMessages)]
/// public async Task Purge()
/// {
/// }
/// </code>
/// </example>
/// <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;
@@ -50,9 +56,9 @@ namespace Discord.Commands
if (GuildPermission.HasValue)
{
if (guildUser == null)
return PreconditionResult.FromError("Command must be used in a guild channel");
return PreconditionResult.FromError("Command must be used in a guild channel.");
if (!guildUser.GuildPermissions.Has(GuildPermission.Value))
return PreconditionResult.FromError($"Bot requires guild permission {GuildPermission.Value}");
return PreconditionResult.FromError($"Bot requires guild permission {GuildPermission.Value}.");
}
if (ChannelPermission.HasValue)
@@ -64,7 +70,7 @@ namespace Discord.Commands
perms = ChannelPermissions.All(context.Channel);
if (!perms.Has(ChannelPermission.Value))
return PreconditionResult.FromError($"Bot requires channel permission {ChannelPermission.Value}");
return PreconditionResult.FromError($"Bot requires channel permission {ChannelPermission.Value}.");
}
return PreconditionResult.FromSuccess();

View File

@@ -1,35 +1,48 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
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>
/// Require that the command be invoked in a specified context.
/// 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; }
/// <summary>
/// Require that the command be invoked in a specified context.
/// </summary>
/// <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="c#">
/// [Command("private_only")]
/// <code language="cs">
/// [Command("secret")]
/// [RequireContext(ContextType.DM | ContextType.Group)]
/// public async Task PrivateOnly()
/// public Task PrivateOnlyAsync()
/// {
/// return ReplyAsync("shh, this command is a secret");
/// }
/// </code>
/// </example>
@@ -38,12 +51,13 @@ namespace Discord.Commands
Contexts = contexts;
}
/// <inheritdoc />
public override Task<PreconditionResult> CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services)
{
bool isValid = false;
if ((Contexts & ContextType.Guild) != 0)
isValid = isValid || context.Channel is IGuildChannel;
isValid = context.Channel is IGuildChannel;
if ((Contexts & ContextType.DM) != 0)
isValid = isValid || context.Channel is IDMChannel;
if ((Contexts & ContextType.Group) != 0)
@@ -52,7 +66,7 @@ namespace Discord.Commands
if (isValid)
return Task.FromResult(PreconditionResult.FromSuccess());
else
return Task.FromResult(PreconditionResult.FromError($"Invalid context for command; accepted contexts: {Contexts}"));
return Task.FromResult(PreconditionResult.FromError($"Invalid context for command; accepted contexts: {Contexts}."));
}
}
}

View File

@@ -4,11 +4,33 @@ using System.Threading.Tasks;
namespace Discord.Commands
{
/// <summary>
/// Require that the command is invoked in a channel marked NSFW
/// 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 Task<PreconditionResult> CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services)
{
if (context.Channel is ITextChannel text && text.IsNsfw)

View File

@@ -4,20 +4,45 @@ using System.Threading.Tasks;
namespace Discord.Commands
{
/// <summary>
/// Require that the command is invoked by the owner of the bot.
/// Requires the command to be invoked by the owner of the bot.
/// </summary>
/// <remarks>This precondition will only work if the bot is a bot account.</remarks>
/// <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 async Task<PreconditionResult> CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services)
{
switch (context.Client.TokenType)
{
case TokenType.Bot:
var application = await context.Client.GetApplicationInfoAsync();
var application = await context.Client.GetApplicationInfoAsync().ConfigureAwait(false);
if (context.User.Id != application.Owner.Id)
return PreconditionResult.FromError("Command can only be run by the owner of the bot");
return PreconditionResult.FromError("Command can only be run by the owner of the bot.");
return PreconditionResult.FromSuccess();
default:
return PreconditionResult.FromError($"{nameof(RequireOwnerAttribute)} is not supported by this {nameof(TokenType)}.");

View File

@@ -1,47 +1,52 @@
using System;
using System;
using System.Threading.Tasks;
namespace Discord.Commands
{
/// <summary>
/// This attribute requires that the user invoking the command has a specified permission.
/// 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; }
/// <summary>
/// Require that the user invoking the command has a specified GuildPermission
/// 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 private channel.</remarks>
/// <param name="permission">The GuildPermission that the user must have. Multiple permissions can be specified by ORing the permissions together.</param>
/// <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>
/// Require that the user invoking the command has a specified ChannelPermission.
/// Requires that the user invoking the command to have a specific <see cref="Discord.ChannelPermission"/>.
/// </summary>
/// <param name="permission">The ChannelPermission that the user must have. Multiple permissions can be specified by ORing the permissions together.</param>
/// <example>
/// <code language="c#">
/// [Command("permission")]
/// [RequireUserPermission(ChannelPermission.ReadMessageHistory | ChannelPermission.ReadMessages)]
/// public async Task HasPermission()
/// {
/// await ReplyAsync("You can read messages and the message history!");
/// }
/// </code>
/// </example>
/// <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;
@@ -49,9 +54,9 @@ namespace Discord.Commands
if (GuildPermission.HasValue)
{
if (guildUser == null)
return Task.FromResult(PreconditionResult.FromError("Command must be used in a guild channel"));
return Task.FromResult(PreconditionResult.FromError("Command must be used in a guild channel."));
if (!guildUser.GuildPermissions.Has(GuildPermission.Value))
return Task.FromResult(PreconditionResult.FromError($"User requires guild permission {GuildPermission.Value}"));
return Task.FromResult(PreconditionResult.FromError($"User requires guild permission {GuildPermission.Value}."));
}
if (ChannelPermission.HasValue)
@@ -63,7 +68,7 @@ namespace Discord.Commands
perms = ChannelPermissions.All(context.Channel);
if (!perms.Has(ChannelPermission.Value))
return Task.FromResult(PreconditionResult.FromError($"User requires channel permission {ChannelPermission.Value}"));
return Task.FromResult(PreconditionResult.FromError($"User requires channel permission {ChannelPermission.Value}."));
}
return Task.FromResult(PreconditionResult.FromSuccess());

View File

@@ -2,14 +2,20 @@ using System;
namespace Discord.Commands
{
/// <summary> Sets priority of commands </summary>
/// <summary>
/// Sets priority of commands.
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class PriorityAttribute : Attribute
{
/// <summary> The priority which has been set for the command </summary>
/// <summary>
/// Gets the priority which has been set for the command.
/// </summary>
public int Priority { get; }
/// <summary> Creates a new <see cref="PriorityAttribute"/> with the given priority. </summary>
/// <summary>
/// Initializes a new <see cref="PriorityAttribute" /> attribute with the given priority.
/// </summary>
public PriorityAttribute(int priority)
{
Priority = priority;

View File

@@ -2,6 +2,9 @@ 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
{

View File

@@ -3,6 +3,9 @@ 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
{

View File

@@ -3,6 +3,9 @@ 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
{

View File

@@ -118,6 +118,7 @@ namespace Discord.Commands.Builders
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

View File

@@ -34,7 +34,7 @@ namespace Discord.Commands
}
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)}.");
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);
}
}

View File

@@ -1,15 +1,27 @@
namespace Discord.Commands
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;

View File

@@ -1,26 +1,51 @@
namespace Discord.Commands
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
}
}

View File

@@ -2,11 +2,24 @@ 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)
{

View File

@@ -1,13 +1,14 @@
using System;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
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)

View File

@@ -170,7 +170,7 @@ namespace Discord.Commands
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");
return ParseResult.FromError(CommandError.ParseFailed, "A quoted parameter is incomplete.");
//Add missing optionals
for (int i = argList.Count; i < command.Parameters.Count; i++)

View File

@@ -11,11 +11,44 @@ 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
{
/// <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>
/// <para>
/// This event is fired when a command has been successfully executed without any of the following errors:
/// </para>
/// <para>* Parsing error</para>
/// <para>* Precondition error</para>
/// <para>* Runtime exception</para>
/// <para>
/// Should the command encounter any of the aforementioned error, this event will not be raised.
/// </para>
/// </remarks>
public event Func<CommandInfo, ICommandContext, IResult, Task> CommandExecuted { add { _commandExecutedEvent.Add(value); } remove { _commandExecutedEvent.Remove(value); } }
internal readonly AsyncEvent<Func<CommandInfo, ICommandContext, IResult, Task>> _commandExecutedEvent = new AsyncEvent<Func<CommandInfo, ICommandContext, IResult, Task>>();
@@ -34,11 +67,33 @@ namespace Discord.Commands
internal readonly LogManager _logManager;
internal readonly IReadOnlyDictionary<char, char> _quotationMarkAliasMap;
/// <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;
@@ -102,12 +157,39 @@ namespace Discord.Commands
}
/// <summary>
/// Add a command module from a type
/// Add a command module from a <see cref="Type" />.
/// </summary>
/// <typeparam name="T">The type of module</typeparam>
/// <param name="services">An IServiceProvider for your dependency injection solution, if using one - otherwise, pass null</param>
/// <returns>A built module</returns>
/// <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;
@@ -135,11 +217,14 @@ namespace Discord.Commands
}
}
/// <summary>
/// Add command modules from an assembly
/// Add command modules from an <see cref="Assembly"/>.
/// </summary>
/// <param name="assembly">The assembly containing command modules</param>
/// <param name="services">An IServiceProvider for your dependency injection solution, if using one - otherwise, pass null</param>
/// <returns>A collection of built modules</returns>
/// <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;
@@ -175,7 +260,14 @@ namespace Discord.Commands
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);
@@ -188,7 +280,23 @@ namespace Discord.Commands
_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);
@@ -222,21 +330,27 @@ namespace Discord.Commands
//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 <see cref="NullableTypeReader{T}"/> 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.
/// 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>
/// <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 <see cref="NullableTypeReader{T}"/> 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.
/// 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>
/// <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))
@@ -245,21 +359,31 @@ namespace Discord.Commands
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 <see cref="NullableTypeReader{T}"/> will also be added.
/// 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">If <paramref name="reader"/> should replace the default <see cref="TypeReader"/> for <typeparamref name="T"/> if one exists.</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<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 <see cref="NullableTypeReader{T}"/> for the value type will also be added.
/// 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">If <paramref name="reader"/> should replace the default <see cref="TypeReader"/> for <paramref name="type"/> if one exists.</param>
/// <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))
@@ -331,8 +455,20 @@ namespace Discord.Commands
}
//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)
@@ -346,8 +482,30 @@ namespace Discord.Commands
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;

View File

@@ -1,29 +1,62 @@
using System;
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 RunMode commands should have, if one is not specified on the Command attribute or builder. </summary>
/// <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> Determines whether commands should be case-sensitive. </summary>
/// <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 Log event. </summary>
/// <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> Determines whether RunMode.Sync commands should push exceptions up to the caller. </summary>
/// <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 that can wrap strings for command parsing.
/// represents the opening quotation mark and the value is the corresponding closing mark.</summary>
/// <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> Determines whether extra parameters should be ignored. </summary>
/// <summary>
/// Gets or sets a value that indicates whether extra parameters should be ignored.
/// </summary>
public bool IgnoreExtraArgs { get; set; } = false;
}
}

View File

@@ -2,8 +2,20 @@ 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;
@@ -14,6 +26,9 @@ namespace Discord.Commands
}
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;
@@ -24,6 +39,9 @@ namespace Discord.Commands
}
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;

View File

@@ -8,10 +8,16 @@ using System.Linq;
using System.Reflection;
using System.Runtime.ExceptionServices;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
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
{
@@ -21,18 +27,63 @@ namespace Discord.Commands
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)
@@ -100,11 +151,11 @@ namespace Discord.Commands
return PreconditionGroupResult.FromSuccess();
}
var moduleResult = await CheckGroups(Module.Preconditions, "Module");
var moduleResult = await CheckGroups(Module.Preconditions, "Module").ConfigureAwait(false);
if (!moduleResult.IsSuccess)
return moduleResult;
var commandResult = await CheckGroups(Preconditions, "Command");
var commandResult = await CheckGroups(Preconditions, "Command").ConfigureAwait(false);
if (!commandResult.IsSuccess)
return commandResult;
@@ -124,7 +175,7 @@ namespace Discord.Commands
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)
@@ -248,11 +299,11 @@ namespace Discord.Commands
foreach (object arg in argList)
{
if (i == argCount)
throw new InvalidOperationException("Command was invoked with too many parameters");
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");
throw new InvalidOperationException("Command was invoked with too few parameters.");
if (HasVarArgs)
{

View File

@@ -6,20 +6,59 @@ 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)

View File

@@ -2,25 +2,56 @@ using Discord.Commands.Builders;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
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)
@@ -65,4 +96,4 @@ namespace Discord.Commands
public override string ToString() => Name;
private string DebuggerDisplay => $"{Name}{(IsOptional ? " (Optional)" : "")}{(IsRemainder ? " (Remainder)" : "")}";
}
}
}

View File

@@ -23,6 +23,7 @@ namespace Discord.Commands
_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);

View File

@@ -4,32 +4,57 @@ 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
/// 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 message aloud using TTS</param>
/// <param name="embed">An embed to be displayed alongside the message</param>
/// <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)
{
}
@@ -38,7 +63,7 @@ namespace Discord.Commands
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}");
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);

View File

@@ -1,8 +1,13 @@
namespace Discord.Commands
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
}
}

View File

@@ -6,19 +6,29 @@ 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);
ulong id;
//By Mention (1.0)
if (MentionUtils.TryParseChannel(input, out id))
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)

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
@@ -44,6 +44,7 @@ namespace Discord.Commands
_enumsByValue = byValueBuilder.ToImmutable();
}
/// <inheritdoc />
public override Task<TypeReaderResult> ReadAsync(ICommandContext context, string input, IServiceProvider services)
{
object enumValue;
@@ -53,14 +54,14 @@ namespace Discord.Commands
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}"));
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}"));
return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, $"Value is not a {_enumType.Name}."));
}
}
}

View File

@@ -4,15 +4,18 @@ 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)
{
ulong id;
//By Id (1.0)
if (ulong.TryParse(input, NumberStyles.None, CultureInfo.InvariantCulture, out id))
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);

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
@@ -24,11 +24,12 @@ namespace Discord.Commands
_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);
return await _baseTypeReader.ReadAsync(context, input, services).ConfigureAwait(false);
}
}
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Threading.Tasks;
namespace Discord.Commands
@@ -17,14 +17,16 @@ namespace Discord.Commands
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]");
throw new ArgumentOutOfRangeException(nameof(score), score, "Scores must be within the range [0, 1].");
_tryParse = tryParse;
_score = score;
@@ -34,7 +36,7 @@ namespace Discord.Commands
{
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}"));
return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, $"Failed to parse {typeof(T).Name}."));
}
}
}

View File

@@ -6,20 +6,23 @@ 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)
{
ulong id;
if (context.Guild != null)
{
var results = new Dictionary<ulong, TypeReaderValue>();
var roles = context.Guild.Roles;
//By Mention (1.0)
if (MentionUtils.TryParseRole(input, out id))
if (MentionUtils.TryParseRole(input, out var id))
AddResult(results, context.Guild.GetRole(id) as T, 1.00f);
//By Id (0.9)

View File

@@ -24,6 +24,7 @@ namespace Discord.Commands
"%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))

View File

@@ -1,10 +1,22 @@
using System;
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);
}
}

View File

@@ -7,21 +7,25 @@ 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>();
ulong id;
if (context.Guild != null)
guildUsers = await context.Guild.GetUsersAsync(CacheMode.CacheOnly).ConfigureAwait(false);
//By Mention (1.0)
if (MentionUtils.TryParseUser(input, out id))
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);
@@ -46,7 +50,7 @@ namespace Discord.Commands
if (ushort.TryParse(input.Substring(index + 1), out ushort discriminator))
{
var channelUser = await channelUsers.FirstOrDefault(x => x.DiscriminatorValue == discriminator &&
string.Equals(username, x.Username, StringComparison.OrdinalIgnoreCase));
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 &&
@@ -59,7 +63,8 @@ namespace Discord.Commands
{
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));
.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);
@@ -69,7 +74,8 @@ namespace Discord.Commands
{
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));
.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);

View File

@@ -1,16 +1,25 @@
using System;
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)
@@ -20,15 +29,56 @@ namespace Discord.Commands
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}";
}

View File

@@ -1,9 +1,31 @@
namespace Discord.Commands
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; }
}
}

View File

@@ -4,15 +4,21 @@ 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; }
/// <inheritdoc/>
public bool IsSuccess => !Error.HasValue;
private ParseResult(IReadOnlyList<TypeReaderResult> argValues, IReadOnlyList<TypeReaderResult> paramValues, CommandError? error, string errorReason)
@@ -22,7 +28,7 @@ namespace Discord.Commands
Error = error;
ErrorReason = errorReason;
}
public static ParseResult FromSuccess(IReadOnlyList<TypeReaderResult> argValues, IReadOnlyList<TypeReaderResult> paramValues)
{
for (int i = 0; i < argValues.Count; i++)

View File

@@ -15,7 +15,7 @@ namespace Discord.Commands
PreconditionResults = (preconditions ?? new List<PreconditionResult>(0)).ToReadOnlyCollection();
}
public static new PreconditionGroupResult FromSuccess()
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);

View File

@@ -3,29 +3,56 @@ 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}";
}

View File

@@ -1,24 +1,30 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
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");

View File

@@ -10,9 +10,12 @@ namespace Discord.Commands
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)

View File

@@ -27,10 +27,15 @@ namespace Discord.Commands
{
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.");

View File

@@ -1,9 +1,23 @@
namespace Discord.Commands
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
}
}

View File

@@ -1,19 +1,18 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Globalization;
namespace Discord.Commands
{
/// <summary>
/// Utility methods for generating matching pairs of unicode quotation marks for CommandServiceConfig
/// Utility class which contains the default matching pairs of quotation marks for CommandServiceConfig
/// </summary>
internal static class QuotationAliasUtils
{
/// <summary>
/// Generates an IEnumerable of characters representing open-close pairs of
/// quotation punctuation.
/// 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

View File

@@ -2,7 +2,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
namespace Discord.Commands
{
@@ -38,7 +37,7 @@ namespace Discord.Commands
}
catch (Exception ex)
{
throw new Exception($"Failed to create \"{ownerType.FullName}\"", ex);
throw new Exception($"Failed to create \"{ownerType.FullName}\".", ex);
}
}
@@ -46,12 +45,12 @@ namespace Discord.Commands
{
var constructors = ownerType.DeclaredConstructors.Where(x => !x.IsStatic).ToArray();
if (constructors.Length == 0)
throw new InvalidOperationException($"No constructor found for \"{ownerType.FullName}\"");
throw new InvalidOperationException($"No constructor found for \"{ownerType.FullName}\".");
else if (constructors.Length > 1)
throw new InvalidOperationException($"Multiple constructors found for \"{ownerType.FullName}\"");
throw new InvalidOperationException($"Multiple constructors found for \"{ownerType.FullName}\".");
return constructors[0];
}
private static System.Reflection.PropertyInfo[] GetProperties(TypeInfo ownerType)
private static PropertyInfo[] GetProperties(TypeInfo ownerType)
{
var result = new List<System.Reflection.PropertyInfo>();
while (ownerType != ObjectTypeInfo)
@@ -71,7 +70,7 @@ namespace Discord.Commands
return commands;
if (memberType == typeof(IServiceProvider) || memberType == services.GetType())
return services;
var service = services?.GetService(memberType);
var service = services.GetService(memberType);
if (service != null)
return service;
throw new InvalidOperationException($"Failed to create \"{ownerType.FullName}\", dependency \"{memberType.Name}\" was not found.");