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.");

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.IO;
namespace Discord.Audio
@@ -7,8 +7,17 @@ namespace Discord.Audio
{
public override bool CanWrite => true;
public override int Read(byte[] buffer, int offset, int count) { throw new NotSupportedException(); }
public override void SetLength(long value) { throw new NotSupportedException(); }
public override long Seek(long offset, SeekOrigin origin) { throw new NotSupportedException(); }
/// <inheritdoc />
/// <exception cref="NotSupportedException">Reading this stream is not supported.</exception>
public override int Read(byte[] buffer, int offset, int count) =>
throw new NotSupportedException();
/// <inheritdoc />
/// <exception cref="NotSupportedException">Setting the length to this stream is not supported.</exception>
public override void SetLength(long value) =>
throw new NotSupportedException();
/// <inheritdoc />
/// <exception cref="NotSupportedException">Seeking this stream is not supported..</exception>
public override long Seek(long offset, SeekOrigin origin) =>
throw new NotSupportedException();
}
}

View File

@@ -11,10 +11,9 @@ namespace Discord.Audio
public override bool CanSeek => false;
public override bool CanWrite => false;
public virtual void WriteHeader(ushort seq, uint timestamp, bool missed)
{
throw new InvalidOperationException("This stream does not accept headers");
}
/// <exception cref="InvalidOperationException">This stream does not accept headers.</exception>
public virtual void WriteHeader(ushort seq, uint timestamp, bool missed) =>
throw new InvalidOperationException("This stream does not accept headers.");
public override void Write(byte[] buffer, int offset, int count)
{
WriteAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult();
@@ -30,15 +29,30 @@ namespace Discord.Audio
public virtual Task ClearAsync(CancellationToken cancellationToken) { return Task.Delay(0); }
public override long Length { get { throw new NotSupportedException(); } }
/// <inheritdoc />
/// <exception cref="NotSupportedException">Reading stream length is not supported.</exception>
public override long Length =>
throw new NotSupportedException();
/// <inheritdoc />
/// <exception cref="NotSupportedException">Getting or setting this stream position is not supported.</exception>
public override long Position
{
get { throw new NotSupportedException(); }
set { throw new NotSupportedException(); }
get => throw new NotSupportedException();
set => throw new NotSupportedException();
}
public override int Read(byte[] buffer, int offset, int count) { throw new NotSupportedException(); }
public override void SetLength(long value) { throw new NotSupportedException(); }
public override long Seek(long offset, SeekOrigin origin) { throw new NotSupportedException(); }
/// <inheritdoc />
/// <exception cref="NotSupportedException">Reading this stream is not supported.</exception>
public override int Read(byte[] buffer, int offset, int count) =>
throw new NotSupportedException();
/// <inheritdoc />
/// <exception cref="NotSupportedException">Setting the length to this stream is not supported.</exception>
public override void SetLength(long value) =>
throw new NotSupportedException();
/// <inheritdoc />
/// <exception cref="NotSupportedException">Seeking this stream is not supported..</exception>
public override long Seek(long offset, SeekOrigin origin) =>
throw new NotSupportedException();
}
}

View File

@@ -15,7 +15,7 @@ namespace Discord.Audio
/// <summary> Gets the current connection state of this client. </summary>
ConnectionState ConnectionState { get; }
/// <summary> Gets the estimated round-trip latency, in milliseconds, to the voice websocket server. </summary>
/// <summary> Gets the estimated round-trip latency, in milliseconds, to the voice WebSocket server. </summary>
int Latency { get; }
/// <summary> Gets the estimated round-trip latency, in milliseconds, to the voice UDP server. </summary>
int UdpLatency { get; }

View File

@@ -2,10 +2,32 @@ using System;
namespace Discord
{
/// <summary>
/// Represents a class containing the strings related to various Content Delivery Networks (CDNs).
/// </summary>
public static class CDN
{
/// <summary>
/// Returns an application icon URL.
/// </summary>
/// <param name="appId">The application identifier.</param>
/// <param name="iconId">The icon identifier.</param>
/// <returns>
/// A URL pointing to the application's icon.
/// </returns>
public static string GetApplicationIconUrl(ulong appId, string iconId)
=> iconId != null ? $"{DiscordConfig.CDNUrl}app-icons/{appId}/{iconId}.jpg" : null;
/// <summary>
/// Returns a user avatar URL.
/// </summary>
/// <param name="userId">The user snowflake identifier.</param>
/// <param name="avatarId">The avatar identifier.</param>
/// <param name="size">The size of the image to return in. This can be any power of two between 16 and 2048.</param>
/// <param name="format">The format to return.</param>
/// <returns>
/// A URL pointing to the user's avatar in the specified size.
/// </returns>
public static string GetUserAvatarUrl(ulong userId, string avatarId, ushort size, ImageFormat format)
{
if (avatarId == null)
@@ -13,27 +35,90 @@ namespace Discord
string extension = FormatToExtension(format, avatarId);
return $"{DiscordConfig.CDNUrl}avatars/{userId}/{avatarId}.{extension}?size={size}";
}
/// <summary>
/// Returns the default user avatar URL.
/// </summary>
/// <param name="discriminator">The discriminator value of a user.</param>
/// <returns>
/// A URL pointing to the user's default avatar when one isn't set.
/// </returns>
public static string GetDefaultUserAvatarUrl(ushort discriminator)
{
return $"{DiscordConfig.CDNUrl}embed/avatars/{discriminator % 5}.png";
}
/// <summary>
/// Returns an icon URL.
/// </summary>
/// <param name="guildId">The guild snowflake identifier.</param>
/// <param name="iconId">The icon identifier.</param>
/// <returns>
/// A URL pointing to the guild's icon.
/// </returns>
public static string GetGuildIconUrl(ulong guildId, string iconId)
=> iconId != null ? $"{DiscordConfig.CDNUrl}icons/{guildId}/{iconId}.jpg" : null;
/// <summary>
/// Returns a guild splash URL.
/// </summary>
/// <param name="guildId">The guild snowflake identifier.</param>
/// <param name="splashId">The splash icon identifier.</param>
/// <returns>
/// A URL pointing to the guild's icon.
/// </returns>
public static string GetGuildSplashUrl(ulong guildId, string splashId)
=> splashId != null ? $"{DiscordConfig.CDNUrl}splashes/{guildId}/{splashId}.jpg" : null;
/// <summary>
/// Returns a channel icon URL.
/// </summary>
/// <param name="channelId">The channel snowflake identifier.</param>
/// <param name="iconId">The icon identifier.</param>
/// <returns>
/// A URL pointing to the channel's icon.
/// </returns>
public static string GetChannelIconUrl(ulong channelId, string iconId)
=> iconId != null ? $"{DiscordConfig.CDNUrl}channel-icons/{channelId}/{iconId}.jpg" : null;
/// <summary>
/// Returns an emoji URL.
/// </summary>
/// <param name="emojiId">The emoji snowflake identifier.</param>
/// <param name="animated">Whether this emoji is animated.</param>
/// <returns>
/// A URL pointing to the custom emote.
/// </returns>
public static string GetEmojiUrl(ulong emojiId, bool animated)
=> $"{DiscordConfig.CDNUrl}emojis/{emojiId}.{(animated ? "gif" : "png")}";
/// <summary>
/// Returns a Rich Presence asset URL.
/// </summary>
/// <param name="appId">The application identifier.</param>
/// <param name="assetId">The asset identifier.</param>
/// <param name="size">The size of the image to return in. This can be any power of two between 16 and 2048.</param>
/// <param name="format">The format to return.</param>
/// <returns>
/// A URL pointing to the asset image in the specified size.
/// </returns>
public static string GetRichAssetUrl(ulong appId, string assetId, ushort size, ImageFormat format)
{
string extension = FormatToExtension(format, "");
return $"{DiscordConfig.CDNUrl}app-assets/{appId}/{assetId}.{extension}?size={size}";
}
/// <summary>
/// Returns a Spotify album URL.
/// </summary>
/// <param name="albumArtId">The identifier for the album art (e.g. 6be8f4c8614ecf4f1dd3ebba8d8692d8ce4951ac).</param>
/// <returns>
/// A URL pointing to the Spotify album art.
/// </returns>
public static string GetSpotifyAlbumArtUrl(string albumArtId)
=> $"https://i.scdn.co/image/{albumArtId}";
/// <summary>
/// Returns a Spotify direct URL for a track.
/// </summary>
/// <param name="trackId">The identifier for the track (e.g. 4uLU6hMCjMI75M1A2tKUQC).</param>
/// <returns>
/// A URL pointing to the Spotify track.
/// </returns>
public static string GetSpotifyDirectUrl(string trackId)
=> $"https://open.spotify.com/track/{trackId}";

View File

@@ -1,11 +1,29 @@
namespace Discord.Commands
namespace Discord.Commands
{
/// <summary>
/// Represents a context of a command. This may include the client, guild, channel, user, and message.
/// </summary>
public interface ICommandContext
{
/// <summary>
/// Gets the <see cref="IDiscordClient" /> that the command is executed with.
/// </summary>
IDiscordClient Client { get; }
/// <summary>
/// Gets the <see cref="IGuild" /> that the command is executed in.
/// </summary>
IGuild Guild { get; }
/// <summary>
/// Gets the <see cref="IMessageChannel" /> that the command is executed in.
/// </summary>
IMessageChannel Channel { get; }
/// <summary>
/// Gets the <see cref="IUser" /> who executed the command.
/// </summary>
IUser User { get; }
/// <summary>
/// Gets the <see cref="IUserMessage" /> that the command is interpreted from.
/// </summary>
IUserMessage Message { get; }
}
}

View File

@@ -1,10 +1,15 @@
namespace Discord
namespace Discord
{
/// <summary> Specifies the connection state of a client. </summary>
public enum ConnectionState : byte
{
/// <summary> The client has disconnected from Discord. </summary>
Disconnected,
/// <summary> The client is connecting to Discord. </summary>
Connecting,
/// <summary> The client has established a connection to Discord. </summary>
Connected,
/// <summary> The client is disconnecting from Discord. </summary>
Disconnecting
}
}

View File

@@ -2,35 +2,143 @@ using System.Reflection;
namespace Discord
{
/// <summary>
/// Defines various behaviors of Discord.Net.
/// </summary>
public class DiscordConfig
{
/// <summary>
/// Returns the API version Discord.Net uses.
/// </summary>
/// <returns>
/// An <see cref="int"/> representing the API version that Discord.Net uses to communicate with Discord.
/// <para>A list of available API version can be seen on the official
/// <see href="https://discordapp.com/developers/docs/reference#api-versioning">Discord API documentation</see>
/// .</para>
/// </returns>
public const int APIVersion = 6;
/// <summary>
/// Returns the Voice API version Discord.Net uses.
/// </summary>
/// <returns>
/// An <see cref="int"/> representing the API version that Discord.Net uses to communicate with Discord's
/// voice server.
/// </returns>
public const int VoiceAPIVersion = 3;
/// <summary>
/// Gets the Discord.Net version, including the build number.
/// </summary>
/// <returns>
/// A string containing the detailed version information, including its build number; <c>Unknown</c> when
/// the version fails to be fetched.
/// </returns>
public static string Version { get; } =
typeof(DiscordConfig).GetTypeInfo().Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion ??
typeof(DiscordConfig).GetTypeInfo().Assembly.GetName().Version.ToString(3) ??
"Unknown";
/// <summary>
/// Gets the user agent that Discord.Net uses in its clients.
/// </summary>
/// <returns>
/// The user agent used in each Discord.Net request.
/// </returns>
public static string UserAgent { get; } = $"DiscordBot (https://github.com/RogueException/Discord.Net, v{Version})";
/// <summary>
/// Returns the base Discord API URL.
/// </summary>
/// <returns>
/// The Discord API URL using <see cref="APIVersion"/>.
/// </returns>
public static readonly string APIUrl = $"https://discordapp.com/api/v{APIVersion}/";
/// <summary>
/// Returns the base Discord CDN URL.
/// </summary>
/// <returns>
/// The base Discord Content Delivery Network (CDN) URL.
/// </returns>
public const string CDNUrl = "https://cdn.discordapp.com/";
/// <summary>
/// Returns the base Discord invite URL.
/// </summary>
/// <returns>
/// The base Discord invite URL.
/// </returns>
public const string InviteUrl = "https://discord.gg/";
/// <summary>
/// Returns the default timeout for requests.
/// </summary>
/// <returns>
/// The amount of time it takes in milliseconds before a request is timed out.
/// </returns>
public const int DefaultRequestTimeout = 15000;
/// <summary>
/// Returns the max length for a Discord message.
/// </summary>
/// <returns>
/// The maximum length of a message allowed by Discord.
/// </returns>
public const int MaxMessageSize = 2000;
/// <summary>
/// Returns the max messages allowed to be in a request.
/// </summary>
/// <returns>
/// The maximum number of messages that can be gotten per-batch.
/// </returns>
public const int MaxMessagesPerBatch = 100;
/// <summary>
/// Returns the max users allowed to be in a request.
/// </summary>
/// <returns>
/// The maximum number of users that can be gotten per-batch.
/// </returns>
public const int MaxUsersPerBatch = 1000;
/// <summary>
/// Returns the max guilds allowed to be in a request.
/// </summary>
/// <returns>
/// The maximum number of guilds that can be gotten per-batch.
/// </returns>
public const int MaxGuildsPerBatch = 100;
/// <summary>
/// Returns the max user reactions allowed to be in a request.
/// </summary>
/// <returns>
/// The maximum number of user reactions that can be gotten per-batch.
/// </returns>
public const int MaxUserReactionsPerBatch = 100;
/// <summary>
/// Returns the max audit log entries allowed to be in a request.
/// </summary>
/// <returns>
/// The maximum number of audit log entries that can be gotten per-batch.
/// </returns>
public const int MaxAuditLogEntriesPerBatch = 100;
/// <summary> Gets or sets how a request should act in the case of an error, by default. </summary>
/// <summary>
/// Gets or sets how a request should act in the case of an error, by default.
/// </summary>
/// <returns>
/// The currently set <see cref="RetryMode"/>.
/// </returns>
public RetryMode DefaultRetryMode { get; set; } = RetryMode.AlwaysRetry;
/// <summary> Gets or sets the minimum log level severity that will be sent to the Log event. </summary>
/// <summary>
/// Gets or sets the minimum log level severity that will be sent to the Log event.
/// </summary>
/// <returns>
/// The currently set <see cref="LogSeverity"/> for logging level.
/// </returns>
public LogSeverity LogLevel { get; set; } = LogSeverity.Info;
/// <summary> Gets or sets whether the initial log entry should be printed. </summary>
/// <summary>
/// Gets or sets whether the initial log entry should be printed.
/// </summary>
/// <remarks>
/// If set to <c>true</c>, the library will attempt to print the current version of the library, as well as
/// the API version it uses on startup.
/// </remarks>
internal bool DisplayInitialLog { get; set; } = true;
}
}

View File

@@ -1,10 +1,25 @@
namespace Discord
namespace Discord
{
/// <summary>
/// Specifies a Discord user's activity type.
/// </summary>
public enum ActivityType
{
/// <summary>
/// The user is playing a game.
/// </summary>
Playing = 0,
/// <summary>
/// The user is streaming online.
/// </summary>
Streaming = 1,
/// <summary>
/// The user is listening to a song.
/// </summary>
Listening = 2,
/// <summary>
/// The user is watching a media.
/// </summary>
Watching = 3
}
}

View File

@@ -2,19 +2,30 @@ using System.Diagnostics;
namespace Discord
{
/// <summary>
/// A user's game status.
/// </summary>
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class Game : IActivity
{
/// <inheritdoc/>
public string Name { get; internal set; }
/// <inheritdoc/>
public ActivityType Type { get; internal set; }
internal Game() { }
/// <summary>
/// Creates a <see cref="Game"/> with the provided <paramref name="name"/> and <see cref="ActivityType"/>.
/// </summary>
/// <param name="name">The name of the game.</param>
/// <param name="type">The type of activity. Default is <see cref="Discord.ActivityType.Playing"/>.</param>
public Game(string name, ActivityType type = ActivityType.Playing)
{
Name = name;
Type = type;
}
/// <summary> Returns the name of the <see cref="Game"/>. </summary>
public override string ToString() => Name;
private string DebuggerDisplay => Name;
}

View File

@@ -1,14 +1,37 @@
namespace Discord
{
/// <summary>
/// An asset for a <see cref="RichGame" /> object containing the text and image.
/// </summary>
public class GameAsset
{
internal GameAsset() { }
internal ulong? ApplicationId { get; set; }
/// <summary>
/// Gets the description of the asset.
/// </summary>
/// <returns>
/// A string containing the description of the asset.
/// </returns>
public string Text { get; internal set; }
/// <summary>
/// Gets the image ID of the asset.
/// </summary>
/// <returns>
/// A string containing the unique image identifier of the asset.
/// </returns>
public string ImageId { get; internal set; }
/// <summary>
/// Returns the image URL of the asset.
/// </summary>
/// <param name="size">The size of the image to return in. This can be any power of two between 16 and 2048.</param>
/// <param name="format">The format to return.</param>
/// <returns>
/// A string pointing to the image URL of the asset; <c>null</c> when the application ID does not exist.
/// </returns>
public string GetImageUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128)
=> ApplicationId.HasValue ? CDN.GetRichAssetUrl(ApplicationId.Value, ImageId, size, format) : null;
}

View File

@@ -1,11 +1,26 @@
namespace Discord
{
/// <summary>
/// Party information for a <see cref="RichGame" /> object.
/// </summary>
public class GameParty
{
internal GameParty() { }
/// <summary>
/// Gets the ID of the party.
/// </summary>
/// <returns>
/// A string containing the unique identifier of the party.
/// </returns>
public string Id { get; internal set; }
public long Members { get; internal set; }
/// <summary>
/// Gets the party's current and maximum size.
/// </summary>
/// <returns>
/// A <see cref="long"/> representing the capacity of the party.
/// </returns>
public long Capacity { get; internal set; }
}
}

View File

@@ -1,9 +1,21 @@
namespace Discord
namespace Discord
{
/// <summary>
/// Party secret for a <see cref="RichGame" /> object.
/// </summary>
public class GameSecrets
{
/// <summary>
/// Gets the secret for a specific instanced match.
/// </summary>
public string Match { get; }
/// <summary>
/// Gets the secret for joining a party.
/// </summary>
public string Join { get; }
/// <summary>
/// Gets the secret for spectating a game.
/// </summary>
public string Spectate { get; }
internal GameSecrets(string match, string join, string spectate)
@@ -13,4 +25,4 @@
Spectate = spectate;
}
}
}
}

View File

@@ -1,10 +1,19 @@
using System;
using System;
namespace Discord
{
/// <summary>
/// Timestamps for a <see cref="RichGame" /> object.
/// </summary>
public class GameTimestamps
{
/// <summary>
/// Gets when the activity started.
/// </summary>
public DateTimeOffset? Start { get; }
/// <summary>
/// Gets when the activity ends.
/// </summary>
public DateTimeOffset? End { get; }
internal GameTimestamps(DateTimeOffset? start, DateTimeOffset? end)
@@ -13,4 +22,4 @@ namespace Discord
End = end;
}
}
}
}

View File

@@ -1,8 +1,23 @@
namespace Discord
namespace Discord
{
/// <summary>
/// A user's activity status, typically a <see cref="Game"/>.
/// </summary>
public interface IActivity
{
/// <summary>
/// Gets the name of the activity.
/// </summary>
/// <returns>
/// A string containing the name of the activity that the user is doing.
/// </returns>
string Name { get; }
/// <summary>
/// Gets the type of the activity.
/// </summary>
/// <returns>
/// The type of activity.
/// </returns>
ActivityType Type { get; }
}
}

View File

@@ -2,20 +2,50 @@ using System.Diagnostics;
namespace Discord
{
/// <summary>
/// A user's Rich Presence status.
/// </summary>
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class RichGame : Game
{
internal RichGame() { }
/// <summary>
/// Gets what the player is currently doing.
/// </summary>
public string Details { get; internal set; }
/// <summary>
/// Gets the user's current party status.
/// </summary>
public string State { get; internal set; }
/// <summary>
/// Gets the application ID for the game.
/// </summary>
public ulong ApplicationId { get; internal set; }
/// <summary>
/// Gets the small image for the presence and their hover texts.
/// </summary>
public GameAsset SmallAsset { get; internal set; }
/// <summary>
/// Gets the large image for the presence and their hover texts.
/// </summary>
public GameAsset LargeAsset { get; internal set; }
/// <summary>
/// Gets the information for the current party of the player.
/// </summary>
public GameParty Party { get; internal set; }
/// <summary>
/// Gets the secrets for Rich Presence joining and spectating.
/// </summary>
public GameSecrets Secrets { get; internal set; }
/// <summary>
/// Gets the timestamps for start and/or end of the game.
/// </summary>
public GameTimestamps Timestamps { get; internal set; }
/// <summary>
/// Returns the name of the Rich Presence.
/// </summary>
public override string ToString() => Name;
private string DebuggerDisplay => $"{Name} (Rich)";
}

View File

@@ -4,22 +4,85 @@ using System.Diagnostics;
namespace Discord
{
/// <summary>
/// A user's activity for listening to a song on Spotify.
/// </summary>
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class SpotifyGame : Game
{
/// <summary>
/// Gets the song's artist(s).
/// </summary>
/// <returns>
/// A collection of string containing all artists featured in the track (e.g. <c>Avicii</c>; <c>Rita Ora</c>).
/// </returns>
public IReadOnlyCollection<string> Artists { get; internal set; }
/// <summary>
/// Gets the Spotify album title of the song.
/// </summary>
/// <returns>
/// A string containing the name of the album (e.g. <c>AVĪCI (01)</c>).
/// </returns>
public string AlbumTitle { get; internal set; }
/// <summary>
/// Gets the track title of the song.
/// </summary>
/// <returns>
/// A string containing the name of the song (e.g. <c>Lonely Together (feat. Rita Ora)</c>).
/// </returns>
public string TrackTitle { get; internal set; }
/// <summary>
/// Gets the duration of the song.
/// </summary>
/// <returns>
/// A <see cref="TimeSpan"/> containing the duration of the song.
/// </returns>
public TimeSpan? Duration { get; internal set; }
/// <summary>
/// Gets the track ID of the song.
/// </summary>
/// <returns>
/// A string containing the Spotify ID of the track (e.g. <c>7DoN0sCGIT9IcLrtBDm4f0</c>).
/// </returns>
public string TrackId { get; internal set; }
/// <summary>
/// Gets the session ID of the song.
/// </summary>
/// <remarks>
/// The purpose of this property is currently unknown.
/// </remarks>
/// <returns>
/// A string containing the session ID.
/// </returns>
public string SessionId { get; internal set; }
/// <summary>
/// Gets the URL of the album art.
/// </summary>
/// <returns>
/// A URL pointing to the album art of the track (e.g.
/// <c>https://i.scdn.co/image/ba2fd8823d42802c2f8738db0b33a4597f2f39e7</c>).
/// </returns>
public string AlbumArtUrl { get; internal set; }
/// <summary>
/// Gets the direct Spotify URL of the track.
/// </summary>
/// <returns>
/// A URL pointing directly to the track on Spotify. (e.g.
/// <c>https://open.spotify.com/track/7DoN0sCGIT9IcLrtBDm4f0</c>).
/// </returns>
public string TrackUrl { get; internal set; }
internal SpotifyGame() { }
/// <summary>
/// Gets the full information of the song.
/// </summary>
/// <returns>
/// A string containing the full information of the song (e.g.
/// <c>Avicii, Rita Ora - Lonely Together (feat. Rita Ora) (3:08)</c>
/// </returns>
public override string ToString() => $"{string.Join(", ", Artists)} - {TrackTitle} ({Duration})";
private string DebuggerDisplay => $"{Name} (Spotify)";
}

View File

@@ -1,12 +1,23 @@
using System.Diagnostics;
using System.Diagnostics;
namespace Discord
{
/// <summary>
/// A user's activity for streaming on services such as Twitch.
/// </summary>
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class StreamingGame : Game
{
/// <summary>
/// Gets the URL of the stream.
/// </summary>
public string Url { get; internal set; }
/// <summary>
/// Creates a new <see cref="StreamingGame" /> based on the <paramref name="name"/> on the stream URL.
/// </summary>
/// <param name="name">The name of the stream.</param>
/// <param name="url">The URL of the stream.</param>
public StreamingGame(string name, string url)
{
Name = name;
@@ -14,7 +25,10 @@ namespace Discord
Type = ActivityType.Streaming;
}
/// <summary>
/// Gets the name of the stream.
/// </summary>
public override string ToString() => Name;
private string DebuggerDisplay => $"{Name} ({Url})";
}
}
}

View File

@@ -1,50 +1,122 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Discord
{
/// <summary>
/// The action type within a <see cref="IAuditLogEntry"/>
/// Representing a type of action within an <see cref="IAuditLogEntry"/>.
/// </summary>
public enum ActionType
{
/// <summary>
/// this guild was updated.
/// </summary>
GuildUpdated = 1,
/// <summary>
/// A channel was created.
/// </summary>
ChannelCreated = 10,
/// <summary>
/// A channel was updated.
/// </summary>
ChannelUpdated = 11,
/// <summary>
/// A channel was deleted.
/// </summary>
ChannelDeleted = 12,
/// <summary>
/// A permission overwrite was created for a channel.
/// </summary>
OverwriteCreated = 13,
/// <summary>
/// A permission overwrite was updated for a channel.
/// </summary>
OverwriteUpdated = 14,
/// <summary>
/// A permission overwrite was deleted for a channel.
/// </summary>
OverwriteDeleted = 15,
/// <summary>
/// A user was kicked from this guild.
/// </summary>
Kick = 20,
/// <summary>
/// A prune took place in this guild.
/// </summary>
Prune = 21,
/// <summary>
/// A user banned another user from this guild.
/// </summary>
Ban = 22,
/// <summary>
/// A user unbanned another user from this guild.
/// </summary>
Unban = 23,
/// <summary>
/// A guild member whose information was updated.
/// </summary>
MemberUpdated = 24,
/// <summary>
/// A guild member's role collection was updated.
/// </summary>
MemberRoleUpdated = 25,
/// <summary>
/// A role was created in this guild.
/// </summary>
RoleCreated = 30,
/// <summary>
/// A role was updated in this guild.
/// </summary>
RoleUpdated = 31,
/// <summary>
/// A role was deleted from this guild.
/// </summary>
RoleDeleted = 32,
/// <summary>
/// An invite was created in this guild.
/// </summary>
InviteCreated = 40,
/// <summary>
/// An invite was updated in this guild.
/// </summary>
InviteUpdated = 41,
/// <summary>
/// An invite was deleted from this guild.
/// </summary>
InviteDeleted = 42,
/// <summary>
/// A Webhook was created in this guild.
/// </summary>
WebhookCreated = 50,
/// <summary>
/// A Webhook was updated in this guild.
/// </summary>
WebhookUpdated = 51,
/// <summary>
/// A Webhook was deleted from this guild.
/// </summary>
WebhookDeleted = 52,
/// <summary>
/// An emoji was created in this guild.
/// </summary>
EmojiCreated = 60,
/// <summary>
/// An emoji was updated in this guild.
/// </summary>
EmojiUpdated = 61,
/// <summary>
/// An emoji was deleted from this guild.
/// </summary>
EmojiDeleted = 62,
/// <summary>
/// A message was deleted from this guild.
/// </summary>
MessageDeleted = 72
}
}

View File

@@ -1,13 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Discord
{
/// <summary>
/// Represents data applied to an <see cref="IAuditLogEntry"/>
/// Represents data applied to an <see cref="IAuditLogEntry"/>.
/// </summary>
public interface IAuditLogData
{ }

View File

@@ -7,28 +7,40 @@ using System.Threading.Tasks;
namespace Discord
{
/// <summary>
/// Represents an entry in an audit log
/// Represents a generic audit log entry.
/// </summary>
public interface IAuditLogEntry : ISnowflakeEntity
{
/// <summary>
/// The action which occured to create this entry
/// Gets the action which occurred to create this entry.
/// </summary>
/// <returns>
/// The type of action for this audit log entry.
/// </returns>
ActionType Action { get; }
/// <summary>
/// The data for this entry. May be <see cref="null"/> if no data was available.
/// Gets the data for this entry.
/// </summary>
/// <returns>
/// An <see cref="IAuditLogData" /> for this audit log entry; <c>null</c> if no data is available.
/// </returns>
IAuditLogData Data { get; }
/// <summary>
/// The user responsible for causing the changes
/// Gets the user responsible for causing the changes.
/// </summary>
/// <returns>
/// A user object.
/// </returns>
IUser User { get; }
/// <summary>
/// The reason behind the change. May be <see cref="null"/> if no reason was provided.
/// Gets the reason behind the change.
/// </summary>
/// <returns>
/// A string containing the reason for the change; <c>null</c> if none is provided.
/// </returns>
string Reason { get; }
}
}

View File

@@ -1,8 +1,17 @@
namespace Discord
namespace Discord
{
/// <summary>
/// Specifies the cache mode that should be used.
/// </summary>
public enum CacheMode
{
/// <summary>
/// Allows the object to be downloaded if it does not exist in the current cache.
/// </summary>
AllowDownload,
/// <summary>
/// Only allows the object to be pulled from the existing cache.
/// </summary>
CacheOnly
}
}

View File

@@ -1,11 +1,17 @@
namespace Discord
namespace Discord
{
/// <summary> Defines the types of channels. </summary>
public enum ChannelType
{
/// <summary> The channel is a text channel. </summary>
Text = 0,
/// <summary> The channel is a Direct Message channel. </summary>
DM = 1,
/// <summary> The channel is a voice channel. </summary>
Voice = 2,
/// <summary> The channel is a group channel. </summary>
Group = 3,
/// <summary> The channel is a category channel. </summary>
Category = 4
}
}

View File

@@ -1,9 +1,21 @@
namespace Discord
namespace Discord
{
/// <summary>
/// Specifies the direction of where message(s) should be gotten from.
/// </summary>
public enum Direction
{
/// <summary>
/// The message(s) should be retrieved before a message.
/// </summary>
Before,
/// <summary>
/// The message(s) should be retrieved after a message.
/// </summary>
After,
/// <summary>
/// The message(s) should be retrieved around a message.
/// </summary>
Around
}
}

View File

@@ -1,33 +1,28 @@
namespace Discord
namespace Discord
{
/// <summary>
/// Modify an IGuildChannel with the specified changes.
/// Properties that are used to modify an <see cref="IGuildChannel" /> with the specified changes.
/// </summary>
/// <example>
/// <code language="c#">
/// await (Context.Channel as ITextChannel)?.ModifyAsync(x =>
/// {
/// x.Name = "do-not-enter";
/// });
/// </code>
/// </example>
/// <seealso cref="IGuildChannel.ModifyAsync"/>
public class GuildChannelProperties
{
/// <summary>
/// Set the channel to this name
/// Gets or sets the channel to this name.
/// </summary>
/// <remarks>
/// When modifying an ITextChannel, the Name MUST be alphanumeric with dashes.
/// It must match the following RegEx: [a-z0-9-_]{2,100}
/// This property defines the new name for this channel.
/// <note type="warning">
/// When modifying an <see cref="ITextChannel"/>, the <see cref="Name"/> must be alphanumeric with
/// dashes. It must match the RegEx <c>[a-z0-9-_]{2,100}</c>.
/// </note>
/// </remarks>
/// <exception cref="Net.HttpException">A BadRequest will be thrown if the name does not match the above RegEx.</exception>
public Optional<string> Name { get; set; }
/// <summary>
/// Move the channel to the following position. This is 0-based!
/// Moves the channel to the following position. This property is zero-based.
/// </summary>
public Optional<int> Position { get; set; }
/// <summary>
/// Sets the category for this channel
/// Gets or sets the category ID for this channel.
/// </summary>
public Optional<ulong?> CategoryId { get; set; }
}

View File

@@ -1,15 +1,31 @@
using Discord.Audio;
using System;
using System.Threading.Tasks;
namespace Discord
{
/// <summary>
/// Represents a generic audio channel.
/// </summary>
public interface IAudioChannel : IChannel
{
/// <summary> Connects to this audio channel. </summary>
/// <summary>
/// Connects to this audio channel.
/// </summary>
/// <param name="selfDeaf">Determines whether the client should deaf itself upon connection.</param>
/// <param name="selfMute">Determines whether the client should mute itself upon connection.</param>
/// <param name="external">Determines whether the audio client is an external one or not.</param>
/// <returns>
/// A task representing the asynchronous connection operation. The task result contains the
/// <see cref="IAudioClient"/> responsible for the connection.
/// </returns>
Task<IAudioClient> ConnectAsync(bool selfDeaf = false, bool selfMute = false, bool external = false);
/// <summary> Disconnects from this audio channel. </summary>
/// <summary>
/// Disconnects from this audio channel.
/// </summary>
/// <returns>
/// A task representing the asynchronous operation for disconnecting from the audio channel.
/// </returns>
Task DisconnectAsync();
}
}

View File

@@ -1,11 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Discord
{
/// <summary>
/// Represents a generic category channel.
/// </summary>
public interface ICategoryChannel : IGuildChannel
{
}

View File

@@ -1,17 +1,43 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Discord
{
/// <summary>
/// Represents a generic channel.
/// </summary>
public interface IChannel : ISnowflakeEntity
{
/// <summary> Gets the name of this channel. </summary>
/// <summary>
/// Gets the name of this channel.
/// </summary>
/// <returns>
/// A string containing the name of this channel.
/// </returns>
string Name { get; }
/// <summary> Gets a collection of all users in this channel. </summary>
/// <summary>
/// Gets a collection of all users in this channel.
/// </summary>
/// <param name="mode">The <see cref="CacheMode"/> that determines whether the object should be fetched from cache.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A paged collection containing a collection of users that can access this channel. Flattening the
/// paginated response into a collection of users with
/// <see cref="AsyncEnumerableExtensions.FlattenAsync{T}"/> is required if you wish to access the users.
/// </returns>
IAsyncEnumerable<IReadOnlyCollection<IUser>> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);
/// <summary> Gets a user in this channel with the provided id. </summary>
/// <summary>
/// Gets a user in this channel.
/// </summary>
/// <param name="id">The snowflake identifier of the user (e.g. <c>168693960628371456</c>).</param>
/// <param name="mode">The <see cref="CacheMode" /> that determines whether the object should be fetched from cache.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation. The task result contains a user object that
/// represents the found user; <c>null</c> if none is found.
/// </returns>
Task<IUser> GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);
}
}

View File

@@ -1,13 +1,27 @@
using System.Threading.Tasks;
using System.Threading.Tasks;
namespace Discord
{
/// <summary>
/// Represents a generic direct-message channel.
/// </summary>
public interface IDMChannel : IMessageChannel, IPrivateChannel
{
/// <summary> Gets the recipient of all messages in this channel. </summary>
/// <summary>
/// Gets the recipient of all messages in this channel.
/// </summary>
/// <returns>
/// A user object that represents the other user in this channel.
/// </returns>
IUser Recipient { get; }
/// <summary> Closes this private channel, removing it from your channel list. </summary>
/// <summary>
/// Closes this private channel, removing it from your channel list.
/// </summary>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous close operation.
/// </returns>
Task CloseAsync(RequestOptions options = null);
}
}
}

View File

@@ -1,10 +1,19 @@
using System.Threading.Tasks;
using System.Threading.Tasks;
namespace Discord
{
/// <summary>
/// Represents a generic private group channel.
/// </summary>
public interface IGroupChannel : IMessageChannel, IPrivateChannel, IAudioChannel
{
/// <summary> Leaves this group. </summary>
/// <summary>
/// Leaves this group.
/// </summary>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous leave operation.
/// </returns>
Task LeaveAsync(RequestOptions options = null);
}
}
}

View File

@@ -4,45 +4,172 @@ using System.Threading.Tasks;
namespace Discord
{
/// <summary>
/// Represents a generic guild channel.
/// </summary>
/// <seealso cref="ITextChannel"/>
/// <seealso cref="IVoiceChannel"/>
/// <seealso cref="ICategoryChannel"/>
public interface IGuildChannel : IChannel, IDeletable
{
/// <summary> Gets the position of this channel in the guild's channel list, relative to others of the same type. </summary>
/// <summary>
/// Gets the position of this channel.
/// </summary>
/// <returns>
/// An <see cref="int"/> representing the position of this channel in the guild's channel list relative to
/// others of the same type.
/// </returns>
int Position { get; }
/// <summary> Gets the guild this channel is a member of. </summary>
/// <summary>
/// Gets the guild associated with this channel.
/// </summary>
/// <returns>
/// A guild object that this channel belongs to.
/// </returns>
IGuild Guild { get; }
/// <summary> Gets the id of the guild this channel is a member of. </summary>
/// <summary>
/// Gets the guild ID associated with this channel.
/// </summary>
/// <returns>
/// An <see cref="ulong"/> representing the guild snowflake identifier for the guild that this channel
/// belongs to.
/// </returns>
ulong GuildId { get; }
/// <summary> Gets a collection of permission overwrites for this channel. </summary>
/// <summary>
/// Gets a collection of permission overwrites for this channel.
/// </summary>
/// <returns>
/// A collection of overwrites associated with this channel.
/// </returns>
IReadOnlyCollection<Overwrite> PermissionOverwrites { get; }
/// <summary> Creates a new invite to this channel. </summary>
/// <param name="maxAge"> The time (in seconds) until the invite expires. Set to null to never expire. </param>
/// <param name="maxUses"> The max amount of times this invite may be used. Set to null to have unlimited uses. </param>
/// <param name="isTemporary"> If true, a user accepting this invite will be kicked from the guild after closing their client. </param>
/// <summary>
/// Creates a new invite to this channel.
/// </summary>
/// <example>
/// The following example creates a new invite to this channel; the invite lasts for 12 hours and can only
/// be used 3 times throughout its lifespan.
/// <code language="cs">
/// await guildChannel.CreateInviteAsync(maxAge: 43200, maxUses: 3);
/// </code>
/// </example>
/// <param name="maxAge">The time (in seconds) until the invite expires. Set to <c>null</c> to never expire.</param>
/// <param name="maxUses">The max amount of times this invite may be used. Set to <c>null</c> to have unlimited uses.</param>
/// <param name="isTemporary">If <c>true</c>, the user accepting this invite will be kicked from the guild after closing their client.</param>
/// <param name="isUnique">If <c>true</c>, don't try to reuse a similar invite (useful for creating many unique one time use invites).</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous invite creation operation. The task result contains an invite
/// metadata object containing information for the created invite.
/// </returns>
Task<IInviteMetadata> CreateInviteAsync(int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null);
/// <summary> Returns a collection of all invites to this channel. </summary>
/// <summary>
/// Gets a collection of all invites to this channel.
/// </summary>
/// <example>
/// The following example gets all of the invites that have been created in this channel and selects the
/// most used invite.
/// <code language="cs">
/// var invites = await channel.GetInvitesAsync();
/// if (invites.Count == 0) return;
/// var invite = invites.OrderByDescending(x => x.Uses).FirstOrDefault();
/// </code>
/// </example>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation. The task result contains a read-only collection
/// of invite metadata that are created for this channel.
/// </returns>
Task<IReadOnlyCollection<IInviteMetadata>> GetInvitesAsync(RequestOptions options = null);
/// <summary> Modifies this guild channel. </summary>
/// <summary>
/// Modifies this guild channel.
/// </summary>
/// <param name="func">The delegate containing the properties to modify the channel with.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous modification operation.
/// </returns>
Task ModifyAsync(Action<GuildChannelProperties> func, RequestOptions options = null);
/// <summary> Gets the permission overwrite for a specific role, or null if one does not exist. </summary>
/// <summary>
/// Gets the permission overwrite for a specific role.
/// </summary>
/// <param name="role">The role to get the overwrite from.</param>
/// <returns>
/// An overwrite object for the targeted role; <c>null</c> if none is set.
/// </returns>
OverwritePermissions? GetPermissionOverwrite(IRole role);
/// <summary> Gets the permission overwrite for a specific user, or null if one does not exist. </summary>
/// <summary>
/// Gets the permission overwrite for a specific user.
/// </summary>
/// <param name="user">The user to get the overwrite from.</param>
/// <returns>
/// An overwrite object for the targeted user; <c>null</c> if none is set.
/// </returns>
OverwritePermissions? GetPermissionOverwrite(IUser user);
/// <summary> Removes the permission overwrite for the given role, if one exists. </summary>
/// <summary>
/// Removes the permission overwrite for the given role, if one exists.
/// </summary>
/// <param name="role">The role to remove the overwrite from.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task representing the asynchronous operation for removing the specified permissions from the channel.
/// </returns>
Task RemovePermissionOverwriteAsync(IRole role, RequestOptions options = null);
/// <summary> Removes the permission overwrite for the given user, if one exists. </summary>
/// <summary>
/// Removes the permission overwrite for the given user, if one exists.
/// </summary>
/// <param name="user">The user to remove the overwrite from.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task representing the asynchronous operation for removing the specified permissions from the channel.
/// </returns>
Task RemovePermissionOverwriteAsync(IUser user, RequestOptions options = null);
/// <summary> Adds or updates the permission overwrite for the given role. </summary>
/// <summary>
/// Adds or updates the permission overwrite for the given role.
/// </summary>
/// <param name="role">The role to add the overwrite to.</param>
/// <param name="permissions">The overwrite to add to the role.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task representing the asynchronous permission operation for adding the specified permissions to the channel.
/// </returns>
Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options = null);
/// <summary> Adds or updates the permission overwrite for the given user. </summary>
/// <summary>
/// Adds or updates the permission overwrite for the given user.
/// </summary>
/// <param name="user">The user to add the overwrite to.</param>
/// <param name="permissions">The overwrite to add to the user.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task representing the asynchronous permission operation for adding the specified permissions to the channel.
/// </returns>
Task AddPermissionOverwriteAsync(IUser user, OverwritePermissions permissions, RequestOptions options = null);
/// <summary> Gets a collection of all users in this channel. </summary>
/// <summary>
/// Gets a collection of users that are able to view the channel.
/// </summary>
/// <param name="mode">The <see cref="CacheMode" /> that determines whether the object should be fetched from cache.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A paged collection containing a collection of guild users that can access this channel. Flattening the
/// paginated response into a collection of users with
/// <see cref="AsyncEnumerableExtensions.FlattenAsync{T}"/> is required if you wish to access the users.
/// </returns>
new IAsyncEnumerable<IReadOnlyCollection<IGuildUser>> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);
/// <summary> Gets a user in this channel with the provided id.</summary>
/// <summary>
/// Gets a user in this channel.
/// </summary>
/// <param name="id">The snowflake identifier of the user.</param>
/// <param name="mode">The <see cref="CacheMode"/> that determines whether the object should be fetched from cache.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task representing the asynchronous get operation. The task result contains a guild user object that
/// represents the user; <c>null</c> if none is found.
/// </returns>
new Task<IGuildUser> GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);
}
}

View File

@@ -5,38 +5,276 @@ using System.Threading.Tasks;
namespace Discord
{
/// <summary>
/// Represents a generic channel that can send and receive messages.
/// </summary>
public interface IMessageChannel : IChannel
{
/// <summary> Sends a message to this message channel. </summary>
/// <summary>
/// Sends a message to this message channel.
/// </summary>
/// <example>
/// The following example sends a message with the current system time in RFC 1123 format to the channel and
/// deletes itself after 5 seconds.
/// <code language="cs">
/// var message = await channel.SendMessageAsync(DateTimeOffset.UtcNow.ToString("R"));
/// await Task.Delay(TimeSpan.FromSeconds(5))
/// .ContinueWith(x => message.DeleteAsync());
/// </code>
/// </example>
/// <param name="text">The message to be sent.</param>
/// <param name="isTTS">Determines whether the message should be read aloud by Discord or not.</param>
/// <param name="embed">The <see cref="Discord.EmbedType.Rich"/> <see cref="Embed"/> to be sent.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents an asynchronous send operation for delivering the message. The task result
/// contains the sent message.
/// </returns>
Task<IUserMessage> SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null);
/// <summary> Sends a file to this text channel, with an optional caption. </summary>
/// <summary>
/// Sends a file to this message channel with an optional caption.
/// </summary>
/// <example>
/// The following example uploads a local file called <c>wumpus.txt</c> along with the text
/// <c>good discord boi</c> to the channel.
/// <code language="cs">
/// await channel.SendFileAsync("wumpus.txt", "good discord boi");
/// </code>
///
/// The following example uploads a local image called <c>b1nzy.jpg</c> embedded inside a rich embed to the
/// channel.
/// <code language="cs">
/// await channel.SendFileAsync("b1nzy.jpg",
/// embed: new EmbedBuilder {ImageUrl = "attachment://b1nzy.jpg"}.Build());
/// </code>
/// </example>
/// <remarks>
/// This method sends a file as if you are uploading an attachment directly from your Discord client.
/// <note>
/// If you wish to upload an image and have it embedded in a <see cref="Discord.EmbedType.Rich"/> embed,
/// you may upload the file and refer to the file with "attachment://filename.ext" in the
/// <see cref="Discord.EmbedBuilder.ImageUrl"/>. See the example section for its usage.
/// </note>
/// </remarks>
/// <param name="filePath">The file path of the file.</param>
/// <param name="text">The message to be sent.</param>
/// <param name="isTTS">Whether the message should be read aloud by Discord or not.</param>
/// <param name="embed">The <see cref="Discord.EmbedType.Rich" /> <see cref="Embed" /> to be sent.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents an asynchronous send operation for delivering the message. The task result
/// contains the sent message.
/// </returns>
Task<IUserMessage> SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null);
/// <summary> Sends a file to this text channel, with an optional caption. </summary>
/// <summary>
/// Sends a file to this message channel with an optional caption.
/// </summary>
/// <example>
/// The following example uploads a streamed image that will be called <c>b1nzy.jpg</c> embedded inside a
/// rich embed to the channel.
/// <code language="cs">
/// await channel.SendFileAsync(b1nzyStream, "b1nzy.jpg",
/// embed: new EmbedBuilder {ImageUrl = "attachment://b1nzy.jpg"}.Build());
/// </code>
/// </example>
/// <remarks>
/// This method sends a file as if you are uploading an attachment directly from your Discord client.
/// <note>
/// If you wish to upload an image and have it embedded in a <see cref="Discord.EmbedType.Rich"/> embed,
/// you may upload the file and refer to the file with "attachment://filename.ext" in the
/// <see cref="Discord.EmbedBuilder.ImageUrl"/>. See the example section for its usage.
/// </note>
/// </remarks>
/// <param name="stream">The <see cref="Stream" /> of the file to be sent.</param>
/// <param name="filename">The name of the attachment.</param>
/// <param name="text">The message to be sent.</param>
/// <param name="isTTS">Whether the message should be read aloud by Discord or not.</param>
/// <param name="embed">The <see cref="Discord.EmbedType.Rich"/> <see cref="Embed"/> to be sent.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents an asynchronous send operation for delivering the message. The task result
/// contains the sent message.
/// </returns>
Task<IUserMessage> SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null);
/// <summary> Gets a message from this message channel with the given id, or null if not found. </summary>
/// <summary>
/// Gets a message from this message channel.
/// </summary>
/// <param name="id">The snowflake identifier of the message.</param>
/// <param name="mode">The <see cref="CacheMode"/> that determines whether the object should be fetched from cache.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents an asynchronous get operation for retrieving the message. The task result contains
/// the retrieved message; <c>null</c> if no message is found with the specified identifier.
/// </returns>
Task<IMessage> GetMessageAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);
/// <summary> Gets the last N messages from this message channel. </summary>
/// <summary>
/// Gets the last N messages from this message channel.
/// </summary>
/// <remarks>
/// <note type="important">
/// The returned collection is an asynchronous enumerable object; one must call
/// <see cref="AsyncEnumerableExtensions.FlattenAsync{T}"/> to access the individual messages as a
/// collection.
/// </note>
/// <note type="warning">
/// Do not fetch too many messages at once! This may cause unwanted preemptive rate limit or even actual
/// rate limit, causing your bot to freeze!
/// </note>
/// This method will attempt to fetch the number of messages specified under <paramref name="limit"/>. The
/// library will attempt to split up the requests according to your <paramref name="limit"/> and
/// <see cref="DiscordConfig.MaxMessagesPerBatch"/>. In other words, should the user request 500 messages,
/// and the <see cref="Discord.DiscordConfig.MaxMessagesPerBatch"/> constant is <c>100</c>, the request will
/// be split into 5 individual requests; thus returning 5 individual asynchronous responses, hence the need
/// of flattening.
/// </remarks>
/// <example>
/// The following example downloads 300 messages and gets messages that belong to the user
/// <c>53905483156684800</c>.
/// <code lang="cs">
/// var messages = await messageChannel.GetMessagesAsync(300).FlattenAsync();
/// var userMessages = messages.Where(x =&gt; x.Author.Id == 53905483156684800);
/// </code>
/// </example>
/// <param name="limit">The numbers of message to be gotten from.</param>
/// <param name="mode">The <see cref="CacheMode" /> that determines whether the object should be fetched from
/// cache.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// Paged collection of messages.
/// </returns>
IAsyncEnumerable<IReadOnlyCollection<IMessage>> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch,
CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);
/// <summary> Gets a collection of messages in this channel. </summary>
/// <summary>
/// Gets a collection of messages in this channel.
/// </summary>
/// <remarks>
/// <note type="important">
/// The returned collection is an asynchronous enumerable object; one must call
/// <see cref="AsyncEnumerableExtensions.FlattenAsync{T}"/> to access the individual messages as a
/// collection.
/// </note>
/// <note type="warning">
/// Do not fetch too many messages at once! This may cause unwanted preemptive rate limit or even actual
/// rate limit, causing your bot to freeze!
/// </note>
/// This method will attempt to fetch the number of messages specified under <paramref name="limit"/> around
/// the message <paramref name="fromMessageId"/> depending on the <paramref name="dir"/>. The library will
/// attempt to split up the requests according to your <paramref name="limit"/> and
/// <see cref="DiscordConfig.MaxMessagesPerBatch"/>. In other words, should the user request 500 messages,
/// and the <see cref="Discord.DiscordConfig.MaxMessagesPerBatch"/> constant is <c>100</c>, the request will
/// be split into 5 individual requests; thus returning 5 individual asynchronous responses, hence the need
/// of flattening.
/// </remarks>
/// <example>
/// The following example gets 5 message prior to the message identifier <c>442012544660537354</c>.
/// <code lang="cs">
/// var messages = await channel.GetMessagesAsync(442012544660537354, Direction.Before, 5).FlattenAsync();
/// </code>
/// </example>
/// <param name="fromMessageId">The ID of the starting message to get the messages from.</param>
/// <param name="dir">The direction of the messages to be gotten from.</param>
/// <param name="limit">The numbers of message to be gotten from.</param>
/// <param name="mode">The <see cref="CacheMode"/> that determines whether the object should be fetched from
/// cache.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// Paged collection of messages.
/// </returns>
IAsyncEnumerable<IReadOnlyCollection<IMessage>> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch,
CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);
/// <summary> Gets a collection of messages in this channel. </summary>
/// <summary>
/// Gets a collection of messages in this channel.
/// </summary>
/// <remarks>
/// <note type="important">
/// The returned collection is an asynchronous enumerable object; one must call
/// <see cref="AsyncEnumerableExtensions.FlattenAsync{T}"/> to access the individual messages as a
/// collection.
/// </note>
/// <note type="warning">
/// Do not fetch too many messages at once! This may cause unwanted preemptive rate limit or even actual
/// rate limit, causing your bot to freeze!
/// </note>
/// This method will attempt to fetch the number of messages specified under <paramref name="limit"/> around
/// the message <paramref name="fromMessage"/> depending on the <paramref name="dir"/>. The library will
/// attempt to split up the requests according to your <paramref name="limit"/> and
/// <see cref="DiscordConfig.MaxMessagesPerBatch"/>. In other words, should the user request 500 messages,
/// and the <see cref="Discord.DiscordConfig.MaxMessagesPerBatch"/> constant is <c>100</c>, the request will
/// be split into 5 individual requests; thus returning 5 individual asynchronous responses, hence the need
/// of flattening.
/// </remarks>
/// <example>
/// The following example gets 5 message prior to a specific message, <c>oldMessage</c>.
/// <code lang="cs">
/// var messages = await channel.GetMessagesAsync(oldMessage, Direction.Before, 5).FlattenAsync();
/// </code>
/// </example>
/// <param name="fromMessage">The starting message to get the messages from.</param>
/// <param name="dir">The direction of the messages to be gotten from.</param>
/// <param name="limit">The numbers of message to be gotten from.</param>
/// <param name="mode">The <see cref="CacheMode"/> that determines whether the object should be fetched from
/// cache.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// Paged collection of messages.
/// </returns>
IAsyncEnumerable<IReadOnlyCollection<IMessage>> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch,
CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);
/// <summary> Gets a collection of pinned messages in this channel. </summary>
/// <summary>
/// Gets a collection of pinned messages in this channel.
/// </summary>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation for retrieving pinned messages in this channel.
/// The task result contains a collection of messages found in the pinned messages.
/// </returns>
Task<IReadOnlyCollection<IMessage>> GetPinnedMessagesAsync(RequestOptions options = null);
/// <summary> Deletes a message based on the message ID in this channel. </summary>
/// <summary>
/// Deletes a message.
/// </summary>
/// <param name="messageId">The snowflake identifier of the message that would be removed.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous removal operation.
/// </returns>
Task DeleteMessageAsync(ulong messageId, RequestOptions options = null);
/// <summary> Deletes a message based on the provided message in this channel. </summary>
/// <param name="message">The message that would be removed.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous removal operation.
/// </returns>
Task DeleteMessageAsync(IMessage message, RequestOptions options = null);
/// <summary> Broadcasts the "user is typing" message to all users in this channel, lasting 10 seconds. </summary>
/// <summary>
/// Broadcasts the "user is typing" message to all users in this channel, lasting 10 seconds.
/// </summary>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous operation that triggers the broadcast.
/// </returns>
Task TriggerTypingAsync(RequestOptions options = null);
/// <summary> Continuously broadcasts the "user is typing" message to all users in this channel until the returned object is disposed. </summary>
/// <summary>
/// Continuously broadcasts the "user is typing" message to all users in this channel until the returned
/// object is disposed.
/// </summary>
/// <example>
/// The following example keeps the client in the typing state until <c>LongRunningAsync</c> has finished.
/// <code lang="cs">
/// using (messageChannel.EnterTypingState())
/// {
/// await LongRunningAsync();
/// }
/// </code>
/// </example>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A disposable object that, upon its disposal, will stop the client from broadcasting its typing state in
/// this channel.
/// </returns>
IDisposable EnterTypingState(RequestOptions options = null);
}
}

View File

@@ -3,14 +3,27 @@ using System.Threading.Tasks;
namespace Discord
{
/// <summary>
/// A type of guild channel that can be nested within a category.
/// Contains a CategoryId that is set to the parent category, if it is set.
/// Represents a type of guild channel that can be nested within a category.
/// </summary>
public interface INestedChannel : IGuildChannel
{
/// <summary> Gets the parentid (category) of this channel in the guild's channel list. </summary>
/// <summary>
/// Gets the parent (category) ID of this channel in the guild's channel list.
/// </summary>
/// <returns>
/// A <see cref="ulong"/> representing the snowflake identifier of the parent of this channel;
/// <c>null</c> if none is set.
/// </returns>
ulong? CategoryId { get; }
/// <summary> Gets the parent channel (category) of this channel, if it is set. If unset, returns null.</summary>
/// <summary>
/// Gets the parent (category) channel of this channel.
/// </summary>
/// <param name="mode">The <see cref="CacheMode"/> that determines whether the object should be fetched from cache.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation. The task result contains the category channel
/// representing the parent of this channel; <c>null</c> if none is set.
/// </returns>
Task<ICategoryChannel> GetCategoryAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);
}
}

View File

@@ -1,9 +1,18 @@
using System.Collections.Generic;
using System.Collections.Generic;
namespace Discord
{
/// <summary>
/// Represents a generic channel that is private to select recipients.
/// </summary>
public interface IPrivateChannel : IChannel
{
/// <summary>
/// Gets the users that can access this channel.
/// </summary>
/// <returns>
/// A read-only collection of users that can access this channel.
/// </returns>
IReadOnlyCollection<IUser> Recipients { get; }
}
}

View File

@@ -5,30 +5,114 @@ using System.Threading.Tasks;
namespace Discord
{
/// <summary>
/// Represents a generic channel in a guild that can send and receive messages.
/// </summary>
public interface ITextChannel : IMessageChannel, IMentionable, INestedChannel
{
/// <summary> Checks if the channel is NSFW. </summary>
/// <summary>
/// Gets a value that indicates whether the channel is NSFW.
/// </summary>
/// <returns>
/// <c>true</c> if the channel has the NSFW flag enabled; otherwise <c>false</c>.
/// </returns>
bool IsNsfw { get; }
/// <summary> Gets the current topic for this text channel. </summary>
/// <summary>
/// Gets the current topic for this text channel.
/// </summary>
/// <returns>
/// A string representing the topic set in the channel; <c>null</c> if none is set.
/// </returns>
string Topic { get; }
///<summary> Gets the current slow-mode delay for this channel. 0 if disabled. </summary>
/// <summary>
/// Gets the current slow-mode delay for this channel.
/// </summary>
/// <returns>
/// An <see cref="Int32"/> representing the time in seconds required before the user can send another
/// message; <c>0</c> if disabled.
/// </returns>
int SlowModeInterval { get; }
/// <summary> Bulk deletes multiple messages. </summary>
/// <summary>
/// Bulk-deletes multiple messages.
/// </summary>
/// <example>
/// The following example gets 250 messages from the channel and deletes them.
/// <code lang="cs">
/// var messages = await textChannel.GetMessagesAsync(250).FlattenAsync();
/// await textChannel.DeleteMessagesAsync(messages);
/// </code>
/// </example>
/// <remarks>
/// This method attempts to remove the messages specified in bulk.
/// <note type="important">
/// Due to the limitation set by Discord, this method can only remove messages that are posted within 14 days!
/// </note>
/// </remarks>
/// <param name="messages">The messages to be bulk-deleted.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous bulk-removal operation.
/// </returns>
Task DeleteMessagesAsync(IEnumerable<IMessage> messages, RequestOptions options = null);
/// <summary> Bulk deletes multiple messages. </summary>
/// <summary>
/// Bulk-deletes multiple messages.
/// </summary>
/// <remarks>
/// This method attempts to remove the messages specified in bulk.
/// <note type="important">
/// Due to the limitation set by Discord, this method can only remove messages that are posted within 14 days!
/// </note>
/// </remarks>
/// <param name="messageIds">The snowflake identifier of the messages to be bulk-deleted.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous bulk-removal operation.
/// </returns>
Task DeleteMessagesAsync(IEnumerable<ulong> messageIds, RequestOptions options = null);
/// <summary> Modifies this text channel. </summary>
/// <summary>
/// Modifies this text channel.
/// </summary>
/// <param name="func">The delegate containing the properties to modify the channel with.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous modification operation.
/// </returns>
/// <seealso cref="TextChannelProperties"/>
Task ModifyAsync(Action<TextChannelProperties> func, RequestOptions options = null);
/// <summary> Creates a webhook in this text channel. </summary>
/// <summary>
/// Creates a webhook in this text channel.
/// </summary>
/// <param name="name">The name of the webhook.</param>
/// <param name="avatar">The avatar of the webhook.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous creation operation. The task result contains the newly created
/// webhook.
/// </returns>
Task<IWebhook> CreateWebhookAsync(string name, Stream avatar = null, RequestOptions options = null);
/// <summary> Gets the webhook in this text channel with the provided id, or null if not found. </summary>
/// <summary>
/// Gets a webhook available in this text channel.
/// </summary>
/// <param name="id">The identifier of the webhook.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation. The task result contains a webhook associated
/// with the identifier; <c>null</c> if the webhook is not found.
/// </returns>
Task<IWebhook> GetWebhookAsync(ulong id, RequestOptions options = null);
/// <summary> Gets the webhooks for this text channel. </summary>
/// <summary>
/// Gets the webhooks available in this text channel.
/// </summary>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous get operation. The task result contains a read-only collection
/// of webhooks that is available in this channel.
/// </returns>
Task<IReadOnlyCollection<IWebhook>> GetWebhooksAsync(RequestOptions options = null);
}
}

View File

@@ -3,14 +3,37 @@ using System.Threading.Tasks;
namespace Discord
{
/// <summary>
/// Represents a generic voice channel in a guild.
/// </summary>
public interface IVoiceChannel : INestedChannel, IAudioChannel
{
/// <summary> Gets the bitrate, in bits per second, clients in this voice channel are requested to use. </summary>
/// <summary>
/// Gets the bit-rate that the clients in this voice channel are requested to use.
/// </summary>
/// <returns>
/// An <see cref="int"/> representing the bit-rate (bps) that this voice channel defines and requests the
/// client(s) to use.
/// </returns>
int Bitrate { get; }
/// <summary> Gets the max amount of users allowed to be connected to this channel at one time. </summary>
/// <summary>
/// Gets the max number of users allowed to be connected to this channel at once.
/// </summary>
/// <returns>
/// An <see cref="int"/> representing the maximum number of users that are allowed to be connected to this
/// channel at once; <c>null</c> if a limit is not set.
/// </returns>
int? UserLimit { get; }
/// <summary> Modifies this voice channel. </summary>
/// <summary>
/// Modifies this voice channel.
/// </summary>
/// <param name="func">The properties to modify the channel with.</param>
/// <param name="options">The options to be used when sending the request.</param>
/// <returns>
/// A task that represents the asynchronous modification operation.
/// </returns>
/// <seealso cref="VoiceChannelProperties"/>
Task ModifyAsync(Action<VoiceChannelProperties> func, RequestOptions options = null);
}
}

View File

@@ -1,12 +1,28 @@
namespace Discord
namespace Discord
{
/// <summary>
/// Provides properties that are used to reorder an <see cref="IGuildChannel"/>.
/// </summary>
public class ReorderChannelProperties
{
/// <summary>The id of the channel to apply this position to.</summary>
/// <summary>
/// Gets the ID of the channel to apply this position to.
/// </summary>
/// <returns>
/// A <see cref="ulong"/> representing the snowflake identifier of this channel.
/// </returns>
public ulong Id { get; }
/// <summary>The new zero-based position of this channel. </summary>
/// <summary>
/// Gets the new zero-based position of this channel.
/// </summary>
/// <returns>
/// An <see cref="int"/> representing the new position of this channel.
/// </returns>
public int Position { get; }
/// <summary> Initializes a new instance of the <see cref="ReorderChannelProperties"/> class used to reorder a channel. </summary>
/// <param name="id"> Sets the ID of the channel to apply this position to. </param>
/// <param name="position"> Sets the new zero-based position of this channel. </param>
public ReorderChannelProperties(ulong id, int position)
{
Id = id;

View File

@@ -1,27 +1,29 @@
using System;
namespace Discord
{
/// <inheritdoc />
/// <summary>
/// Provides properties that are used to modify an <see cref="ITextChannel"/> with the specified changes.
/// </summary>
/// <seealso cref="ITextChannel.ModifyAsync(System.Action{TextChannelProperties}, RequestOptions)"/>
public class TextChannelProperties : GuildChannelProperties
{
/// <summary>
/// What the topic of the channel should be set to.
/// Gets or sets the topic of the channel.
/// </summary>
public Optional<string> Topic { get; set; }
/// <summary>
/// Should this channel be flagged as NSFW?
/// Gets or sets whether this channel should be flagged as NSFW.
/// </summary>
public Optional<bool> IsNsfw { get; set; }
/// <summary>
/// What the slow-mode ratelimit for this channel should be set to; 0 will disable slow-mode.
/// Gets or sets the slow-mode ratelimit in seconds for this channel.
/// </summary>
/// <remarks>
/// This value must fall within [0, 120]
///
/// Users with <see cref="ChannelPermission.ManageMessages"/> will be exempt from slow-mode.
/// Setting this value to <c>0</c> will disable slow-mode for this channel.
/// <note>
/// Users with <see cref="Discord.ChannelPermission.ManageMessages" /> will be exempt from slow-mode.
/// </note>
/// </remarks>
/// <exception cref="ArgumentOutOfRangeException">Throws ArgummentOutOfRange if the value does not fall within [0, 120]</exception>
/// <exception cref="ArgumentOutOfRangeException">Thrown if the value does not fall within [0, 120].</exception>
public Optional<int> SlowModeInterval { get; set; }
}
}

View File

@@ -1,14 +1,16 @@
namespace Discord
namespace Discord
{
/// <inheritdoc />
/// <summary>
/// Provides properties that are used to modify an <see cref="IVoiceChannel" /> with the specified changes.
/// </summary>
public class VoiceChannelProperties : GuildChannelProperties
{
/// <summary>
/// The bitrate of the voice connections in this channel. Must be greater than 8000
/// Gets or sets the bitrate of the voice connections in this channel. Must be greater than 8000.
/// </summary>
public Optional<int> Bitrate { get; set; }
/// <summary>
/// The maximum number of users that can be present in a channel.
/// Gets or sets the maximum number of users that can be present in a channel, or <c>null</c> if none.
/// </summary>
public Optional<int?> UserLimit { get; set; }
}

View File

@@ -1,28 +1,35 @@
namespace Discord
namespace Discord
{
/// <summary>
/// A unicode emoji
/// A Unicode emoji.
/// </summary>
public class Emoji : IEmote
{
// TODO: need to constrain this to unicode-only emojis somehow
// TODO: need to constrain this to Unicode-only emojis somehow
/// <summary>
/// The unicode representation of this emote.
/// </summary>
/// <inheritdoc />
public string Name { get; }
/// <summary>
/// Gets the Unicode representation of this emote.
/// </summary>
/// <returns>
/// A string that resolves to <see cref="Emoji.Name"/>.
/// </returns>
public override string ToString() => Name;
/// <summary>
/// Creates a unicode emoji.
/// Initializes a new <see cref="Emoji"/> class with the provided Unicode.
/// </summary>
/// <param name="unicode">The pure UTF-8 encoding of an emoji</param>
/// <param name="unicode">The pure UTF-8 encoding of an emoji.</param>
public Emoji(string unicode)
{
Name = unicode;
}
/// <summary>
/// Determines whether the specified emoji is equal to the current one.
/// </summary>
/// <param name="other">The object to compare with the current object.</param>
public override bool Equals(object other)
{
if (other == null) return false;
@@ -34,6 +41,7 @@
return string.Equals(Name, otherEmoji.Name);
}
/// <inheritdoc />
public override int GetHashCode() => Name.GetHashCode();
}
}

View File

@@ -1,26 +1,34 @@
using System;
using System.Globalization;
using System.Diagnostics;
namespace Discord
{
/// <summary>
/// A custom image-based emote
/// A custom image-based emote.
/// </summary>
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class Emote : IEmote, ISnowflakeEntity
{
/// <summary>
/// The display name (tooltip) of this emote
/// </summary>
/// <inheritdoc />
public string Name { get; }
/// <summary>
/// The ID of this emote
/// </summary>
/// <inheritdoc />
public ulong Id { get; }
/// <summary>
/// Is this emote animated?
/// Gets whether this emote is animated.
/// </summary>
/// <returns>
/// A boolean that determines whether or not this emote is an animated one.
/// </returns>
public bool Animated { get; }
/// <inheritdoc />
public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id);
/// <summary>
/// Gets the image URL of this emote.
/// </summary>
/// <returns>
/// A string that points to the URL of this emote.
/// </returns>
public string Url => CDN.GetEmojiUrl(Id, Animated);
internal Emote(ulong id, string name, bool animated)
@@ -30,6 +38,10 @@ namespace Discord
Animated = animated;
}
/// <summary>
/// Determines whether the specified emote is equal to the current emote.
/// </summary>
/// <param name="other">The object to compare with the current object.</param>
public override bool Equals(object other)
{
if (other == null) return false;
@@ -41,6 +53,7 @@ namespace Discord
return string.Equals(Name, otherEmote.Name) && Id == otherEmote.Id;
}
/// <inheritdoc />
public override int GetHashCode()
{
unchecked
@@ -49,18 +62,20 @@ namespace Discord
}
}
/// <summary>
/// Parse an Emote from its raw format
/// </summary>
/// <param name="text">The raw encoding of an emote; for example, &lt;:dab:277855270321782784&gt;</param>
/// <returns>An emote</returns>
/// <summary> Parses an <see cref="Emote"/> from its raw format. </summary>
/// <param name="text">The raw encoding of an emote (e.g. <c>&lt;:dab:277855270321782784&gt;</c>).</param>
/// <returns>An emote.</returns>
/// <exception cref="ArgumentException">Invalid emote format.</exception>
public static Emote Parse(string text)
{
if (TryParse(text, out Emote result))
return result;
throw new ArgumentException(message: "Invalid emote format", paramName: nameof(text));
throw new ArgumentException(message: "Invalid emote format.", paramName: nameof(text));
}
/// <summary> Tries to parse an <see cref="Emote"/> from its raw format. </summary>
/// <param name="text">The raw encoding of an emote; for example, &lt;:dab:277855270321782784&gt;.</param>
/// <param name="result">An emote.</param>
public static bool TryParse(string text, out Emote result)
{
result = null;
@@ -85,6 +100,12 @@ namespace Discord
}
private string DebuggerDisplay => $"{Name} ({Id})";
/// <summary>
/// Returns the raw representation of the emote.
/// </summary>
/// <returns>
/// A string representing the raw presentation of the emote (e.g. <c>&lt;:thonkang:282745590985523200&gt;</c>).
/// </returns>
public override string ToString() => $"<{(Animated ? "a" : "")}:{Name}:{Id}>";
}
}

View File

@@ -1,10 +1,20 @@
using System.Collections.Generic;
using System.Collections.Generic;
namespace Discord
{
/// <summary>
/// Provides properties that are used to modify an <see cref="Emote" /> with the specified changes.
/// </summary>
/// <seealso cref="IGuild.ModifyEmoteAsync"/>
public class EmoteProperties
{
/// <summary>
/// Gets or sets the name of the <see cref="Emote"/>.
/// </summary>
public Optional<string> Name { get; set; }
/// <summary>
/// Gets or sets the roles that can access this <see cref="Emote"/>.
/// </summary>
public Optional<IEnumerable<IRole>> Roles { get; set; }
}
}

View File

@@ -1,16 +1,34 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Diagnostics;
namespace Discord
{
/// <summary>
/// An image-based emote that is attached to a guild
/// An image-based emote that is attached to a guild.
/// </summary>
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class GuildEmote : Emote
{
/// <summary>
/// Gets whether this emoji is managed by an integration.
/// </summary>
/// <returns>
/// A boolean that determines whether or not this emote is managed by a Twitch integration.
/// </returns>
public bool IsManaged { get; }
/// <summary>
/// Gets whether this emoji must be wrapped in colons.
/// </summary>
/// <returns>
/// A boolean that determines whether or not this emote requires the use of colons in chat to be used.
/// </returns>
public bool RequireColons { get; }
/// <summary>
/// Gets the roles that are allowed to use this emoji.
/// </summary>
/// <returns>
/// A read-only list containing snowflake identifiers for roles that are allowed to use this emoji.
/// </returns>
public IReadOnlyList<ulong> RoleIds { get; }
internal GuildEmote(ulong id, string name, bool animated, bool isManaged, bool requireColons, IReadOnlyList<ulong> roleIds) : base(id, name, animated)
@@ -21,6 +39,12 @@ namespace Discord
}
private string DebuggerDisplay => $"{Name} ({Id})";
/// <summary>
/// Gets the raw representation of the emote.
/// </summary>
/// <returns>
/// A string representing the raw presentation of the emote (e.g. <c>&lt;:thonkang:282745590985523200&gt;</c>).
/// </returns>
public override string ToString() => $"<{(Animated ? "a" : "")}:{Name}:{Id}>";
}
}

View File

@@ -1,13 +1,16 @@
namespace Discord
namespace Discord
{
/// <summary>
/// A general container for any type of emote in a message.
/// Represents a general container for any type of emote in a message.
/// </summary>
public interface IEmote
{
/// <summary>
/// The display name or unicode representation of this emote
/// Gets the display name or Unicode representation of this emote.
/// </summary>
/// <returns>
/// A string representing the display name or the Unicode representation (e.g. <c>🤔</c>) of this emote.
/// </returns>
string Name { get; }
}
}

View File

@@ -1,10 +1,17 @@
namespace Discord
namespace Discord
{
/// <summary>
/// Specifies the default message notification behavior the guild uses.
/// </summary>
public enum DefaultMessageNotifications
{
/// <summary> By default, all messages will trigger notifications. </summary>
/// <summary>
/// By default, all messages will trigger notifications.
/// </summary>
AllMessages = 0,
/// <summary> By default, only mentions will trigger notifications. </summary>
/// <summary>
/// By default, only mentions will trigger notifications.
/// </summary>
MentionsOnly = 1
}
}

View File

@@ -1,20 +1,20 @@
namespace Discord
namespace Discord
{
/// <summary>
/// Modify the widget of an IGuild with the specified parameters
/// Provides properties that are used to modify the widget of an <see cref="IGuild" /> with the specified changes.
/// </summary>
public class GuildEmbedProperties
{
/// <summary>
/// Should the widget be enabled?
/// Sets whether the widget should be enabled.
/// </summary>
public Optional<bool> Enabled { get; set; }
/// <summary>
/// What channel should the invite place users in, if not null.
/// Sets the channel that the invite should place its users in, if not <c>null</c>.
/// </summary>
public Optional<IChannel> Channel { get; set; }
/// <summary>
/// What channel should the invite place users in, if not null.
/// Sets the channel the invite should place its users in, if not <c>null</c>.
/// </summary>
public Optional<ulong?> ChannelId { get; set; }
}

View File

@@ -1,9 +1,21 @@
namespace Discord
namespace Discord
{
/// <summary>
/// Provides properties used to modify an <see cref="IGuildIntegration" /> with the specified changes.
/// </summary>
public class GuildIntegrationProperties
{
/// <summary>
/// Gets or sets the behavior when an integration subscription lapses.
/// </summary>
public Optional<int> ExpireBehavior { get; set; }
/// <summary>
/// Gets or sets the period (in seconds) where the integration will ignore lapsed subscriptions.
/// </summary>
public Optional<int> ExpireGracePeriod { get; set; }
/// <summary>
/// Gets or sets whether emoticons should be synced for this integration.
/// </summary>
public Optional<bool> EnableEmoticons { get; set; }
}
}

Some files were not shown because too many files have changed in this diff Show More