diff --git a/.gitignore b/.gitignore index 45e5e009d..954362408 100644 --- a/.gitignore +++ b/.gitignore @@ -206,3 +206,6 @@ project.lock.json docs/api/\.manifest \.idea/ + +# Codealike UID +codealike.json \ No newline at end of file diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 000000000..b483339c4 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,16 @@ +# Instructions for Building Documentation + +The documentation for the Discord.Net library uses [DocFX][docfx-main]. [Instructions for installing this tool can be found here.][docfx-installing] + +1. Navigate to the root of the repository. +2. (Optional) If you intend to target a specific version, ensure that you +have the correct version checked out. +3. Build the library. Run `dotnet build` in the root of this repository. + Ensure that the build passes without errors. +4. Build the docs using `docfx .\docs\docfx.json`. Add the `--serve` parameter +to preview the site locally. Some elements of the page may appear incorrect +when not hosted by a server. + - Remarks: According to the docfx website, this tool does work on Linux under mono. + +[docfx-main]: https://dotnet.github.io/docfx/ +[docfx-installing]: https://dotnet.github.io/docfx/tutorial/docfx_getting_started.html diff --git a/docs/_overwrites/Commands/CommandException.Overwrite.md b/docs/_overwrites/Commands/CommandException.Overwrite.md new file mode 100644 index 000000000..166a011de --- /dev/null +++ b/docs/_overwrites/Commands/CommandException.Overwrite.md @@ -0,0 +1,31 @@ +--- +uid: Discord.Commands.CommandException +remarks: *content +--- + +This @System.Exception class is typically used when diagnosing +an error thrown during the execution of a command. You will find the +thrown exception passed into +[LogMessage.Exception](xref:Discord.LogMessage.Exception), which is +sent to your [CommandService.Log](xref:Discord.Commands.CommandService.Log) +event handler. + +--- +uid: Discord.Commands.CommandException +example: [*content] +--- + +You may use this information to handle runtime exceptions after +execution. Below is an example of how you may use this: + +```cs +public Task LogHandlerAsync(LogMessage logMessage) +{ + // Note that this casting method requires C#7 and up. + if (logMessage?.Exception is CommandException cmdEx) + { + Console.WriteLine($"{cmdEx.GetBaseException().GetType()} was thrown while executing {cmdEx.Command.Aliases.First()} in {cmdEx.Context.Channel} by {cmdEx.Context.User}."); + } + return Task.CompletedTask; +} +``` \ No newline at end of file diff --git a/docs/_overwrites/Commands/DontAutoLoadAttribute.Overwrite.md b/docs/_overwrites/Commands/DontAutoLoadAttribute.Overwrite.md new file mode 100644 index 000000000..d47980df7 --- /dev/null +++ b/docs/_overwrites/Commands/DontAutoLoadAttribute.Overwrite.md @@ -0,0 +1,22 @@ +--- +uid: Discord.Commands.DontAutoLoadAttribute +remarks: *content +--- + +The attribute can be applied to a public class that inherits +@Discord.Commands.ModuleBase. By applying this attribute, +@Discord.Commands.CommandService.AddModulesAsync* will not discover and +add the marked module to the CommandService. + +--- +uid: Discord.Commands.DontAutoLoadAttribute +example: [*content] +--- + +```cs +[DontAutoLoad] +public class MyModule : ModuleBase +{ + // ... +} +``` \ No newline at end of file diff --git a/docs/_overwrites/Commands/DontInjectAttribute.Overwrite.md b/docs/_overwrites/Commands/DontInjectAttribute.Overwrite.md new file mode 100644 index 000000000..950d2990c --- /dev/null +++ b/docs/_overwrites/Commands/DontInjectAttribute.Overwrite.md @@ -0,0 +1,27 @@ +--- +uid: Discord.Commands.DontInjectAttribute +remarks: *content +--- + +The attribute can be applied to a public settable property inside a +@Discord.Commands.ModuleBase based class. By applying this attribute, +the marked property will not be automatically injected of the +dependency. See @Guides.Commands.DI to learn more. + +--- +uid: Discord.Commands.DontInjectAttribute +example: [*content] +--- + +```cs +public class MyModule : ModuleBase +{ + [DontInject] + public MyService MyService { get; set; } + + public MyModule() + { + MyService = new MyService(); + } +} +``` \ No newline at end of file diff --git a/docs/_overwrites/Commands/ICommandContext.Inclusion.md b/docs/_overwrites/Commands/ICommandContext.Inclusion.md new file mode 100644 index 000000000..c99148db7 --- /dev/null +++ b/docs/_overwrites/Commands/ICommandContext.Inclusion.md @@ -0,0 +1,5 @@ +An example of how this class is used the command system can be seen +below: + +[!code[Sample module](../../guides/commands/samples/empty-module.cs)] +[!code[Command handler](../../guides/commands/samples/command_handler.cs)] \ No newline at end of file diff --git a/docs/_overwrites/Commands/ICommandContext.Overwrite.md b/docs/_overwrites/Commands/ICommandContext.Overwrite.md new file mode 100644 index 000000000..d9e50b46d --- /dev/null +++ b/docs/_overwrites/Commands/ICommandContext.Overwrite.md @@ -0,0 +1,27 @@ +--- +uid: Discord.Commands.ICommandContext +example: [*content] +--- + +[!include[Example Section](ICommandContext.Inclusion.md)] + +--- +uid: Discord.Commands.CommandContext +example: [*content] +--- + +[!include[Example Section](ICommandContext.Inclusion.md)] + +--- +uid: Discord.Commands.SocketCommandContext +example: [*content] +--- + +[!include[Example Section](ICommandContext.Inclusion.md)] + +--- +uid: Discord.Commands.ShardCommandContext +example: [*content] +--- + +[!include[Example Section](ICommandContext.Inclusion.md)] \ No newline at end of file diff --git a/docs/_overwrites/Commands/PreconditionAttribute.Overwrites.md b/docs/_overwrites/Commands/PreconditionAttribute.Overwrites.md new file mode 100644 index 000000000..8117b2c3f --- /dev/null +++ b/docs/_overwrites/Commands/PreconditionAttribute.Overwrites.md @@ -0,0 +1,107 @@ +--- +uid: Discord.Commands.PreconditionAttribute +seealso: + - linkId: Discord.Commands.ParameterPreconditionAttribute +remarks: *content +--- + +This precondition attribute can be applied on module-level or +method-level for a command. + +[!include[Additional Remarks](PreconditionAttribute.Remarks.Inclusion.md)] + +--- +uid: Discord.Commands.ParameterPreconditionAttribute +seealso: + - linkId: Discord.Commands.PreconditionAttribute +remarks: *content +--- + +This precondition attribute can be applied on parameter-level for a +command. + +[!include[Additional Remarks](PreconditionAttribute.Remarks.Inclusion.md)] + +--- +uid: Discord.Commands.PreconditionAttribute +example: [*content] +--- + +The following example creates a precondition to see if the user has +sufficient role required to access the command. + +```cs +public class RequireRoleAtribute : PreconditionAttribute +{ + private readonly ulong _roleId; + + public RequireRoleAtribute(ulong roleId) + { + _roleId = roleId; + } + + public override async Task CheckPermissionsAsync(ICommandContext context, + CommandInfo command, IServiceProvider services) + { + var guildUser = context.User as IGuildUser; + if (guildUser == null) + return PreconditionResult.FromError("This command cannot be executed outside of a guild."); + + var guild = guildUser.Guild; + if (guild.Roles.All(r => r.Id != _roleId)) + return PreconditionResult.FromError( + $"The guild does not have the role ({_roleId}) required to access this command."); + + return guildUser.RoleIds.Any(rId => rId == _roleId) + ? PreconditionResult.FromSuccess() + : PreconditionResult.FromError("You do not have the sufficient role required to access this command."); + } +} +``` + +--- +uid: Discord.Commands.ParameterPreconditionAttribute +example: [*content] +--- + +The following example creates a precondition on a parameter-level to +see if the targeted user has a lower hierarchy than the user who +executed the command. + +```cs +public class RequireHierarchyAttribute : ParameterPreconditionAttribute +{ + public override async Task CheckPermissionsAsync(ICommandContext context, + ParameterInfo parameter, object value, IServiceProvider services) + { + // Hierarchy is only available under the socket variant of the user. + if (!(context.User is SocketGuildUser guildUser)) + return PreconditionResult.FromError("This command cannot be used outside of a guild."); + + SocketGuildUser targetUser; + switch (value) + { + case SocketGuildUser targetGuildUser: + targetUser = targetGuildUser; + break; + case ulong userId: + targetUser = await context.Guild.GetUserAsync(userId).ConfigureAwait(false) as SocketGuildUser; + break; + default: + throw new ArgumentOutOfRangeException(); + } + + if (targetUser == null) + return PreconditionResult.FromError("Target user not found."); + + if (guildUser.Hierarchy < targetUser.Hierarchy) + return PreconditionResult.FromError("You cannot target anyone else whose roles are higher than yours."); + + var currentUser = await context.Guild.GetCurrentUserAsync().ConfigureAwait(false) as SocketGuildUser; + if (currentUser?.Hierarchy < targetUser.Hierarchy) + return PreconditionResult.FromError("The bot's role is lower than the targeted user."); + + return PreconditionResult.FromSuccess(); + } +} +``` \ No newline at end of file diff --git a/docs/_overwrites/Commands/PreconditionAttribute.Remarks.Inclusion.md b/docs/_overwrites/Commands/PreconditionAttribute.Remarks.Inclusion.md new file mode 100644 index 000000000..499cdb0ad --- /dev/null +++ b/docs/_overwrites/Commands/PreconditionAttribute.Remarks.Inclusion.md @@ -0,0 +1,6 @@ +A "precondidtion" in the command system is used to determine if a +condition is met before entering the command task. Using a +precondidtion may aid in keeping a well-organized command logic. + +The most common use case being whether a user has sufficient +permission to execute the command. \ No newline at end of file diff --git a/docs/_overwrites/Common/EmbedBuilder.Overwrites.md b/docs/_overwrites/Common/EmbedBuilder.Overwrites.md new file mode 100644 index 000000000..de7da8f5d --- /dev/null +++ b/docs/_overwrites/Common/EmbedBuilder.Overwrites.md @@ -0,0 +1,48 @@ +--- +uid: Discord.EmbedBuilder +seealso: + - linkId: Discord.EmbedFooterBuilder + - linkId: Discord.EmbedAuthorBuilder + - linkId: Discord.EmbedFieldBuilder +remarks: *content +--- + +This builder class is used to build an @Discord.Embed (rich embed) +object that will be ready to be sent via @Discord.IMessageChannel.SendMessageAsync* +after @Discord.EmbedBuilder.Build* is called. + +--- +uid: Discord.EmbedBuilder +example: [*content] +--- + +The example below builds an embed and sends it to the chat using the +command system. + +```cs +[Command("embed")] +public async Task SendRichEmbedAsync() +{ + var embed = new EmbedBuilder + { + // Embed property can be set within object initializer + Title = "Hello world!" + } + // Or with methods + .AddField("Field title", + "Field value. I also support [hyperlink markdown](https://example.com)!") + .WithAuthor(Context.Client.CurrentUser) + .WithFooter(footer => footer.Text = "I am a footer.") + .WithColor(Color.Blue) + .WithTitle("I overwrote \"Hello world!\"") + .WithDescription("I am a description.") + .WithUrl("https://example.com") + .WithCurrentTimestamp() + .Build(); + await ReplyAsync(embed: embed); +} +``` + +#### Result + +![Embed Example](images/embed-example.png) \ No newline at end of file diff --git a/docs/_overwrites/Common/EmbedObjectBuilder.Inclusion.md b/docs/_overwrites/Common/EmbedObjectBuilder.Inclusion.md new file mode 100644 index 000000000..eac0d9ca5 --- /dev/null +++ b/docs/_overwrites/Common/EmbedObjectBuilder.Inclusion.md @@ -0,0 +1,25 @@ +The example will build a rich embed with an author field, a footer +field, and 2 normal fields using an @Discord.EmbedBuilder: + +```cs +var exampleAuthor = new EmbedAuthorBuilder() + .WithName("I am a bot") + .WithIconUrl("https://discordapp.com/assets/e05ead6e6ebc08df9291738d0aa6986d.png"); +var exampleFooter = new EmbedFooterBuilder() + .WithText("I am a nice footer") + .WithIconUrl("https://discordapp.com/assets/28174a34e77bb5e5310ced9f95cb480b.png"); +var exampleField = new EmbedFieldBuilder() + .WithName("Title of Another Field") + .WithValue("I am an [example](https://example.com).") + .WithInline(true); +var otherField = new EmbedFieldBuilder() + .WithName("Title of a Field") + .WithValue("Notice how I'm inline with that other field next to me.") + .WithInline(true); +var embed = new EmbedBuilder() + .AddField(exampleField) + .AddField(otherField) + .WithAuthor(exampleAuthor) + .WithFooter(exampleFooter) + .Build(); +``` \ No newline at end of file diff --git a/docs/_overwrites/Common/EmbedObjectBuilder.Overwrites.md b/docs/_overwrites/Common/EmbedObjectBuilder.Overwrites.md new file mode 100644 index 000000000..c633c29b1 --- /dev/null +++ b/docs/_overwrites/Common/EmbedObjectBuilder.Overwrites.md @@ -0,0 +1,20 @@ +--- +uid: Discord.EmbedAuthorBuilder +example: [*content] +--- + +[!include[Embed Object Builder Sample](EmbedObjectBuilder.Inclusion.md)] + +--- +uid: Discord.EmbedFooterBuilder +example: [*content] +--- + +[!include[Embed Object Builder Sample](EmbedObjectBuilder.Inclusion.md)] + +--- +uid: Discord.EmbedFieldBuilder +example: [*content] +--- + +[!include[Embed Object Builder Sample](EmbedObjectBuilder.Inclusion.md)] \ No newline at end of file diff --git a/docs/_overwrites/Common/IEmote.Inclusion.md b/docs/_overwrites/Common/IEmote.Inclusion.md new file mode 100644 index 000000000..cf93c7eb5 --- /dev/null +++ b/docs/_overwrites/Common/IEmote.Inclusion.md @@ -0,0 +1,26 @@ +The sample below sends a message and adds an @Discord.Emoji and a custom +@Discord.Emote to the message. + +```cs +public async Task SendAndReactAsync(ISocketMessageChannel channel) +{ + var message = await channel.SendMessageAsync("I am a message."); + + // Creates a Unicode-based emoji based on the Unicode string. + // This is effectively the same as new Emoji("💕"). + var heartEmoji = new Emoji("\U0001f495"); + // Reacts to the message with the Emoji. + await message.AddReactionAsync(heartEmoji); + + // Parses a custom emote based on the provided Discord emote format. + // Please note that this does not guarantee the existence of + // the emote. + var emote = Emote.Parse("<:thonkang:282745590985523200>"); + // Reacts to the message with the Emote. + await message.AddReactionAsync(emote); +} +``` + +#### Result + +![React Example](images/react-example.png) \ No newline at end of file diff --git a/docs/_overwrites/Common/IEmote.Overwrites.md b/docs/_overwrites/Common/IEmote.Overwrites.md new file mode 100644 index 000000000..034533d1d --- /dev/null +++ b/docs/_overwrites/Common/IEmote.Overwrites.md @@ -0,0 +1,81 @@ +--- +uid: Discord.IEmote +seealso: + - linkId: Discord.Emote + - linkId: Discord.Emoji + - linkId: Discord.GuildEmote + - linkId: Discord.IUserMessage +remarks: *content +--- + +This interface is often used with reactions. It can represent an +unicode-based @Discord.Emoji, or a custom @Discord.Emote. + +--- +uid: Discord.Emote +seealso: + - linkId: Discord.IEmote + - linkId: Discord.GuildEmote + - linkId: Discord.Emoji + - linkId: Discord.IUserMessage +remarks: *content +--- + +> [!NOTE] +> A valid @Discord.Emote format is `<:emoteName:emoteId>`. This can be +> obtained by escaping with a `\` in front of the emote using the +> Discord chat client. + +This class represents a custom emoji. This type of emoji can be +created via the @Discord.Emote.Parse* or @Discord.Emote.TryParse* +method. + +--- +uid: Discord.Emoji +seealso: + - linkId: Discord.Emote + - linkId: Discord.GuildEmote + - linkId: Discord.Emoji + - linkId: Discord.IUserMessage +remarks: *content +--- + +> [!NOTE] +> A valid @Discord.Emoji format is Unicode-based. This means only +> something like `🙃` or `\U0001f643` would work, instead of +> `:upside_down:`. +> +> A Unicode-based emoji can be obtained by escaping with a `\` in +> front of the emote using the Discord chat client or by looking up on +> [Emojipedia](https://emojipedia.org). + +This class represents a standard Unicode-based emoji. This type of emoji +can be created by passing the Unicode into the constructor. + +--- +uid: Discord.IEmote +example: [*content] +--- + +[!include[Example Section](IEmote.Inclusion.md)] + +--- +uid: Discord.Emoji +example: [*content] +--- + +[!include[Example Section](IEmote.Inclusion.md)] + +--- +uid: Discord.Emote +example: [*content] +--- + +[!include[Example Section](IEmote.Inclusion.md)] + +--- +uid: Discord.GuildEmote +example: [*content] +--- + +[!include[Example Section](IEmote.Inclusion.md)] \ No newline at end of file diff --git a/docs/_overwrites/Common/OverrideTypeReaderAttribute.Overwrites.md b/docs/_overwrites/Common/OverrideTypeReaderAttribute.Overwrites.md new file mode 100644 index 000000000..29b547e49 --- /dev/null +++ b/docs/_overwrites/Common/OverrideTypeReaderAttribute.Overwrites.md @@ -0,0 +1,24 @@ +--- +uid: Discord.Commands.OverrideTypeReaderAttribute +remarks: *content +--- + +This attribute is used to override a command parameter's type reading +behaviour. This may be useful when you have multiple custom +@Discord.Commands.TypeReader and would like to specify one. + +--- +uid: Discord.Commands.OverrideTypeReaderAttribute +examples: [*content] +--- + +The following example will override the @Discord.Commands.TypeReader +of @Discord.IUser to `MyUserTypeReader`. + +```cs +public async Task PrintUserAsync( + [OverrideTypeReader(typeof(MyUserTypeReader))] IUser user) +{ + //... +} +``` \ No newline at end of file diff --git a/docs/_overwrites/Common/images/embed-example.png b/docs/_overwrites/Common/images/embed-example.png new file mode 100644 index 000000000..f23fb4d70 Binary files /dev/null and b/docs/_overwrites/Common/images/embed-example.png differ diff --git a/docs/_overwrites/Common/images/react-example.png b/docs/_overwrites/Common/images/react-example.png new file mode 100644 index 000000000..822857d3d Binary files /dev/null and b/docs/_overwrites/Common/images/react-example.png differ diff --git a/docs/_template/light-dark-theme/partials/affix.tmpl.partial b/docs/_template/light-dark-theme/partials/affix.tmpl.partial new file mode 100644 index 000000000..ffbd27dcb --- /dev/null +++ b/docs/_template/light-dark-theme/partials/affix.tmpl.partial @@ -0,0 +1,32 @@ +{{!Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information.}} + + diff --git a/docs/_template/light-dark-theme/partials/head.tmpl.partial b/docs/_template/light-dark-theme/partials/head.tmpl.partial new file mode 100644 index 000000000..3deee4aba --- /dev/null +++ b/docs/_template/light-dark-theme/partials/head.tmpl.partial @@ -0,0 +1,25 @@ +{{!Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information.}} + + + + + {{#title}}{{title}}{{/title}}{{^title}}{{>partials/title}}{{/title}} {{#_appTitle}}| {{_appTitle}} {{/_appTitle}} + + + + {{#_description}}{{/_description}} + + + + + + + + + + + {{#_noindex}}{{/_noindex}} + {{#_enableSearch}}{{/_enableSearch}} + {{#_enableNewTab}}{{/_enableNewTab}} + + diff --git a/docs/_template/light-dark-theme/partials/scripts.tmpl.partial b/docs/_template/light-dark-theme/partials/scripts.tmpl.partial new file mode 100644 index 000000000..f353cf0c5 --- /dev/null +++ b/docs/_template/light-dark-theme/partials/scripts.tmpl.partial @@ -0,0 +1,14 @@ +{{!Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information.}} + + + + + + + + + + \ No newline at end of file diff --git a/docs/_template/light-dark-theme/styles/cornerify.js b/docs/_template/light-dark-theme/styles/cornerify.js new file mode 100644 index 000000000..4430f2d01 --- /dev/null +++ b/docs/_template/light-dark-theme/styles/cornerify.js @@ -0,0 +1,3 @@ +window.onload = function (e) { + $('img').corner(); +} diff --git a/docs/_template/light-dark-theme/styles/dark.css b/docs/_template/light-dark-theme/styles/dark.css new file mode 100644 index 000000000..19899c7e9 --- /dev/null +++ b/docs/_template/light-dark-theme/styles/dark.css @@ -0,0 +1,263 @@ +/* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License.txt in the project root for license information. */ + +@import url('vs2015.css'); +html, +body { + background: #212121; + color: #C0C0C0; +} + +button, +a { + color: #64B5F6; +} + +.sidenav{ + background-color: rgb(30, 30, 30); +} + +button:hover, +button:focus, +a:hover, +a:focus, +.btn:focus, +.btn:hover{ + color: #2196F3; +} + +a.disable, +a.disable:hover { + color: #EEEEEE; +} + +.divider { + color: #37474F; +} + +hr { + border-color: #37474F; +} + +.subnav { + background: #383838 +} + +.inheritance h5, +.inheritedMembers h5 { + border-bottom: 1px solid #37474F; +} + +article h4 { + border-bottom: 1px solid #37474F; +} + +.docs-search { + background: #424242; +} + +.search-results-group-heading { + color: #424242; +} + +.search-close { + color: #424242; +} + +.sidetoc { + background-color: #1b1b1b; + border-left: 0px solid #37474F; + border-right: 0px solid #37474F; +} + +.sideaffix { + overflow: visible; +} + +.sideaffix>div.contribution>ul>li>a.contribution-link:hover { + background-color: #333333; +} + +.toc .nav>li>a { + color: rgb(218, 218, 218); +} + +.toc .nav>li>a:hover, +.toc .nav>li>a:focus { + color: #E0E0E0; +} + +.toc .nav>li.active>a { + color: #90CAF9; +} + +.toc .nav>li.active>a:hover, +.toc .nav>li.active>a:focus { + background-color: #37474F; + color: #4FC3F7; +} + +.sidefilter { + background-color: #1b1b1b; + border-left: 0px solid #37474F; + border-right: 0px solid #37474F; +} + +.affix ul>li>a:hover { + background: none; + color: #EEEEEE; +} + +.affix ul>li.active>a, +.affix ul>li.active>a:before { + color: #B3E5FC; +} + +.affix ul>li>a { + color: #EEEEEE; +} + +.affix>ul>li.active>a, +.affix>ul>li.active>a:before { + color: #B3E5FC; +} + +.tryspan { + border-color: #37474F; +} + +.footer { + border-top: 1px solid #5F5F5F; + background: #616161; +} + +.alert-info { + color: #d9edf7; + background: #004458; + border-color: #005873 +} + +.alert-warning { + color: #fffaf2; + background: #80551a; + border-color: #99661f +} + +.alert-danger { + color: #fff2f2; + background: #4d0000; + border-color: #660000 +} + +/* For tabbed content */ + +.tabGroup { + margin-top: 1rem; +} + +.tabGroup ul[role="tablist"] { + margin: 0; + padding: 0; + list-style: none; +} + +.tabGroup ul[role="tablist"]>li { + list-style: none; + display: inline-block; +} + +.tabGroup a[role="tab"] { + color: white; + box-sizing: border-box; + display: inline-block; + padding: 5px 7.5px; + text-decoration: none; + border-bottom: 2px solid #fff; +} + +.tabGroup a[role="tab"]:hover, +.tabGroup a[role="tab"]:focus, +.tabGroup a[role="tab"][aria-selected="true"] { + border-bottom: 2px solid #607D8B; +} + +.tabGroup a[role="tab"][aria-selected="true"] { + color: #81D4FA; +} + +.tabGroup a[role="tab"]:hover, +.tabGroup a[role="tab"]:focus { + color: #29B6F6; +} + +.tabGroup a[role="tab"]:focus { + outline: 1px solid #607D8B; + outline-offset: -1px; +} + +@media (min-width: 768px) { + .tabGroup a[role="tab"] { + padding: 5px 15px; + } +} + +.tabGroup section[role="tabpanel"] { + border: 1px solid #607D8B; + padding: 15px; + margin: 0; + overflow: hidden; +} + +.tabGroup section[role="tabpanel"]>.codeHeader, +.tabGroup section[role="tabpanel"]>pre { + margin-left: -16px; + margin-right: -16px; +} + +.tabGroup section[role="tabpanel"]> :first-child { + margin-top: 0; +} + +.tabGroup section[role="tabpanel"]>pre:last-child { + display: block; + margin-bottom: -16px; +} + +.mainContainer[dir='rtl'] main ul[role="tablist"] { + margin: 0; +} + +/* code */ + +code { + color: white; + background-color: #4a4c52; + border-radius: 4px; + padding: 3px 7px; +} + +pre { + background-color: #282a36; +} + +/* table */ + +.table-striped>tbody>tr:nth-of-type(odd) { + background-color: #333333; + color: #d3d3d3 +} + +tbody>tr { + background-color: #424242; + color: #c0c0c0 +} + +.table>tbody+tbody { + border-top: 2px solid rgb(173, 173, 173) +} + +/* select */ + +select { + background-color: #3b3b3b; + border-color: #2e2e2e; +} diff --git a/docs/_template/light-dark-theme/styles/light.css b/docs/_template/light-dark-theme/styles/light.css new file mode 100644 index 000000000..18660dfe6 --- /dev/null +++ b/docs/_template/light-dark-theme/styles/light.css @@ -0,0 +1,43 @@ +/* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License.txt in the project root for license information. */ + +html, +body { + background: #fff; + color: #000; +} + +.sideaffix { + overflow: visible; +} + +/* code */ + +code { + color: #222f3d; + background-color: #f9f9f9; + border-radius: 4px; + padding: 3px 7px; +} + +/* table */ + +.table-striped>tbody>tr:nth-of-type(odd) { + color: #333333; + background-color: #d3d3d3 +} + +tbody>tr { + color: #424242; + background-color: #c0c0c0 +} + +.table>tbody+tbody { + border-top: 2px solid rgb(173, 173, 173) +} + +/* select */ + +select { + background-color: #fcfcfc; + border-color: #aeb1b5; +} diff --git a/docs/_template/light-dark-theme/styles/master.css b/docs/_template/light-dark-theme/styles/master.css new file mode 100644 index 000000000..c8dbe8884 --- /dev/null +++ b/docs/_template/light-dark-theme/styles/master.css @@ -0,0 +1,65 @@ +@import url('https://fonts.googleapis.com/css?family=Titillium+Web'); +html, +body { + font-family: 'Titillium Web', 'Segoe UI', Tahoma, Helvetica, sans-serif; + height: 100%; + font-size: 15px; +} + +p, +li, +.toc { + line-height: 160%; +} + +img { + box-shadow: 0px 0px 3px 0px rgb(66, 66, 66); + max-width: 95% !important; + margin-top: 15px; + margin-bottom: 15px; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + line-height: 130%; +} + +.sideaffix { + line-height: 140%; +} + +header .navbar { + border-width: 0 0 0px; + border-radius: 0; +} + +body .toc { + background-color: inherit; + overflow: visible; +} + +select { + display: inline-block; + overflow: auto; + -webkit-box-sizing: border-box; + box-sizing: border-box; + margin: 0; + padding: 0 30px 0 6px; + vertical-align: middle; + height: 28px; + border: 1px solid #e3e3e3; + line-height: 16px; + outline: 0; + text-overflow: ellipsis; + -webkit-appearance: none; + -moz-appearance: none; + cursor: pointer; + background-image: linear-gradient(45deg, transparent 50%, #707070 0), linear-gradient(135deg, #707070 50%, transparent 0); + background-position: calc(100% - 13px) 11px, calc(100% - 8px) 11px; + background-size: 5px 5px, 5px 6px; + background-repeat: no-repeat; +} diff --git a/docs/_template/light-dark-theme/styles/plugin-featherlight.js b/docs/_template/light-dark-theme/styles/plugin-featherlight.js new file mode 100644 index 000000000..846494cf3 --- /dev/null +++ b/docs/_template/light-dark-theme/styles/plugin-featherlight.js @@ -0,0 +1,15 @@ +$(document).ready(function() { + //find all images, but not the logo, and add the lightbox + $('img').not('#logo').each(function(){ + var $img = $(this); + var filename = $img.attr('src') + //add cursor + $img.css('cursor','zoom-in'); + $img.css('cursor','-moz-zoom-in'); + $img.css('cursor','-webkit-zoom-in'); + + //add featherlight + $img.attr('alt', filename); + $img.featherlight(filename); + }); +}); \ No newline at end of file diff --git a/docs/_template/light-dark-theme/styles/styleswitcher.js b/docs/_template/light-dark-theme/styles/styleswitcher.js new file mode 100644 index 000000000..e80a17daf --- /dev/null +++ b/docs/_template/light-dark-theme/styles/styleswitcher.js @@ -0,0 +1,27 @@ +const baseUrl = document.getElementById("docfx-style:rel").content; +var themeElement; + +function onThemeSelect(event) { + const theme = event.target.value; + window.localStorage.setItem("theme", theme); + window.themeElement.href = getUrl(theme); +} + +function getUrl(slug) { + return baseUrl + "styles/" + slug + ".css"; +} + +const themeElement = document.createElement("link"); +themeElement.rel = "stylesheet"; + +const theme = window.localStorage.getItem("theme") || "light"; +themeElement.href = getUrl(theme); + +document.head.appendChild(themeElement); +window.themeElement = themeElement; + +document.addEventListener("DOMContentLoaded", function() { + const themeSwitcher = document.getElementById("theme-switcher"); + themeSwitcher.onchange = onThemeSelect; + themeSwitcher.value = theme; +}, false); diff --git a/docs/_template/light-dark-theme/styles/theme-switcher.css b/docs/_template/light-dark-theme/styles/theme-switcher.css new file mode 100644 index 000000000..c6e27c93a --- /dev/null +++ b/docs/_template/light-dark-theme/styles/theme-switcher.css @@ -0,0 +1,9 @@ +div.theme-switch-field { + padding-left: 10px; + padding-bottom: 15px +} + +div.theme-switch-field > p{ + font-weight: bold; + font-size: 1.2em; +} \ No newline at end of file diff --git a/docs/_template/light-dark-theme/styles/vs2015.css b/docs/_template/light-dark-theme/styles/vs2015.css new file mode 100644 index 000000000..d8c14a046 --- /dev/null +++ b/docs/_template/light-dark-theme/styles/vs2015.css @@ -0,0 +1,115 @@ +/* + * Visual Studio 2015 dark style + * Author: Nicolas LLOBERA + */ + +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + background: #282a36; + color: #DCDCDC; +} + +.hljs-keyword, +.hljs-literal, +.hljs-symbol, +.hljs-name { + color: #569CD6; +} +.hljs-link { + color: #569CD6; + text-decoration: underline; +} + +.hljs-built_in, +.hljs-type { + color: #4EC9B0; +} + +.hljs-number, +.hljs-class { + color: #B8D7A3; +} + +.hljs-string, +.hljs-meta-string { + color: #D69D85; +} + +.hljs-regexp, +.hljs-template-tag { + color: #9A5334; +} + +.hljs-subst, +.hljs-function, +.hljs-title, +.hljs-params, +.hljs-formula { + color: #DCDCDC; +} + +.hljs-comment, +.hljs-quote { + color: #57A64A; + font-style: italic; +} + +.hljs-doctag { + color: #608B4E; +} + +.hljs-meta, +.hljs-meta-keyword, +.hljs-tag { + color: #9B9B9B; +} + +.hljs-variable, +.hljs-template-variable { + color: #BD63C5; +} + +.hljs-attr, +.hljs-attribute, +.hljs-builtin-name { + color: #9CDCFE; +} + +.hljs-section { + color: gold; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-strong { + font-weight: bold; +} + +/*.hljs-code { + font-family:'Monospace'; +}*/ + +.hljs-bullet, +.hljs-selector-tag, +.hljs-selector-id, +.hljs-selector-class, +.hljs-selector-attr, +.hljs-selector-pseudo { + color: #D7BA7D; +} + +.hljs-addition { + background-color: #144212; + display: inline-block; + width: 100%; +} + +.hljs-deletion { + background-color: #600; + display: inline-block; + width: 100%; +} diff --git a/docs/api/index.md b/docs/api/index.md index d9433363f..c16ca1363 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -1,13 +1,16 @@ +--- +uid: API.Docs +--- # API Documentation -This is where you will find documentation for all members and objects in Discord.Net +This is where you will find documentation for all members and objects in Discord.Net. -__Commonly Used Entities__ +# Commonly Used Entities -* @Discord.WebSocket -* @Discord.WebSocket.DiscordSocketClient -* @Discord.WebSocket.SocketGuildChannel -* @Discord.WebSocket.SocketGuildUser -* @Discord.WebSocket.SocketMessage -* @Discord.WebSocket.SocketRole \ No newline at end of file +* @Discord.WebSocket +* @Discord.WebSocket.DiscordSocketClient +* @Discord.WebSocket.SocketGuildChannel +* @Discord.WebSocket.SocketGuildUser +* @Discord.WebSocket.SocketMessage +* @Discord.WebSocket.SocketRole \ No newline at end of file diff --git a/docs/docfx.json b/docs/docfx.json index 3c0b0611e..8691c2732 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -1,74 +1,51 @@ { - "metadata": [ - { - "src": [ - { - "src": "..", - "files": [ - "src/**/*.cs" - ], - "exclude": [ - "**/obj/**", - "**/bin/**", - "_site/**" - ] - } - ], - "dest": "api", - "filter": "filterConfig.yml" + "metadata": [{ + "src": [{ + "src": "../src", + "files": [ + "**.csproj" + ] + }], + "dest": "api", + "filter": "filterConfig.yml", + "properties": { + "TargetFramework": "netstandard1.3" } - ], + }], "build": { - "content": [ - { - "files": [ - "api/**.yml", - "api/index.md" - ] + "content": [{ + "files": ["api/**.yml", "api/index.md"] }, { - "files": [ - "guides/**.md", - "guides/**/toc.yml", - "toc.yml", - "*.md" - ], - "exclude": [ - "obj/**", - "_site/**" - ] - } - ], - "resource": [ + "files": ["toc.yml", "index.md"] + }, { - "files": [ - "**/images/**", - "**/samples/**" - ], - "exclude": [ - "obj/**", - "_site/**" - ] - } - ], - "overwrite": [ + "files": ["faq/**.md", "faq/**/toc.yml"] + }, { - "files": [ - "apidoc/**.md" - ], - "exclude": [ - "obj/**", - "_site/**" - ] + "files": ["guides/**.md", "guides/**/toc.yml"] } ], + "resource": [{ + "files": [ + "**/images/**", + "**/samples/**" + ] + }], "dest": "_site", "template": [ - "default" + "default", + "_template/light-dark-theme" ], + "overwrite": "_overwrites/**/**.md", "globalMetadata": { - "_appFooter": "Discord.Net (c) 2015-2017" + "_appTitle": "Discord.Net Documentation", + "_appFooter": "Discord.Net (c) 2015-2018 2.0.0-beta", + "_enableSearch": true, }, - "noLangKeyword": false + "noLangKeyword": false, + "xrefService": [ + "https://xref.docs.microsoft.com/query?uid={uid}" + ] } -} \ No newline at end of file +} diff --git a/docs/faq/basics/basic-operations.md b/docs/faq/basics/basic-operations.md new file mode 100644 index 000000000..518a7426d --- /dev/null +++ b/docs/faq/basics/basic-operations.md @@ -0,0 +1,111 @@ +--- +uid: FAQ.Basics.BasicOp +title: Questions about Basic Operations +--- + +# Basic Operations Questions + +## How should I safely check a type? + +> [!WARNING] +> Direct casting (e.g. `(Type)type`) is **the least recommended** +> way of casting, as it *can* throw an [InvalidCastException] +> when the object isn't the desired type. +> +> Please refer to [this post] for more details. + +In Discord.Net, the idea of polymorphism is used throughout. You may +need to cast the object as a certain type before you can perform any +action. + +A good and safe casting example: + +[!code-csharp[Casting](samples/cast.cs)] + +[InvalidCastException]: https://docs.microsoft.com/en-us/dotnet/api/system.invalidcastexception +[this post]: https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/types/how-to-safely-cast-by-using-as-and-is-operators + +## How do I send a message? + +> [!TIP] +> The [GetChannel] method by default returns an [IChannel]. +> This means channels such as [IVoiceChannel], [ICategoryChannel] +> can be returned. This is why that you cannot send message +> to channels like those. + +Any implementation of [IMessageChannel] has a [SendMessageAsync] +method. You can get the channel via [GetChannel] under the client. +Remember, when using Discord.Net, polymorphism is a common recurring +theme. This means an object may take in many shapes or form, which +means casting is your friend. You should attempt to cast the channel +as an [IMessageChannel] or any other entity that implements it to be +able to message. + +[SendMessageAsync]: xref:Discord.IMessageChannel.SendMessageAsync* +[GetChannel]: xref:Discord.WebSocket.DiscordSocketClient.GetChannel* + +## How can I tell if a message is from X, Y, Z channel? + +You may check the message channel type. Visit [Glossary] to see the +various types of channels. + +[Glossary]: xref:FAQ.Misc.Glossary#message-channels + +## How can I get the guild from a message? + +There are 2 ways to do this. You can do either of the following, + +1. Cast the user as an [IGuildUser] and use its [IGuild] property. +2. Cast the channel as an [IGuildChannel] and use its [IGuild] property. + +## How do I add hyperlink text to an embed? + +Embeds can use standard [markdown] in the description field as well +as in field values. With that in mind, links can be added with +`[text](link)`. + +[markdown]: https://support.discordapp.com/hc/en-us/articles/210298617-Markdown-Text-101-Chat-Formatting-Bold-Italic-Underline- + +## How do I add reactions to a message? + +Any entity that implements [IUserMessage] has an [AddReactionAsync] +method. This method expects an [IEmote] as a parameter. +In Discord.Net, an Emote represents a custom-image emote, while an +Emoji is a Unicode emoji (standard emoji). Both [Emoji] and [Emote] +implement [IEmote] and are valid options. + +[!code-csharp[Emoji](samples/emoji.cs)] + +[AddReactionAsync]: xref:Discord.IUserMessage.AddReactionAsync* + +## What is a "preemptive rate limit?" + +A preemptive rate limit is Discord.Net's way of telling you to slow +down before you get hit by the real rate limit. Hitting a real rate +limit might prevent your entire client from sending any requests for +a period of time. This is calculated based on the HTTP header +returned by a Discord response. + +## Why am I getting so many preemptive rate limits when I try to add more than one reactions? + +This is due to how HTML header works, mistreating +0.25sec/action to 1sec. This causes the lib to throw preemptive rate +limit more frequently than it should for methods such as adding +reactions. + +## Can I opt-out of preemptive rate limits? + +Unfortunately, not at the moment. See [#401](https://github.com/RogueException/Discord.Net/issues/401). + +[IChannel]: xref:Discord.IChannel +[ICategoryChannel]: xref:Discord.ICategoryChannel +[IGuildChannel]: xref:Discord.IGuildChannel +[ITextChannel]: xref:Discord.ITextChannel +[IGuild]: xref:Discord.IGuild +[IVoiceChannel]: xref:Discord.IVoiceChannel +[IGuildUser]: xref:Discord.IGuildUser +[IMessageChannel]: xref:Discord.IMessageChannel +[IUserMessage]: xref:Discord.IUserMessage +[IEmote]: xref:Discord.IEmote +[Emote]: xref:Discord.Emote +[Emoji]: xref:Discord.Emoji \ No newline at end of file diff --git a/docs/faq/basics/client-basics.md b/docs/faq/basics/client-basics.md new file mode 100644 index 000000000..376667ca0 --- /dev/null +++ b/docs/faq/basics/client-basics.md @@ -0,0 +1,58 @@ +--- +uid: FAQ.Basics.ClientBasics +title: Basic Questions about Client +--- + +# Client Basics Questions + +## My client keeps returning 401 upon logging in! + +> [!WARNING] +> Userbot/selfbot (logging in with a user token) is not +> officially supported with this library. +> +> Logging in under a user account may result in account +> termination! + +There are few possible reasons why this may occur. + +1. You are not using the appropriate [TokenType]. If you are using a + bot account created from the Discord Developer portal, you should + be using `TokenType.Bot`. +2. You are not using the correct login credentials. Please keep in + mind that tokens is different from a *client secret*. + +[TokenType]: xref:Discord.TokenType + +## How do I do X, Y, Z when my bot connects/logs on? Why do I get a `NullReferenceException` upon calling any client methods after connect? + +Your bot should **not** attempt to interact in any way with +guilds/servers until the [Ready] event fires. When the bot +connects, it first has to download guild information from +Discord in order for you to get access to any server +information; the client is not ready at this point. + +Technically, the [GuildAvailable] event fires once the data for a +particular guild has downloaded; however, it's best to wait for all +guilds to be downloaded. Once all downloads are complete, the [Ready] +event is triggered, then you can proceed to do whatever you like. + +[Ready]: xref:Discord.WebSocket.DiscordSocketClient.Ready +[GuildAvailable]: xref:Discord.WebSocket.BaseSocketClient.GuildAvailable + +## How do I get a message's previous content when that message is edited? + +If you need to do anything with messages (e.g. checking Reactions, +checking the content of edited/deleted messages), you must set the +[MessageCacheSize] in your [DiscordSocketConfig] settings in order to +use the cached message entity. Read more about it [here](xref:Guides.Concepts.Events#cacheable). + +1. Message Cache must be enabled. +2. Hook the MessageUpdated event. This event provides a *before* and + *after* object. +3. Only messages received *after* the bot comes online will be + available in the cache. + +[MessageCacheSize]: xref:Discord.WebSocket.DiscordSocketConfig.MessageCacheSize +[DiscordSocketConfig]: xref:Discord.WebSocket.DiscordSocketConfig +[MessageUpdated]: xref:Discord.WebSocket.BaseSocketClient.MessageUpdated \ No newline at end of file diff --git a/docs/faq/basics/getting-started.md b/docs/faq/basics/getting-started.md new file mode 100644 index 000000000..08972ba2e --- /dev/null +++ b/docs/faq/basics/getting-started.md @@ -0,0 +1,73 @@ +--- +uid: FAQ.Basics.GetStarted +title: Beginner Questions / How to Get Started +--- + +# Basic Concepts / Getting Started + +## How do I add my bot to my server/guild? + +You can do so by using the [permission calculator] provided +by FiniteReality. +This tool allows you to set the permissions that the bot will be +added with, and invite the bot into your guild. With this method, +bots will also be assigned their own special roles that normal users +cannot use; this is what we call a `Managed` role, and this is a much +safer method of permission management than to create a role that any +users can be assigned to. + +[permission calculator]: https://finitereality.github.io/permissions-calculator + +## What is a token? + +A token is a credential used to log into an account. This information +should be kept **private** and for your eyes only. Anyone with your +token can log into your account. This applies to both user and bot +accounts. That also means that you should never ever hardcode your +token or add it into source control, as your identity may be stolen +by scrape bots on the internet that scours through constantly to +obtain a token. + +## What is a client/user/object ID? + +Each user and object on Discord has its own snowflake ID generated +based on various conditions. + +![Snowflake Generation](images/snowflake.png) + +The ID can be seen by anyone; it is public. It is merely used to +identify an object in the Discord ecosystem. Many things in the +Discord ecosystem require an ID to retrieve or identify the said +object. + +There are 2 common ways to obtain the said ID. + +### [Discord Developer Mode](#tab/dev-mode) + +By enabling the developer mode you can right click on most objects +to obtain their snowflake IDs (please note that this may not apply to +all objects, such as role IDs, or DM channel IDs). + +![Developer Mode](images/dev-mode.png) + +### [Escape Character](#tab/escape-char) + +You can escape an object by using `\` in front the object in the +Discord client. For example, when you do `\@Example#1234` in chat, +it will return the user ID of the aforementioned user. + +![Escaping mentions](images/mention-escape.png) + +*** + +## How do I get the role ID? + +> [!WARNING] +> Right-clicking on the role and copying the ID will **not** work. +> This will only copy the message ID. + +Several common ways to do this: + +1. Make the role mentionable and mention the role, and escape it + using the `\` character in front. +2. Inspect the roles collection within the guild via your debugger. \ No newline at end of file diff --git a/docs/faq/basics/images/dev-mode.png b/docs/faq/basics/images/dev-mode.png new file mode 100644 index 000000000..fd20b95d1 Binary files /dev/null and b/docs/faq/basics/images/dev-mode.png differ diff --git a/docs/faq/basics/images/mention-escape.png b/docs/faq/basics/images/mention-escape.png new file mode 100644 index 000000000..927978061 Binary files /dev/null and b/docs/faq/basics/images/mention-escape.png differ diff --git a/docs/faq/basics/images/snowflake.png b/docs/faq/basics/images/snowflake.png new file mode 100644 index 000000000..816a10eee Binary files /dev/null and b/docs/faq/basics/images/snowflake.png differ diff --git a/docs/faq/basics/samples/cast.cs b/docs/faq/basics/samples/cast.cs new file mode 100644 index 000000000..73ef5237f --- /dev/null +++ b/docs/faq/basics/samples/cast.cs @@ -0,0 +1,15 @@ +public async Task MessageReceivedHandler(SocketMessage msg) +{ + // Option 1: + // Using the `as` keyword, which will return `null` if the object isn't the desired type. + var usermsg = msg as SocketUserMessage; + // We bail when the message isn't the desired type. + if (msg == null) return; + + // Option 2: + // Using the `is` keyword to cast (C#7 or above only) + if (msg is SocketUserMessage usermsg) + { + // Do things + } +} \ No newline at end of file diff --git a/docs/faq/basics/samples/emoji.cs b/docs/faq/basics/samples/emoji.cs new file mode 100644 index 000000000..dd3e6317f --- /dev/null +++ b/docs/faq/basics/samples/emoji.cs @@ -0,0 +1,18 @@ +// bail if the message is not a user one (system messages cannot have reactions) +var usermsg = msg as IUserMessage; +if (usermsg == null) return; + +// standard Unicode emojis +Emoji emoji = new Emoji("👍"); +// or +// Emoji emoji = new Emoji("\uD83D\uDC4D"); + +// custom guild emotes +Emote emote = Emote.Parse("<:dotnet:232902710280716288>"); +// using Emote.TryParse may be safer in regards to errors being thrown; +// please note that the method does not verify if the emote exists, +// it simply creates the Emote object for you. + +// add the reaction to the message +await usermsg.AddReactionAsync(emoji); +await usermsg.AddReactionAsync(emote); \ No newline at end of file diff --git a/docs/faq/commands/Commands.md b/docs/faq/commands/Commands.md new file mode 100644 index 000000000..4811b02be --- /dev/null +++ b/docs/faq/commands/Commands.md @@ -0,0 +1,151 @@ +--- +uid: FAQ.Commands +title: Questions about Commands +--- + +# Command-related Questions + +## How can I restrict some of my commands so only certain users can execute them? + +Based on how you want to implement the restrictions, you can use the +built-in [RequireUserPermission] precondition, which allows you to +restrict the command based on the user's current permissions in the +guild or channel (*e.g. `GuildPermission.Administrator`, +`ChannelPermission.ManageMessages` etc.*). + +If, however, you wish to restrict the commands based on the user's +role, you can either create your own custom precondition or use +Joe4evr's [Preconditions Addons] that provides a few custom +preconditions that aren't provided in the stock library. +Its source can also be used as an example for creating your own +custom preconditions. + +[RequireUserPermission]: xref:Discord.Commands.RequireUserPermissionAttribute +[Preconditions Addons]: https://github.com/Joe4evr/Discord.Addons/tree/master/src/Discord.Addons.Preconditions + +## I'm getting an error about `Assembly#GetEntryAssembly`. + +You may be confusing [CommandService#AddModulesAsync] with +[CommandService#AddModuleAsync]. The former is used to add modules +via the assembly, while the latter is used to add a single module. + +[CommandService#AddModulesAsync]: xref:Discord.Commands.CommandService.AddModulesAsync* +[CommandService#AddModuleAsync]: xref:Discord.Commands.CommandService.AddModuleAsync* + +## What does [Remainder] do in the command signature? + +The [RemainderAttribute] leaves the string unparsed, meaning you +don't have to add quotes around the text for the text to be +recognized as a single object. Please note that if your method has +multiple parameters, the remainder attribute can only be applied to +the last parameter. + +[!code-csharp[Remainder](samples/Remainder.cs)] + +[RemainderAttribute]: xref:Discord.Commands.RemainderAttribute + +## What is a service? Why does my module not hold any data after execution? + +In Discord.Net, modules are created similarly to ASP.NET, meaning +that they have a transient nature. This means that they are spawned +every time when a request is received, and are killed from memory +when the execution finishes. This is why you cannot store persistent +data inside a module. To workaround this, consider using a service. + +Service is often used to hold data externally, so that they will +persist throughout execution. Think of it like a chest that holds +whatever you throw at it that won't be affected by anything unless +you want it to. Note that you should also learn Microsoft's +implementation of [Dependency Injection] \([video]) before proceeding, +as well as how it works in [Discord.Net](xref:Guides.Commands.DI#usage-in-modules). + +A brief example of service and dependency injection can be seen below. + +[!code-csharp[DI](samples/DI.cs)] + +[Dependency Injection]: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection +[video]: https://www.youtube.com/watch?v=QtDTfn8YxXg + +## I have a long-running Task in my command, and Discord.Net keeps saying that a `MessageReceived` handler is blocking the gateway. What gives? + +By default, all commands are executed on the same thread as the +gateway task, which is responsible for keeping the connection from +your client to Discord alive. When you execute a command, +this blocks the gateway from communicating for as long as the command +task is being executed. The library will warn you about any long +running event handler (in this case, the command handler) that +persists for **more than 3 seconds**. + +To resolve this, the library has designed a flag called [RunMode]. + +There are 2 main `RunMode`s. + +1. `RunMode.Sync` (default) +2. `RunMode.Async` + +You can set the `RunMode` either by specifying it individually via +the `CommandAttribute`, or by setting the global default with +the [DefaultRunMode] flag under `CommandServiceConfig`. + +# [CommandAttribute](#tab/cmdattrib) + +[!code-csharp[Command Attribute](samples/runmode-cmdattrib.cs)] + +# [CommandServiceConfig](#tab/cmdconfig) + +[!code-csharp[Command Service Config](samples/runmode-cmdconfig.cs)] + +*** + +*** + +> [!IMPORTANT] +> While specifying `RunMode.Async` allows the command to be spun off +> to a different thread instead of the gateway thread, +> keep in mind that there will be **potential consequences** +> by doing so. Before applying this flag, please +> consider whether it is necessary to do so. +> +> Further details regarding `RunMode.Async` can be found below. + +[RunMode]: xref:Discord.Commands.RunMode +[CommandAttribute]: xref:Discord.Commands.CommandAttribute +[DefaultRunMode]: xref:Discord.Commands.CommandServiceConfig.DefaultRunMode + +## How does `RunMode.Async` work, and why is Discord.Net *not* using it by default? + +`RunMode.Async` works by spawning a new `Task` with an unawaited +[Task.Run], essentially making `ExecuteAsyncInternalAsync`, the task +that is used to invoke the command task, to be finished on a +different thread. This means that [ExecuteAsync] will be forced to +return a successful [ExecuteResult] regardless of the actual +execution result. + +The following are the known caveats with `RunMode.Async`, + +1. You can potentially introduce race condition. +2. Unnecessary overhead caused by [async state machine]. +3. [ExecuteAsync] will immediately return [ExecuteResult] instead of + other result types (this is particularly important for those who wish + to utilize [RuntimeResult] in 2.0). +4. Exceptions are swallowed. + +However, there are ways to remedy some of these. + +For #3, in Discord.Net 2.0, the library introduces a new event called +[CommandExecuted], which is raised whenever the command is +**successfully executed**. This event will be raised regardless of +the `RunMode` type and will return the appropriate execution result. + +For #4, exceptions are caught in [CommandService#Log] event under +[LogMessage.Exception] as [CommandException]. + +[Task.Run]: https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.run +[async state machine]: https://www.red-gate.com/simple-talk/dotnet/net-tools/c-async-what-is-it-and-how-does-it-work/ +[ExecuteAsync]: xref:Discord.Commands.CommandService.ExecuteAsync* +[ExecuteResult]: xref:Discord.Commands.ExecuteResult +[RuntimeResult]: xref:Discord.Commands.RuntimeResult +[CommandExecuted]: xref:Discord.Commands.CommandService.CommandExecuted +[CommandService#Log]: xref:Discord.Commands.CommandService.Log +[LogMessage.Exception]: xref:Discord.LogMessage.Exception* +[CommandException]: xref:Discord.Commands.CommandException \ No newline at end of file diff --git a/docs/faq/commands/samples/DI.cs b/docs/faq/commands/samples/DI.cs new file mode 100644 index 000000000..59f098ff9 --- /dev/null +++ b/docs/faq/commands/samples/DI.cs @@ -0,0 +1,27 @@ +public class MyService +{ + public string MyCoolString {get; set;} +} +public class Setup +{ + public IServiceProvider BuildProvider() => + new ServiceCollection() + .AddSingleton() + .BuildServiceProvider(); +} +public class MyModule : ModuleBase +{ + // Inject via public settable prop + public MyService MyService {get; set;} + + // or via ctor + private readonly MyService _myService; + public MyModule (MyService myService) => _myService = myService; + + [Command("string")] + public Task GetOrSetStringAsync(string input) + { + if (_myService.MyCoolString == null) _myService.MyCoolString = input; + return ReplyAsync(_myService.MyCoolString); + } +} \ No newline at end of file diff --git a/docs/faq/commands/samples/Remainder.cs b/docs/faq/commands/samples/Remainder.cs new file mode 100644 index 000000000..a28c782e0 --- /dev/null +++ b/docs/faq/commands/samples/Remainder.cs @@ -0,0 +1,20 @@ +// Input: +// !echo Coffee Cake + +// Output: +// Coffee Cake +[Command("echo")] +public Task EchoRemainderAsync([Remainder]string text) => ReplyAsync(text); + +// Output: +// CommandError.BadArgCount +[Command("echo-hassle")] +public Task EchoAsync(string text) => ReplyAsync(text); + +// The message would be seen as having multiple parameters, +// while the method only accepts one. +// Wrapping the message in quotes solves this. +// This way, the system knows the entire message is to be parsed as a +// single String. +// e.g. +// !echo "Coffee Cake" \ No newline at end of file diff --git a/docs/faq/commands/samples/runmode-cmdattrib.cs b/docs/faq/commands/samples/runmode-cmdattrib.cs new file mode 100644 index 000000000..253acc4a9 --- /dev/null +++ b/docs/faq/commands/samples/runmode-cmdattrib.cs @@ -0,0 +1,7 @@ +[Command("process", RunMode = RunMode.Async)] +public async Task ProcessAsync(string input) +{ + // Does heavy calculation here. + await Task.Delay(TimeSpan.FromMinute(1)); + await ReplyAsync(input); +} \ No newline at end of file diff --git a/docs/faq/commands/samples/runmode-cmdconfig.cs b/docs/faq/commands/samples/runmode-cmdconfig.cs new file mode 100644 index 000000000..22b356aa3 --- /dev/null +++ b/docs/faq/commands/samples/runmode-cmdconfig.cs @@ -0,0 +1,9 @@ +public class Setup +{ + private readonly CommandService _command; + public Setup() + { + var config = new CommandServiceConfig{DefaultRunMode = RunMode.Async}; + _command = new CommandService(config); + } +} \ No newline at end of file diff --git a/docs/faq/misc/Glossary.md b/docs/faq/misc/Glossary.md new file mode 100644 index 000000000..44dbb9a9e --- /dev/null +++ b/docs/faq/misc/Glossary.md @@ -0,0 +1,73 @@ +--- +uid: FAQ.Misc.Glossary +title: Common Terminologies / Glossary +--- + +# Glossary + +## Common Types + +* A **Guild** ([IGuild]) is an isolated collection of users and +channels, and are often referred to as "servers". + - Example: [Discord API](https://discord.gg/jkrBmQR) +* A **Channel** ([IChannel]) represents a generic channel. + - Example: #dotnet_discord-net + - See [Channel Types](#channel-types) + +[IGuild]: xref:Discord.IGuild +[IChannel]: xref:Discord.IChannel + +## Channel Types + +### Message Channels +* A **Text channel** ([ITextChannel]) is a message channel from a +Guild. +* A **DM channel** ([IDMChannel]) is a message channel from a DM. +* A **Group channel** ([IGroupChannel]) is a message channel from a +Group. + - This is rarely used due to the bot's inability to join groups. +* A **Private channel** ([IPrivateChannel]) is a DM or a Group. +* A **Message channel** ([IMessageChannel]) can be any of the above. + +### Misc Channels +* A **Guild channel** ([IGuildChannel]) is a guild channel in a guild. + - This can be any channels that may exist in a guild. +* A **Voice channel** ([IVoiceChannel]) is a voice channel in a guild. +* A **Category channel** ([ICategoryChannel]) (2.0+) is a category that +holds one or more sub-channels. + +[IGuildChannel]: xref:Discord.IGuildChannel +[IMessageChannel]: xref:Discord.IMessageChannel +[ITextChannel]: xref:Discord.ITextChannel +[IGroupChannel]: xref:Discord.IGroupChannel +[IDMChannel]: xref:Discord.IDMChannel +[IPrivateChannel]: xref:Discord.IPrivateChannel +[IVoiceChannel]: xref:Discord.IVoiceChannel +[ICategoryChannel]: xref:Discord.ICategoryChannel + +## Emoji Types + +* An **Emote** ([Emote]) is a custom emote from a guild. + - Example: `<:dotnet:232902710280716288>` +* An **Emoji** ([Emoji]) is a Unicode emoji. + - Example: `👍` + +[Emote]: xref:Discord.Emote +[Emoji]: xref:Discord.Emoji + +## Activity Types + +* A **Game** ([Game]) refers to a user's game activity. +* A **Rich Presence** ([RichGame]) refers to a user's detailed +gameplay status. + - Visit [Rich Presence Intro] on Discord docs for more info. +* A **Streaming Status** ([StreamingGame]) refers to user's activity +for streaming on services such as Twitch. +* A **Spotify Status** ([SpotifyGame]) (2.0+) refers to a user's +activity for listening to a song on Spotify. + +[Game]: xref:Discord.Game +[RichGame]: xref:Discord.RichGame +[StreamingGame]: xref:Discord.StreamingGame +[SpotifyGame]: xref:Discord.SpotifyGame +[Rich Presence Intro]: https://discordapp.com/developers/docs/rich-presence/best-practices \ No newline at end of file diff --git a/docs/faq/misc/Legacy.md b/docs/faq/misc/Legacy.md new file mode 100644 index 000000000..ef4caa1cd --- /dev/null +++ b/docs/faq/misc/Legacy.md @@ -0,0 +1,26 @@ +--- +uid: FAQ.Misc.Legacy +title: Questions about Legacy Versions +--- + +# Legacy Questions + +## X, Y, Z does not work! It doesn't return a valid value anymore + +If you're currently using an older version in stable branch, please +upgrade to the latest pre-release version to ensure maximum +compatibility. Several features may be broken in older +versions and will likely not be fixed in the version branch due to +their breaking nature. + +Visit the repo's [release tag] to see the latest public pre-release. + +[release tag]: https://github.com/RogueException/Discord.Net/releases + +## I came from an earlier version of Discord.Net 1.0, and DependencyMap doesn't seem to exist anymore in the later revision? What happened to it? + +The `DependencyMap` has been replaced with Microsoft's +[DependencyInjection] Abstractions. An example usage can be seen +[here](https://github.com/foxbot/DiscordBotBase/blob/csharp/src/DiscordBot/Program.cs#L36). + +[DependencyInjection]: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection \ No newline at end of file diff --git a/docs/faq/toc.yml b/docs/faq/toc.yml new file mode 100644 index 000000000..1a6cd21bb --- /dev/null +++ b/docs/faq/toc.yml @@ -0,0 +1,18 @@ +- name: Basic Concepts + items: + - name: Getting Started + topicUid: FAQ.Basics.GetStarted + - name: Basic Operations + topicUid: FAQ.Basics.BasicOp + - name: Client Basics + topicUid: FAQ.Basics.ClientBasics +- name: Command + items: + - name: Commands + topicUid: FAQ.Commands +- name: Misc + items: + - name: Glossary + topicUid: FAQ.Misc.Glossary + - name: Legacy or Upgrade + topicUid: FAQ.Misc.Legacy diff --git a/docs/guides/commands/commands.md b/docs/guides/commands/commands.md deleted file mode 100644 index 2b012af0e..000000000 --- a/docs/guides/commands/commands.md +++ /dev/null @@ -1,343 +0,0 @@ -# The Command Service - -[Discord.Commands](xref:Discord.Commands) provides an Attribute-based -command parser. - -## Setup - -To use Commands, you must create a [Command Service] and a Command -Handler. - -Included below is a very barebone Command Handler. You can extend your -Command Handler as much as you like; however, the below is the bare -minimum. - -The `CommandService` will optionally accept a [CommandServiceConfig], -which _does_ set a few default values for you. It is recommended to -look over the properties in [CommandServiceConfig] and their default -values. - -[!code-csharp[Command Handler](samples/command_handler.cs)] - -[Command Service]: xref:Discord.Commands.CommandService -[CommandServiceConfig]: xref:Discord.Commands.CommandServiceConfig - -## With Attributes - -In 1.0, Commands can be defined ahead of time with attributes, or at -runtime with builders. - -For most bots, ahead-of-time Commands should be all you need, and this -is the recommended method of defining Commands. - -### Modules - -The first step to creating Commands is to create a _module_. - -A Module is an organizational pattern that allows you to write your -Commands in different classes and have them automatically loaded. - -Discord.Net's implementation of Modules is influenced heavily from -ASP.NET Core's Controller pattern. This means that the lifetime of a -module instance is only as long as the Command is being invoked. - -**Avoid using long-running code** in your modules wherever possible. -You should **not** be implementing very much logic into your modules, -instead, outsource to a service for that. - -If you are unfamiliar with Inversion of Control, it is recommended to -read the MSDN article on [IoC] and [Dependency Injection]. - -To begin, create a new class somewhere in your project and inherit the -class from [ModuleBase]. This class **must** be `public`. - ->[!NOTE] ->[ModuleBase] is an _abstract_ class, meaning that you may extend it ->or override it as you see fit. Your module may inherit from any ->extension of ModuleBase. - -By now, your module should look like this: - -[!code-csharp[Empty Module](samples/empty-module.cs)] - -[IoC]: https://msdn.microsoft.com/en-us/library/ff921087.aspx -[Dependency Injection]: https://msdn.microsoft.com/en-us/library/ff921152.aspx -[ModuleBase]: xref:Discord.Commands.ModuleBase`1 - -### Adding Commands - -The next step to creating Commands is actually creating the Commands. - -To create a Command, add a method to your module of type `Task`. -Typically, you will want to mark this method as `async`, although it -is not required. - -Adding parameters to a Command is done by adding parameters to the -parent Task. - -For example, to take an integer as an argument from the user, add `int -arg`; to take a user as an argument from the user, add `IUser user`. -In 1.0, a Command can accept nearly any type of argument; a full list -of types that are parsed by default can be found in the below section -on _Type Readers_. - -Parameters, by default, are always required. To make a parameter -optional, give it a default value. To accept a comma-separated list, -set the parameter to `params Type[]`. - -Should a parameter include spaces, it **must** be wrapped in quotes. -For example, for a Command with a parameter `string food`, you would -execute it with `!favoritefood "Key Lime Pie"`. - -If you would like a parameter to parse until the end of a Command, -flag the parameter with the [RemainderAttribute]. This will allow a -user to invoke a Command without wrapping a parameter in quotes. - -Finally, flag your Command with the [CommandAttribute]. (you must -specify a name for this Command, except for when it is part of a -Module Group - see below) - -[RemainderAttribute]: xref:Discord.Commands.RemainderAttribute -[CommandAttribute]: xref:Discord.Commands.CommandAttribute - -### Command Overloads - -You may add overloads to your Commands, and the Command parser will -automatically pick up on it. - -If for whatever reason, you have two Commands which are ambiguous to -each other, you may use the @Discord.Commands.PriorityAttribute to -specify which should be tested before the other. - -The `Priority` attributes are sorted in ascending order; the higher -priority will be called first. - -### Command Context - -Every Command can access the execution context through the [Context] -property on [ModuleBase]. `ICommandContext` allows you to access the -message, channel, guild, and user that the Command was invoked from, -as well as the underlying Discord client that the Command was invoked -from. - -Different types of Contexts may be specified using the generic variant -of [ModuleBase]. When using a [SocketCommandContext], for example, the -properties on this context will already be Socket entities, so you -will not need to cast them. - -To reply to messages, you may also invoke [ReplyAsync], instead of -accessing the channel through the [Context] and sending a message. - -> [!WARNING] ->Contexts should **NOT** be mixed! You cannot have one module that ->uses `CommandContext` and another that uses `SocketCommandContext`. - -[Context]: xref:Discord.Commands.ModuleBase`1#Discord_Commands_ModuleBase_1_Context -[SocketCommandContext]: xref:Discord.Commands.SocketCommandContext -[ReplyAsync]: xref:Discord.Commands.ModuleBase`1#Discord_Commands_ModuleBase_1_ReplyAsync_System_String_System_Boolean_Discord_Embed_Discord_RequestOptions_ - -### Example Module - -At this point, your module should look comparable to this example: -[!code-csharp[Example Module](samples/module.cs)] - -#### Loading Modules Automatically - -The Command Service can automatically discover all classes in an -Assembly that inherit [ModuleBase] and load them. - -To opt a module out of auto-loading, flag it with -[DontAutoLoadAttribute]. - -Invoke [CommandService.AddModulesAsync] to discover modules and -install them. - -[DontAutoLoadAttribute]: xref:Discord.Commands.DontAutoLoadAttribute -[CommandService.AddModulesAsync]: xref:Discord.Commands.CommandService#Discord_Commands_CommandService_AddModulesAsync_Assembly_ - -#### Loading Modules Manually - -To manually load a module, invoke [CommandService.AddModuleAsync] by -passing in the generic type of your module and optionally, a -dependency map. - -[CommandService.AddModuleAsync]: xref:Discord.Commands.CommandService#Discord_Commands_CommandService_AddModuleAsync__1 - -### Module Constructors - -Modules are constructed using Dependency Injection. Any parameters -that are placed in the Module's constructor must be injected into an -@System.IServiceProvider first. Alternatively, you may accept an -`IServiceProvider` as an argument and extract services yourself. - -### Module Properties - -Modules with `public` settable properties will have the dependencies -injected after the construction of the Module. - -### Module Groups - -Module Groups allow you to create a module where Commands are -prefixed. To create a group, flag a module with the -@Discord.Commands.GroupAttribute. - -Module groups also allow you to create **nameless Commands**, where -the [CommandAttribute] is configured with no name. In this case, the -Command will inherit the name of the group it belongs to. - -### Submodules - -Submodules are Modules that reside within another one. Typically, -submodules are used to create nested groups (although not required to -create nested groups). - -[!code-csharp[Groups and Submodules](samples/groups.cs)] - -## With Builders - -**TODO** - -## Dependency Injection - -The Command Service is bundled with a very barebone Dependency -Injection service for your convenience. It is recommended that you use -DI when writing your modules. - -### Setup - -First, you need to create an @System.IServiceProvider; you may create -your own one if you wish. - -Next, add the dependencies that your modules will use to the map. - -Finally, pass the map into the `LoadAssembly` method. Your modules -will be automatically loaded with this dependency map. - -[!code-csharp[IServiceProvider Setup](samples/dependency_map_setup.cs)] - -### Usage in Modules - -In the constructor of your Module, any parameters will be filled in by -the @System.IServiceProvider that you've passed into `LoadAssembly`. - -Any publicly settable properties will also be filled in the same -manner. - ->[!NOTE] -> Annotating a property with a [DontInjectAttribute] attribute will prevent the -property from being injected. - ->[!NOTE] ->If you accept `CommandService` or `IServiceProvider` as a parameter -in your constructor or as an injectable property, these entries will -be filled by the `CommandService` that the Module is loaded from and -the `ServiceProvider` that is passed into it respectively. - -[!code-csharp[ServiceProvider in Modules](samples/dependency_module.cs)] - -[DontInjectAttribute]: xref:Discord.Commands.DontInjectAttribute - -# Preconditions - -Precondition serve as a permissions system for your Commands. Keep in -mind, however, that they are not limited to _just_ permissions and can -be as complex as you want them to be. - ->[!NOTE] ->There are two types of Preconditions. -[PreconditionAttribute] can be applied to Modules, Groups, or Commands; -[ParameterPreconditionAttribute] can be applied to Parameters. - -[PreconditionAttribute]: xref:Discord.Commands.PreconditionAttribute -[ParameterPreconditionAttribute]: xref:Discord.Commands.ParameterPreconditionAttribute - -## Bundled Preconditions - -Commands ship with four bundled Preconditions; you may view their -usages on their respective API pages. - -- @Discord.Commands.RequireContextAttribute -- @Discord.Commands.RequireOwnerAttribute -- @Discord.Commands.RequireBotPermissionAttribute -- @Discord.Commands.RequireUserPermissionAttribute - -## Custom Preconditions - -To write your own Precondition, create a new class that inherits from -either [PreconditionAttribute] or [ParameterPreconditionAttribute] -depending on your use. - -In order for your Precondition to function, you will need to override -the [CheckPermissions] method. - -Your IDE should provide an option to fill this in for you. - -If the context meets the required parameters, return -[PreconditionResult.FromSuccess], otherwise return -[PreconditionResult.FromError] and include an error message if -necessary. - -[!code-csharp[Custom Precondition](samples/require_owner.cs)] - -[CheckPermissions]: xref:Discord.Commands.PreconditionAttribute#Discord_Commands_PreconditionAttribute_CheckPermissions_Discord_Commands_ICommandContext_Discord_Commands_CommandInfo_IServiceProvider_ -[PreconditionResult.FromSuccess]: xref:Discord.Commands.PreconditionResult#Discord_Commands_PreconditionResult_FromSuccess -[PreconditionResult.FromError]: xref:Discord.Commands.PreconditionResult#Discord_Commands_PreconditionResult_FromError_System_String_ - -# Type Readers - -Type Readers allow you to parse different types of arguments in -your commands. - -By default, the following Types are supported arguments: - -- bool -- char -- sbyte/byte -- ushort/short -- uint/int -- ulong/long -- float, double, decimal -- string -- DateTime/DateTimeOffset/TimeSpan -- IMessage/IUserMessage -- IChannel/IGuildChannel/ITextChannel/IVoiceChannel/IGroupChannel -- IUser/IGuildUser/IGroupUser -- IRole - -### Creating a Type Readers - -To create a `TypeReader`, create a new class that imports @Discord and -@Discord.Commands and ensure the class inherits from -@Discord.Commands.TypeReader. - -Next, satisfy the `TypeReader` class by overriding the [Read] method. - ->[!NOTE] ->In many cases, Visual Studio can fill this in for you, using the ->"Implement Abstract Class" IntelliSense hint. - -Inside this task, add whatever logic you need to parse the input -string. - -If you are able to successfully parse the input, return -[TypeReaderResult.FromSuccess] with the parsed input, otherwise return -[TypeReaderResult.FromError] and include an error message if -necessary. - -[TypeReaderResult]: xref:Discord.Commands.TypeReaderResult -[TypeReaderResult.FromSuccess]: xref:Discord.Commands.TypeReaderResult#Discord_Commands_TypeReaderResult_FromSuccess_Discord_Commands_TypeReaderValue_ -[TypeReaderResult.FromError]: xref:Discord.Commands.TypeReaderResult#Discord_Commands_TypeReaderResult_FromError_Discord_Commands_CommandError_System_String_ -[Read]: xref:Discord.Commands.TypeReader#Discord_Commands_TypeReader_Read_Discord_Commands_ICommandContext_System_String_IServiceProvider_ - -#### Sample - -[!code-csharp[TypeReaders](samples/typereader.cs)] - -### Installing TypeReaders - -TypeReaders are not automatically discovered by the Command Service -and must be explicitly added. - -To install a TypeReader, invoke [CommandService.AddTypeReader]. - -[CommandService.AddTypeReader]: xref:Discord.Commands.CommandService#Discord_Commands_CommandService_AddTypeReader__1_Discord_Commands_TypeReader_ diff --git a/docs/guides/commands/dependency-injection.md b/docs/guides/commands/dependency-injection.md new file mode 100644 index 000000000..db63e00c4 --- /dev/null +++ b/docs/guides/commands/dependency-injection.md @@ -0,0 +1,45 @@ +--- +uid: Guides.Commands.DI +title: Dependency Injection +--- + +# Dependency Injection + +The Command Service is bundled with a very barebone Dependency +Injection service for your convenience. It is recommended that you use +DI when writing your modules. + +## Setup + +1. Create an @System.IServiceProvider. +2. Add the dependencies to the service collection that you wish + to use in the modules. +3. Pass the service collection into `AddModulesAsync`. + +### Example - Setting up Injection + +[!code-csharp[IServiceProvider Setup](samples/dependency_map_setup.cs)] + +## Usage in Modules + +In the constructor of your module, any parameters will be filled in by +the @System.IServiceProvider that you've passed. + +Any publicly settable properties will also be filled in the same +manner. + +> [!NOTE] +> Annotating a property with a [DontInjectAttribute] attribute will +> prevent the property from being injected. + +> [!NOTE] +> If you accept `CommandService` or `IServiceProvider` as a parameter +> in your constructor or as an injectable property, these entries will +> be filled by the `CommandService` that the module is loaded from and +> the `IServiceProvider` that is passed into it respectively. + +### Example - Injection in Modules + +[!code-csharp[IServiceProvider in Modules](samples/dependency_module.cs)] + +[DontInjectAttribute]: xref:Discord.Commands.DontInjectAttribute \ No newline at end of file diff --git a/docs/guides/commands/intro.md b/docs/guides/commands/intro.md new file mode 100644 index 000000000..8036ed09e --- /dev/null +++ b/docs/guides/commands/intro.md @@ -0,0 +1,221 @@ +--- +uid: Guides.Commands.Intro +title: Introduction to Command Service +--- + +# The Command Service + +[Discord.Commands](xref:Discord.Commands) provides an attribute-based +command parser. + +## Get Started + +To use commands, you must create a [Command Service] and a command +handler. + +Included below is a barebone command handler. You can extend your +command handler as much as you like; however, the below is the bare +minimum. + +> [!NOTE] +> The `CommandService` will optionally accept a [CommandServiceConfig], +> which *does* set a few default values for you. It is recommended to +> look over the properties in [CommandServiceConfig] and their default +> values. + +[!code-csharp[Command Handler](samples/command_handler.cs)] + +[Command Service]: xref:Discord.Commands.CommandService +[CommandServiceConfig]: xref:Discord.Commands.CommandServiceConfig + +## With Attributes + +Starting from 1.0, commands can be defined ahead of time with +attributes, or at runtime with builders. + +For most bots, ahead-of-time commands should be all you need, and this +is the recommended method of defining commands. + +### Modules + +The first step to creating commands is to create a _module_. + +A module is an organizational pattern that allows you to write your +commands in different classes and have them automatically loaded. + +Discord.Net's implementation of "modules" is influenced heavily from +ASP.NET Core's Controller pattern. This means that the lifetime of a +module instance is only as long as the command is being invoked. + +Before we create a module, it is **crucial** for you to remember that +in order to create a module and have it automatically discovered, +your module must: + +* Be public +* Inherit [ModuleBase] + +By now, your module should look like this: + +[!code-csharp[Empty Module](samples/empty-module.cs)] + +> [!NOTE] +> [ModuleBase] is an `abstract` class, meaning that you may extend it +> or override it as you see fit. Your module may inherit from any +> extension of ModuleBase. + +[IoC]: https://msdn.microsoft.com/en-us/library/ff921087.aspx +[Dependency Injection]: https://msdn.microsoft.com/en-us/library/ff921152.aspx +[ModuleBase]: xref:Discord.Commands.ModuleBase`1 + +### Adding/Creating Commands + +> [!WARNING] +> **Avoid using long-running code** in your modules wherever possible. +> You should **not** be implementing very much logic into your +> modules, instead, outsource to a service for that. +> +> If you are unfamiliar with Inversion of Control, it is recommended +> to read the MSDN article on [IoC] and [Dependency Injection]. + +The next step to creating commands is actually creating the commands. + +For a command to be valid, it **must** have a return type of `Task` +or `Task`. Typically, you might want to mark this +method as `async`, although it is not required. + +Then, flag your command with the [CommandAttribute]. Note that you must +specify a name for this command, except for when it is part of a +[Module Group](#module-groups). + +### Command Parameters + +Adding parameters to a command is done by adding parameters to the +parent `Task`. + +For example: + +* To take an integer as an argument from the user, add `int num`. +* To take a user as an argument from the user, add `IUser user`. +* ...etc. + +Starting from 1.0, a command can accept nearly any type of argument; +a full list of types that are parsed by default can +be found in @Guides.Commands.TypeReaders. + +[CommandAttribute]: xref:Discord.Commands.CommandAttribute + +#### Optional Parameters + +Parameters, by default, are always required. To make a parameter +optional, give it a default value (i.e. `int num = 0`). + +#### Parameters with Spaces + +To accept a comma-separated list, set the parameter to `params Type[]`. + +Should a parameter include spaces, the parameter **must** be +wrapped in quotes. For example, for a command with a parameter +`string food`, you would execute it with +`!favoritefood "Key Lime Pie"`. + +If you would like a parameter to parse until the end of a command, +flag the parameter with the [RemainderAttribute]. This will +allow a user to invoke a command without wrapping a +parameter in quotes. + +[RemainderAttribute]: xref:Discord.Commands.RemainderAttribute + +### Command Overloads + +You may add overloads to your commands, and the command parser will +automatically pick up on it. + +If, for whatever reason, you have two commands which are ambiguous to +each other, you may use the @Discord.Commands.PriorityAttribute to +specify which should be tested before the other. + +The `Priority` attributes are sorted in ascending order; the higher +priority will be called first. + +### Command Context + +Every command can access the execution context through the [Context] +property on [ModuleBase]. `ICommandContext` allows you to access the +message, channel, guild, user, and the underlying Discord client +that the command was invoked from. + +Different types of `Context` may be specified using the generic variant +of [ModuleBase]. When using a [SocketCommandContext], for example, the +properties on this context will already be Socket entities, so you +will not need to cast them. + +To reply to messages, you may also invoke [ReplyAsync], instead of +accessing the channel through the [Context] and sending a message. + +> [!WARNING] +> Contexts should **NOT** be mixed! You cannot have one module that +> uses `CommandContext` and another that uses `SocketCommandContext`. + +[Context]: xref:Discord.Commands.ModuleBase`1.Context +[SocketCommandContext]: xref:Discord.Commands.SocketCommandContext +[ReplyAsync]: xref:Discord.Commands.ModuleBase`1.ReplyAsync* + +> [!TIP] +> At this point, your module should look comparable to this example: +> [!code-csharp[Example Module](samples/module.cs)] + +#### Loading Modules Automatically + +The Command Service can automatically discover all classes in an +`Assembly` that inherit [ModuleBase] and load them. Invoke +[CommandService.AddModulesAsync] to discover modules and +install them. + +To opt a module out of auto-loading, flag it with +[DontAutoLoadAttribute]. + +[DontAutoLoadAttribute]: xref:Discord.Commands.DontAutoLoadAttribute +[CommandService.AddModulesAsync]: xref:Discord.Commands.CommandService.AddModulesAsync* + +#### Loading Modules Manually + +To manually load a module, invoke [CommandService.AddModuleAsync] by +passing in the generic type of your module and optionally, a +service provider. + +[CommandService.AddModuleAsync]: xref:Discord.Commands.CommandService.AddModuleAsync* + +### Module Constructors + +Modules are constructed using @Guides.Commands.DI. Any parameters +that are placed in the Module's constructor must be injected into an +@System.IServiceProvider first. + +> [!TIP] +> Alternatively, you may accept an +> `IServiceProvider` as an argument and extract services yourself, +> although this is discouraged. + +### Module Properties + +Modules with `public` settable properties will have the dependencies +injected after the construction of the module. See @Guides.Commands.DI +to learn more. + +### Module Groups + +Module Groups allow you to create a module where commands are +prefixed. To create a group, flag a module with the +@Discord.Commands.GroupAttribute. + +Module Groups also allow you to create **nameless Commands**, where +the [CommandAttribute] is configured with no name. In this case, the +command will inherit the name of the group it belongs to. + +### Submodules + +Submodules are "modules" that reside within another one. Typically, +submodules are used to create nested groups (although not required to +create nested groups). + +[!code-csharp[Groups and Submodules](samples/groups.cs)] \ No newline at end of file diff --git a/docs/guides/commands/post-execution.md b/docs/guides/commands/post-execution.md new file mode 100644 index 000000000..5f19147cb --- /dev/null +++ b/docs/guides/commands/post-execution.md @@ -0,0 +1,122 @@ +--- +uid: Guides.Commands.PostExecution +title: Post-command Execution Handling +--- + +# Preface + +When developing commands, you may want to consider building a +post-execution handling system so you can have a finer control +over commands. Discord.Net offers several post-execution workflows +for you to work with. + +If you recall, in the [Command Guide], we've shown the following +example for executing and handling commands, + +[!code[Command Handler](samples/command_handler.cs)] + +You may notice that after we perform [ExecuteAsync], we store the +result and print it to the chat. This is essentially the most +basic post-execution handling. + +With this in mind, we could start doing things like the following, + +[!code[Basic Command Handler](samples/post-execution_basic.cs)] + +However, this may not always be preferred, because you are +creating your post-execution logic *with* the essential command +handler. This could lead to messy code and could potentially be a +violation of the SRP (Single Responsibility Principle). + +Another major issue is if your command is marked with +`RunMode.Async`, [ExecuteAsync] will **always** return a successful +[ExecuteResult] instead of the actual result. You can learn more +about the impact in the [FAQ](xref:FAQ.Commands). + +## CommandExecuted Event + +Enter [CommandExecuted], an event that was introduced in +Discord.Net 2.0. This event is raised whenever a command is +successfully executed **without any run-time exceptions** or **without +any parsing or precondition failure**. This means this event can be +used to streamline your post-execution design, and the best thing +about this event is that it is not prone to `RunMode.Async`'s +[ExecuteAsync] drawbacks. + +Thus, we can begin working on code such as: + +[!code[CommandExecuted demo](samples/command_executed_demo.cs)] + +So now we have a streamlined post-execution pipeline, great! What's +next? We can take this further by using [RuntimeResult]. + +### RuntimeResult + +`RuntimeResult` was originally introduced in 1.0 to allow +developers to centralize their command result logic. +In other words, it is a result type that is designed to be +returned when the command has finished its execution. + +However, it wasn't widely adopted due to the aforementioned +[ExecuteAsync] drawback. Since we now have access to a proper +result-handler via the [CommandExecuted] event, we can start +making use of this class. + +The best way to make use of it is to create your own version of +`RuntimeResult`. You can achieve this by inheriting the `RuntimeResult` +class. + +The following creates a bare-minimum required for a sub-class +of `RuntimeResult`, + +[!code[Base Use](samples/customresult_base.cs)] + +The sky's the limit from here. You can add any additional information +you'd like regarding the execution result. + +For example, you may want to add your own result type or other +helpful information regarding the execution, or something +simple like static methods to help you create return types easily. + +[!code[Extended Use](samples/customresult_extended.cs)] + +After you're done creating your own [RuntimeResult], you can +implement it in your command by marking the command return type to +`Task`. + +> [!NOTE] +> You must mark the return type as `Task` instead of +> `Task`. Only the former will be picked up when +> building the module. + +Here's an example of a command that utilizes such logic: + +[!code[Usage](samples/customresult_usage.cs)] + +And now we can check for it in our [CommandExecuted] handler: + +[!code[Usage](samples/command_executed_adv_demo.cs)] + +## CommandService.Log Event + +We have so far covered the handling of various result types, but we +haven't talked about what to do if the command enters a catastrophic +failure (i.e. exceptions). To resolve this, we can make use of the +[CommandService.Log] event. + +All exceptions thrown during a command execution will be caught and +be sent to the Log event under the [LogMessage.Exception] property +as a [CommandException] type. The [CommandException] class allows +us to access the exception thrown, as well as the context +of the command. + +[!code[Logger Sample](samples/command_exception_log.cs)] + +[CommandException]: xref:Discord.Commands.CommandException +[LogMessage.Exception]: xref:Discord.LogMessage.Exception +[CommandService.Log]: xref:Discord.Commands.CommandService.Log +[RuntimeResult]: xref:Discord.Commands.RuntimeResult +[CommandExecuted]: xref:Discord.Commands.CommandService.CommandExecuted +[ExecuteAsync]: xref:Discord.Commands.CommandService.ExecuteAsync* +[ExecuteResult]: xref:Discord.Commands.ExecuteResult +[Command Guide]: xref:Guides.Commands.Intro \ No newline at end of file diff --git a/docs/guides/commands/preconditions.md b/docs/guides/commands/preconditions.md new file mode 100644 index 000000000..7fe6dd3a8 --- /dev/null +++ b/docs/guides/commands/preconditions.md @@ -0,0 +1,58 @@ +--- +uid: Guides.Commands.Preconditions +title: Preconditions +--- + +# Preconditions + +Preconditions serve as a permissions system for your Commands. Keep in +mind, however, that they are not limited to _just_ permissions and can +be as complex as you want them to be. + +There are two types of Preconditions you can use: + +* [PreconditionAttribute] can be applied to Modules, Groups, or Commands. +* [ParameterPreconditionAttribute] can be applied to Parameters. + +You may visit their respective API documentation to find out more. + +[PreconditionAttribute]: xref:Discord.Commands.PreconditionAttribute +[ParameterPreconditionAttribute]: xref:Discord.Commands.ParameterPreconditionAttribute + +## Bundled Preconditions + +@Discord.Commands ships with several bundled Preconditions for you +to use. + +* @Discord.Commands.RequireContextAttribute +* @Discord.Commands.RequireOwnerAttribute +* @Discord.Commands.RequireBotPermissionAttribute +* @Discord.Commands.RequireUserPermissionAttribute +* @Discord.Commands.RequireNsfwAttribute + +## Custom Preconditions + +To write your own Precondition, create a new class that inherits from +either [PreconditionAttribute] or [ParameterPreconditionAttribute] +depending on your use. + +In order for your Precondition to function, you will need to override +the [CheckPermissionsAsync] method. + +If the context meets the required parameters, return +[PreconditionResult.FromSuccess], otherwise return +[PreconditionResult.FromError] and include an error message if +necessary. + +> [!NOTE] +> Visual Studio can help you implement missing members +> from the abstract class by using the "Implement Abstract Class" +> IntelliSense hint. + +### Example - Creating a Custom Precondition + +[!code-csharp[Custom Precondition](samples/require_owner.cs)] + +[CheckPermissionsAsync]: xref:Discord.Commands.PreconditionAttribute.CheckPermissionsAsync* +[PreconditionResult.FromSuccess]: xref:Discord.Commands.PreconditionResult.FromSuccess* +[PreconditionResult.FromError]: xref:Discord.Commands.PreconditionResult.FromError* diff --git a/docs/guides/commands/samples/command_exception_log.cs b/docs/guides/commands/samples/command_exception_log.cs new file mode 100644 index 000000000..2956a0203 --- /dev/null +++ b/docs/guides/commands/samples/command_exception_log.cs @@ -0,0 +1,13 @@ +public async Task LogAsync(LogMessage logMessage) +{ + // This casting type requries C#7 + if (logMessage.Exception is CommandException cmdException) + { + // We can tell the user that something unexpected has happened + await cmdException.Context.Channel.SendMessageAsync("Something went catastrophically wrong!"); + + // We can also log this incident + Console.WriteLine($"{cmdException.Context.User} failed to execute '{cmdException.Command.Name}' in {cmdException.Context.Channel}."); + Console.WriteLine(cmdException.ToString()); + } +} \ No newline at end of file diff --git a/docs/guides/commands/samples/command_executed_adv_demo.cs b/docs/guides/commands/samples/command_executed_adv_demo.cs new file mode 100644 index 000000000..1ce40c51d --- /dev/null +++ b/docs/guides/commands/samples/command_executed_adv_demo.cs @@ -0,0 +1,13 @@ +public async Task OnCommandExecutedAsync(CommandInfo command, ICommandContext context, IResult result) +{ + switch(result) + { + case MyCustomResult customResult: + // do something extra with it + break; + default: + if (!string.IsNullOrEmpty(result.ErrorReason)) + await context.Channel.SendMessageAsync(result.ErrorReason); + break; + } +} \ No newline at end of file diff --git a/docs/guides/commands/samples/command_executed_demo.cs b/docs/guides/commands/samples/command_executed_demo.cs new file mode 100644 index 000000000..8d8fb911b --- /dev/null +++ b/docs/guides/commands/samples/command_executed_demo.cs @@ -0,0 +1,38 @@ +public async Task SetupAsync() +{ + await _command.AddModulesAsync(Assembly.GetEntryAssembly(), _services); + // Hook the execution event + _command.CommandExecuted += OnCommandExecutedAsync; + // Hook the command handler + _client.MessageReceived += HandleCommandAsync; +} +public async Task OnCommandExecutedAsync(CommandInfo command, ICommandContext context, IResult result) +{ + // We have access to the information of the command executed, + // the context of the command, and the result returned from the + // execution in this event. + + // We can tell the user what went wrong + if (!string.IsNullOrEmpty(result?.ErrorReason)) + { + await context.Channel.SendMessageAsync(result.ErrorReason); + } + + // ...or even log the result (the method used should fit into + // your existing log handler) + await _log.LogAsync(new LogMessage(LogSeverity.Info, "CommandExecution", $"{command?.Name} was executed at {DateTime.UtcNow}.")); +} +public async Task HandleCommandAsync(SocketMessage msg) +{ + var message = messageParam as SocketUserMessage; + if (message == null) return; + int argPos = 0; + if (!(message.HasCharPrefix('!', ref argPos) || message.HasMentionPrefix(_client.CurrentUser, ref argPos))) return; + var context = new SocketCommandContext(_client, message); + var result = await _commands.ExecuteAsync(context, argPos, _services); + // Optionally, you may pass the result manually into your + // CommandExecuted event handler if you wish to handle parsing or + // precondition failures in the same method. + + // await OnCommandExecutedAsync(null, context, result); +} \ No newline at end of file diff --git a/docs/guides/commands/samples/command_handler.cs b/docs/guides/commands/samples/command_handler.cs index da2453aa8..470be2707 100644 --- a/docs/guides/commands/samples/command_handler.cs +++ b/docs/guides/commands/samples/command_handler.cs @@ -1,46 +1,20 @@ -using System; -using System.Threading.Tasks; -using System.Reflection; -using Discord; -using Discord.WebSocket; -using Discord.Commands; -using Microsoft.Extensions.DependencyInjection; - -public class Program +public class CommandHandle { - private CommandService _commands; - private DiscordSocketClient _client; - private IServiceProvider _services; - - private static void Main(string[] args) => new Program().StartAsync().GetAwaiter().GetResult(); + private readonly DiscordSocketClient _client; + private readonly CommandService _commands; - public async Task StartAsync() + public CommandHandle(DiscordSocketClient client) { - _client = new DiscordSocketClient(); + _client = client; _commands = new CommandService(); - - // Avoid hard coding your token. Use an external source instead in your code. - string token = "bot token here"; - - _services = new ServiceCollection() - .AddSingleton(_client) - .AddSingleton(_commands) - .BuildServiceProvider(); - - await InstallCommandsAsync(); - - await _client.LoginAsync(TokenType.Bot, token); - await _client.StartAsync(); - - await Task.Delay(-1); } - + public async Task InstallCommandsAsync() { // Hook the MessageReceived Event into our Command Handler _client.MessageReceived += HandleCommandAsync; // Discover all of the commands in this assembly and load them. - await _commands.AddModulesAsync(Assembly.GetEntryAssembly()); + await _commands.AddModulesAsync(Assembly.GetEntryAssembly(), _services); } private async Task HandleCommandAsync(SocketMessage messageParam) diff --git a/docs/guides/commands/samples/customresult_base.cs b/docs/guides/commands/samples/customresult_base.cs new file mode 100644 index 000000000..895a370c7 --- /dev/null +++ b/docs/guides/commands/samples/customresult_base.cs @@ -0,0 +1,6 @@ +public class MyCustomResult : RuntimeResult +{ + public MyCustomResult(CommandError? error, string reason) : base(error, reason) + { + } +} \ No newline at end of file diff --git a/docs/guides/commands/samples/customresult_extended.cs b/docs/guides/commands/samples/customresult_extended.cs new file mode 100644 index 000000000..6754c96aa --- /dev/null +++ b/docs/guides/commands/samples/customresult_extended.cs @@ -0,0 +1,10 @@ +public class MyCustomResult : RuntimeResult +{ + public MyCustomResult(CommandError? error, string reason) : base(error, reason) + { + } + public static MyCustomResult FromError(string reason) => + new MyCustomResult(CommandError.Unsuccessful, reason); + public static MyCustomResult FromSuccess(string reason = null) => + new MyCustomResult(null, reason); +} \ No newline at end of file diff --git a/docs/guides/commands/samples/customresult_usage.cs b/docs/guides/commands/samples/customresult_usage.cs new file mode 100644 index 000000000..f7ec303a2 --- /dev/null +++ b/docs/guides/commands/samples/customresult_usage.cs @@ -0,0 +1,10 @@ +public class MyModule : ModuleBase +{ + [Command("eat")] + public async Task ChooseAsync(string food) + { + if (food == "salad") + return MyCustomResult.FromError("No salad allowed!"); + return MyCustomResult.FromSuccess($"I'll take a {food}!"). + } +} \ No newline at end of file diff --git a/docs/guides/commands/samples/dependency_map_setup.cs b/docs/guides/commands/samples/dependency_map_setup.cs index a36925904..34e4c994e 100644 --- a/docs/guides/commands/samples/dependency_map_setup.cs +++ b/docs/guides/commands/samples/dependency_map_setup.cs @@ -14,5 +14,5 @@ public async Task InstallAsync(DiscordSocketClient client) .AddSingleton() .BuildServiceProvider(); // ... - await _commands.AddModulesAsync(Assembly.GetEntryAssembly()); + await _commands.AddModulesAsync(Assembly.GetEntryAssembly(), _services); } \ No newline at end of file diff --git a/docs/guides/commands/samples/dependency_module.cs b/docs/guides/commands/samples/dependency_module.cs index 561b0f6ac..3c9b51d7d 100644 --- a/docs/guides/commands/samples/dependency_module.cs +++ b/docs/guides/commands/samples/dependency_module.cs @@ -1,40 +1,30 @@ -using Discord; -using Discord.Commands; -using Discord.WebSocket; - -public class ModuleA : ModuleBase +public class DatabaseModule : ModuleBase { private readonly DatabaseService _database; // Dependencies can be injected via the constructor - public ModuleA(DatabaseService database) + public DatabaseModule(DatabaseService database) { _database = database; } - public async Task ReadFromDb() + [Command("read")] + public async Task ReadFromDbAsync() { - var x = _database.getX(); - await ReplyAsync(x); + await ReplyAsync(_database.GetData()); } } -public class ModuleB +public class MixModule : ModuleBase { - // Public settable properties will be injected - public AnnounceService { get; set; } + public AnnounceService AnnounceService { get; set; } - // Public properties without setters will not - public CommandService Commands { get; } + // Public properties without setters will not be injected + public ImageService ImageService { get; } // Public properties annotated with [DontInject] will not + // be injected [DontInject] - public NotificationService { get; set; } - - public ModuleB(CommandService commands) - { - Commands = commands; - } - -} + public NotificationService NotificationService { get; set; } +} \ No newline at end of file diff --git a/docs/guides/commands/samples/empty-module.cs b/docs/guides/commands/samples/empty-module.cs index 6483c7cd2..db62032c2 100644 --- a/docs/guides/commands/samples/empty-module.cs +++ b/docs/guides/commands/samples/empty-module.cs @@ -1,5 +1,7 @@ using Discord.Commands; +// Keep in mind your module **must** be public and inherit ModuleBase. +// If it isn't, it will not be discovered by AddModulesAsync! public class InfoModule : ModuleBase { diff --git a/docs/guides/commands/samples/module.cs b/docs/guides/commands/samples/module.cs index 1e3555501..e5fe70534 100644 --- a/docs/guides/commands/samples/module.cs +++ b/docs/guides/commands/samples/module.cs @@ -1,24 +1,25 @@ // Create a module with no prefix -public class Info : ModuleBase +public class InfoModule : ModuleBase { - // ~say hello -> hello + // ~say hello world -> hello world [Command("say")] [Summary("Echoes a message.")] - public async Task SayAsync([Remainder] [Summary("The text to echo")] string echo) - { - // ReplyAsync is a method on ModuleBase - await ReplyAsync(echo); - } + public Task SayAsync([Remainder] [Summary("The text to echo")] string echo) + => ReplyAsync(echo); + + // ReplyAsync is a method on ModuleBase } // Create a module with the 'sample' prefix [Group("sample")] -public class Sample : ModuleBase +public class SampleModule : ModuleBase { // ~sample square 20 -> 400 [Command("square")] [Summary("Squares a number.")] - public async Task SquareAsync([Summary("The number to square.")] int num) + public async Task SquareAsync( + [Summary("The number to square.")] + int num) { // We can also access the channel from the Command Context. await Context.Channel.SendMessageAsync($"{num}^2 = {Math.Pow(num, 2)}"); @@ -31,9 +32,12 @@ public class Sample : ModuleBase // ~sample userinfo 96642168176807936 --> Khionu#8708 // ~sample whois 96642168176807936 --> Khionu#8708 [Command("userinfo")] - [Summary("Returns info about the current user, or the user parameter, if one passed.")] + [Summary + ("Returns info about the current user, or the user parameter, if one passed.")] [Alias("user", "whois")] - public async Task UserInfoAsync([Summary("The (optional) user to get info for")] SocketUser user = null) + public async Task UserInfoAsync( + [Summary("The (optional) user to get info from")] + SocketUser user = null) { var userInfo = user ?? Context.Client.CurrentUser; await ReplyAsync($"{userInfo.Username}#{userInfo.Discriminator}"); diff --git a/docs/guides/commands/samples/post-execution_basic.cs b/docs/guides/commands/samples/post-execution_basic.cs new file mode 100644 index 000000000..19c7bed59 --- /dev/null +++ b/docs/guides/commands/samples/post-execution_basic.cs @@ -0,0 +1,11 @@ +var result = await _commands.ExecuteAsync(context, argPos, _services); +if (result.CommandError != null) + switch(result.CommandError) + { + case CommandError.BadArgCount: + await context.Channel.SendMessageAsync("Parameter count does not match any command's."); + break; + default: + await context.Channel.SendMessageAsync($"An error has occurred {result.ErrorReason}"); + break; + } \ No newline at end of file diff --git a/docs/guides/commands/samples/require_owner.cs b/docs/guides/commands/samples/require_owner.cs index 3611afab8..aa218539e 100644 --- a/docs/guides/commands/samples/require_owner.cs +++ b/docs/guides/commands/samples/require_owner.cs @@ -10,7 +10,7 @@ using System.Threading.Tasks; public class RequireOwnerAttribute : PreconditionAttribute { // Override the CheckPermissions method - public async override Task CheckPermissions(ICommandContext context, CommandInfo command, IServiceProvider services) + public async override Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) { // Get the ID of the bot's owner var ownerId = (await services.GetService().GetApplicationInfoAsync()).Owner.Id; diff --git a/docs/guides/commands/samples/typereader-register.cs b/docs/guides/commands/samples/typereader-register.cs new file mode 100644 index 000000000..292caea6f --- /dev/null +++ b/docs/guides/commands/samples/typereader-register.cs @@ -0,0 +1,29 @@ +public class CommandHandler +{ + private readonly CommandService _commands; + private readonly DiscordSocketClient _client; + private readonly IServiceProvider _services; + + public CommandHandler(CommandService commands, DiscordSocketClient client, IServiceProvider services) + { + _commands = commands; + _client = client; + _services = services; + } + + public async Task SetupAsync() + { + _client.MessageReceived += CommandHandleAsync; + + // Add BooleanTypeReader to type read for the type "bool" + _commands.AddTypeReader(typeof(bool), new BooleanTypeReader()); + + // Then register the modules + await _commands.AddModulesAsync(Assembly.GetEntryAssembly(), _services); + } + + public async Task CommandHandleAsync(SocketMessage msg) + { + // ... + } +} \ No newline at end of file diff --git a/docs/guides/commands/samples/typereader.cs b/docs/guides/commands/samples/typereader.cs index d2864a4c7..b792a9269 100644 --- a/docs/guides/commands/samples/typereader.cs +++ b/docs/guides/commands/samples/typereader.cs @@ -4,7 +4,7 @@ using Discord.Commands; public class BooleanTypeReader : TypeReader { - public override Task Read(ICommandContext context, string input, IServiceProvider services) + public override Task ReadAsync(ICommandContext context, string input, IServiceProvider services) { bool result; if (bool.TryParse(input, out result)) diff --git a/docs/guides/commands/typereaders.md b/docs/guides/commands/typereaders.md new file mode 100644 index 000000000..a502fb3c8 --- /dev/null +++ b/docs/guides/commands/typereaders.md @@ -0,0 +1,69 @@ +--- +uid: Guides.Commands.TypeReaders +title: Type Readers +--- + +# Type Readers + +Type Readers allow you to parse different types of arguments in +your commands. + +By default, the following Types are supported arguments: + +* `bool` +* `char` +* `sbyte`/`byte` +* `ushort`/`short` +* `uint`/`int` +* `ulong`/`long` +* `float`, `double`, `decimal` +* `string` +* `DateTime`/`DateTimeOffset`/`TimeSpan` +* `Nullable` where applicible +* Any implementation of `IChannel`/`IMessage`/`IUser`/`IRole` + +## Creating a Type Reader + +To create a `TypeReader`, create a new class that imports @Discord and +@Discord.Commands and ensure the class inherits from +@Discord.Commands.TypeReader. Next, satisfy the `TypeReader` class by +overriding the [ReadAsync] method. + +Inside this Task, add whatever logic you need to parse the input +string. + +If you are able to successfully parse the input, return +[TypeReaderResult.FromSuccess] with the parsed input, otherwise return +[TypeReaderResult.FromError] and include an error message if +necessary. + +> [!NOTE] +> Visual Studio can help you implement missing members +> from the abstract class by using the "Implement Abstract Class" +> IntelliSense hint. + +[TypeReaderResult]: xref:Discord.Commands.TypeReaderResult +[TypeReaderResult.FromSuccess]: xref:Discord.Commands.TypeReaderResult.FromSuccess* +[TypeReaderResult.FromError]: xref:Discord.Commands.TypeReaderResult.FromError* +[ReadAsync]: xref:Discord.Commands.TypeReader.ReadAsync* + +### Example - Creating a Type Reader + +[!code-csharp[TypeReaders](samples/typereader.cs)] + +## Registering a Type Reader + +TypeReaders are not automatically discovered by the Command Service +and must be explicitly added. + +To register a TypeReader, invoke [CommandService.AddTypeReader]. + +> [!IMPORTANT] +> TypeReaders must be added prior to module discovery, otherwise your +> TypeReaders may not work! + +[CommandService.AddTypeReader]: xref:Discord.Commands.CommandService.AddTypeReader* + +### Example - Adding a Type Reader + +[!code-csharp[Adding TypeReaders](samples/typereader-register.cs)] \ No newline at end of file diff --git a/docs/guides/concepts/connections.md b/docs/guides/concepts/connections.md index 30e5e55cd..324b67566 100644 --- a/docs/guides/concepts/connections.md +++ b/docs/guides/concepts/connections.md @@ -1,24 +1,21 @@ --- +uid: Guides.Concepts.ManageConnections title: Managing Connections --- In Discord.Net, once a client has been started, it will automatically -maintain a connection to Discord's gateway, until it is manually +maintain a connection to Discord's gateway until it is manually stopped. ### Usage To start a connection, invoke the `StartAsync` method on a client that -supports a WebSocket connection. - -These clients include the [DiscordSocketClient] and -[DiscordRpcClient], as well as Audio clients. - -To end a connection, invoke the `StopAsync` method. This will -gracefully close any open WebSocket or UdpSocket connections. +supports a WebSocket connection; to end a connection, invoke the +`StopAsync` method. This will gracefully close any open WebSocket or +UdpSocket connections. Since the Start/Stop methods only signal to an underlying connection -manager that a connection needs to be started, **they return before a +manager that a connection needs to be started, **they return before a connection is actually made.** As a result, you will need to hook into one of the connection-state @@ -27,25 +24,25 @@ ready for use. All clients provide a `Connected` and `Disconnected` event, which is raised respectively when a connection opens or closes. In the case of -the DiscordSocketClient, this does **not** mean that the client is +the [DiscordSocketClient], this does **not** mean that the client is ready to be used. -A separate event, `Ready`, is provided on DiscordSocketClient, which +A separate event, `Ready`, is provided on [DiscordSocketClient], which is raised only when the client has finished guild stream or guild sync, and has a complete guild cache. [DiscordSocketClient]: xref:Discord.WebSocket.DiscordSocketClient -[DiscordRpcClient]: xref:Discord.Rpc.DiscordRpcClient ### Samples [!code-csharp[Connection Sample](samples/events.cs)] -### Tips +### Reconnection -Avoid running long-running code on the gateway! If you deadlock the -gateway (as explained in [events]), the connection manager will be -unable to recover and reconnect. +> [!TIP] +> Avoid running long-running code on the gateway! If you deadlock the +> gateway (as explained in [events]), the connection manager will be +> unable to recover and reconnect. Assuming the client disconnected because of a fault on Discord's end, and not a deadlock on your end, we will always attempt to reconnect @@ -55,4 +52,4 @@ Don't worry about trying to maintain your own connections, the connection manager is designed to be bulletproof and never fail - if your client doesn't manage to reconnect, you've found a bug! -[events]: events.md \ No newline at end of file +[events]: xref:Guides.Concepts.Events diff --git a/docs/guides/concepts/deployment.md b/docs/guides/concepts/deployment.md new file mode 100644 index 000000000..d26abee34 --- /dev/null +++ b/docs/guides/concepts/deployment.md @@ -0,0 +1,86 @@ +--- +uid: Guides.Concepts.Deployment +title: Deploying the Bot +--- + +# Deploying the Bot + +After finishing your application, you may want to deploy your bot to a +remote location such as a Virtual Private Server (VPS) or another +computer so you can keep the bot up and running 24/7. + +## Recommended VPS + +For small-medium scaled bots, a cheap VPS (~$5) might be sufficient +enough. Here is a list of recommended VPS provider. + +* [DigitalOcean](https://www.digitalocean.com/) + * Description: American cloud infrastructure provider headquartered + in New York City with data centers worldwide. + * Location(s): + * Asia: Singapore, India + * America: Canada, United States + * Europe: Netherlands, Germany, United Kingdom + * Based in: United States +* [Vultr](https://www.vultr.com/) + * Description: DigitalOcean-like + * Location(s): + * Asia: Japan, Australia, Singapore + * America: United States + * Europe: United Kingdom, France, Netherlands, Germany + * Based in: United States +* [OVH](https://www.ovh.com/) + * Description: French cloud computing company that offers VPS, + dedicated servers and other web services. + * Location(s): + * Asia: Australia, Singapore + * America: United States, Canada + * Europe: United Kingdom, Poland, Germany + * Based in: Europe +* [Scaleway](https://www.scaleway.com/) + * Description: Cheap but powerful VPS owned by [Online.net](https://online.net/). + * Location(s): + * Europe: France, Netherlands + * Based in: Europe +* [Time4VPS](https://www.time4vps.eu/) + * Description: Affordable and powerful VPS Hosting in Europe. + * Location(s): + * Europe: Lithuania + * Based in: Europe + +## .NET Core Deployment + +> [!NOTE] +> This section only covers the very basics of .NET Core deployment. +> To learn more about deployment, visit [.NET Core application deployment] +> by Microsoft. + +By default, .NET Core compiles all projects as a DLL file, so that any +.NET Core runtime can execute the application. + +You may execute the application via `dotnet myprogram.dll` assuming you +have the dotnet CLI installed. + +When redistributing the application, you may want to publish the +application, or in other words, create a self-contained package +for use on another machine without installing the dependencies first. + +This can be achieved by using the dotnet CLI too on the development +machine: + + `dotnet publish -c Release` + +Additionally, you may want to target a specific platform when +publishing the application so you may use the application without +having to install the Core runtime on the target machine. To do this, +you may specify an [Runtime ID] upon build/publish with the `-r` +option. + +For example, when targeting a Windows 10 machine, you may want to use +the following to create the application in Windows executable +format (.exe): + + `dotnet publish -c Release -r win10-x64` + +[.NET Core application deployment]: https://docs.microsoft.com/en-us/dotnet/core/deploying/ +[Runtime ID]: https://docs.microsoft.com/en-us/dotnet/core/rid-catalog \ No newline at end of file diff --git a/docs/guides/concepts/entities.md b/docs/guides/concepts/entities.md index 3a5d5496b..7c66c7a57 100644 --- a/docs/guides/concepts/entities.md +++ b/docs/guides/concepts/entities.md @@ -1,23 +1,26 @@ --- +uid: Guides.Concepts.Entities title: Entities --- ->[!NOTE] -This article is written with the Socket variants of entities in mind, -not the general interfaces or Rest/Rpc entities. +# Entities in Discord.Net + +> [!NOTE] +> This article is written with the Socket variants of entities in mind, +> not the general interfaces or Rest/Rpc entities. Discord.Net provides a versatile entity system for navigating the Discord API. -### Inheritance +## Inheritance Due to the nature of the Discord API, some entities are designed with multiple variants; for example, `SocketUser` and `SocketGuildUser`. All models will contain the most detailed version of an entity -possible, even if the type is less detailed. +possible, even if the type is less detailed. -For example, in the case of the `MessageReceived` event, a +For example, in the case of the `MessageReceived` event, a `SocketMessage` is passed in with a channel property of type `SocketMessageChannel`. All messages come from channels capable of messaging, so this is the only variant of a channel that can cover @@ -28,44 +31,36 @@ But that doesn't mean a message _can't_ come from a retrieve information about a guild from a message entity, you will need to cast its channel object to a `SocketTextChannel`. -### Navigation +You can find out various types of entities in the @FAQ.Misc.Glossary +page. + +## Navigation All socket entities have navigation properties on them, which allow you to easily navigate to an entity's parent or children. As explained above, you will sometimes need to cast to a more detailed version of an entity to navigate to its parent. -### Accessing Entities +## Accessing Entities The most basic forms of entities, `SocketGuild`, `SocketUser`, and `SocketChannel` can be pulled from the DiscordSocketClient's global cache, and can be retrieved using the respective `GetXXX` method on DiscordSocketClient. ->[!TIP] -It is **vital** that you use the proper IDs for an entity when using -a GetXXX method. It is recommended that you enable Discord's -_developer mode_ to allow easy access to entity IDs, found in -Settings > Appearance > Advanced +> [!TIP] +> It is **vital** that you use the proper IDs for an entity when using +> a `GetXXX` method. It is recommended that you enable Discord's +> _developer mode_ to allow easy access to entity IDs, found in +> Settings > Appearance > Advanced. Read more about it in the +> [FAQ](xref:FAQ.Basics.GetStarted) page. More detailed versions of entities can be pulled from the basic -entities, e.g. `SocketGuild.GetUser`, which returns a -`SocketGuildUser`, or `SocketGuild.GetChannel`, which returns a +entities, e.g. `SocketGuild.GetUser`, which returns a +`SocketGuildUser`, or `SocketGuild.GetChannel`, which returns a `SocketGuildChannel`. Again, you may need to cast these objects to get a variant of the type that you need. -### Samples - -[!code-csharp[Entity Sample](samples/entities.cs)] - -### Tips - -Avoid using boxing-casts to coerce entities into a variant, use the -[`as`] keyword, and a null-conditional operator instead. - -This allows you to write safer code and avoid [InvalidCastExceptions]. - -For example, `(message.Author as SocketGuildUser)?.Nickname`. +## Sample -[`as`]: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/as -[InvalidCastExceptions]: https://msdn.microsoft.com/en-us/library/system.invalidcastexception(v=vs.110).aspx \ No newline at end of file +[!code-csharp[Entity Sample](samples/entities.cs)] \ No newline at end of file diff --git a/docs/guides/concepts/events.md b/docs/guides/concepts/events.md index 47db49aa8..d8e586681 100644 --- a/docs/guides/concepts/events.md +++ b/docs/guides/concepts/events.md @@ -1,16 +1,19 @@ --- +uid: Guides.Concepts.Events title: Working with Events --- +# Events in Discord.Net + Events in Discord.Net are consumed in a similar manner to the standard convention, with the exception that every event must be of the type -`System.Threading.Tasks.Task` and instead of using `EventArgs`, the -event's parameters are passed directly into the handler. +@System.Threading.Tasks.Task and instead of using @System.EventArgs, +the event's parameters are passed directly into the handler. This allows for events to be handled in an async context directly instead of relying on `async void`. -### Usage +## Usage To receive data from an event, hook into it using C#'s delegate event pattern. @@ -18,7 +21,7 @@ event pattern. You may either opt to hook an event to an anonymous function (lambda) or a named function. -### Safety +## Safety All events are designed to be thread-safe; events are executed synchronously off the gateway task in the same context as the gateway @@ -39,7 +42,7 @@ a deadlock that will be impossible to recover from. Exceptions in commands will be swallowed by the gateway and logged out through the client's log method. -### Common Patterns +## Common Patterns As you may know, events in Discord.Net are only given a signature of `Func`. There is no room for predefined argument names, @@ -49,7 +52,7 @@ directly. That being said, there are a variety of common patterns that allow you to infer what the parameters in an event mean. -#### Entity, Entity +### Entity, Entity An event handler with a signature of `Func` typically means that the first object will be a clone of the entity @@ -58,10 +61,10 @@ model of the entity _after_ the change was made. This pattern is typically only found on `EntityUpdated` events. -#### Cacheable +### Cacheable An event handler with a signature of `Func` -means that the `before` state of the entity was not provided by the +means that the `before` state of the entity was not provided by the API, so it can either be pulled from the client's cache or downloaded from the API. @@ -70,15 +73,12 @@ object. [Cacheable]: xref:Discord.Cacheable`2 -### Samples - -[!code-csharp[Event Sample](samples/events.cs)] +> [!NOTE] +> Many events relating to a Message entity (i.e. `MessageUpdated` and +> `ReactionAdded`) rely on the client's message cache, which is +> **not** enabled by default. Set the `MessageCacheSize` flag in +> @Discord.WebSocket.DiscordSocketConfig to enable it. -### Tips +## Sample -Many events relating to a Message entity (i.e. `MessageUpdated` and -`ReactionAdded`) rely on the client's message cache, which is -**not** enabled by default. Set the `MessageCacheSize` flag in -[DiscordSocketConfig] to enable it. - -[DiscordSocketConfig]: xref:Discord.WebSocket.DiscordSocketConfig \ No newline at end of file +[!code-csharp[Event Sample](samples/events.cs)] diff --git a/docs/guides/concepts/logging.md b/docs/guides/concepts/logging.md index 50d2e9546..dba78006f 100644 --- a/docs/guides/concepts/logging.md +++ b/docs/guides/concepts/logging.md @@ -1,19 +1,28 @@ --- -title: Logging +uid: Guides.Concepts.Logging +title: Logging Events/Data --- -Discord.Net's clients provide a [Log] event that all messages will be -disbatched over. +# Logging in Discord.Net + +Discord.Net's clients provide a log event that all messages will be +dispatched over. For more information about events in Discord.Net, see the [Events] section. -[Log]: xref:Discord.Rest.BaseDiscordClient#Discord_Rest_BaseDiscordClient_Log -[Events]: events.md +[Events]: xref:Guides.Concepts.Events + +> [!WARNING] +> Due to the nature of Discord.Net's event system, all log event +> handlers will be executed synchronously on the gateway thread. If your +> log output will be dumped to a Web API (e.g. Sentry), you are advised +> to wrap your output in a `Task.Run` so the gateway thread does not +> become blocked while waiting for logging data to be written. -### Usage +## Usage in Client(s) -To receive log events, simply hook the discord client's log method +To receive log events, simply hook the Discord client's @Discord.Rest.BaseDiscordClient.Log to a `Task` with a single parameter of type [LogMessage]. It is recommended that you use an established function instead of a @@ -22,10 +31,10 @@ to a logging function to write their own messages. [LogMessage]: xref:Discord.LogMessage -### Usage in Commands +## Usage in Commands -Discord.Net's [CommandService] also provides a log event, identical -in signature to other log events. +Discord.Net's [CommandService] also provides a @Discord.Commands.CommandService.Log +event, identical in signature to other log events. Data logged through this event is typically coupled with a [CommandException], where information about the command's context @@ -34,14 +43,6 @@ and error can be found and handled. [CommandService]: xref:Discord.Commands.CommandService [CommandException]: xref:Discord.Commands.CommandException -#### Samples +## Sample [!code-csharp[Logging Sample](samples/logging.cs)] - -#### Tips - -Due to the nature of Discord.Net's event system, all log event -handlers will be executed synchronously on the gateway thread. If your -log output will be dumped to a Web API (e.g. Sentry), you are advised -to wrap your output in a `Task.Run` so the gateway thread does not -become blocked while waiting for logging data to be written. \ No newline at end of file diff --git a/docs/guides/concepts/samples/entities.cs b/docs/guides/concepts/samples/entities.cs index 7655c44e9..64383858d 100644 --- a/docs/guides/concepts/samples/entities.cs +++ b/docs/guides/concepts/samples/entities.cs @@ -1,13 +1,11 @@ public string GetChannelTopic(ulong id) { var channel = client.GetChannel(81384956881809408) as SocketTextChannel; - if (channel == null) return ""; - return channel.Topic; + return channel?.Topic; } -public string GuildOwner(SocketChannel channel) +public SocketGuildUser GetGuildOwner(SocketChannel channel) { var guild = (channel as SocketGuildChannel)?.Guild; - if (guild == null) return ""; - return Context.Guild.Owner.Username; + return guild?.Owner; } \ No newline at end of file diff --git a/docs/guides/getting_started/first-bot.md b/docs/guides/getting_started/first-bot.md new file mode 100644 index 000000000..622709eca --- /dev/null +++ b/docs/guides/getting_started/first-bot.md @@ -0,0 +1,248 @@ +--- +uid: Guides.GettingStarted.FirstBot +title: Start making a bot +--- + +# Making a Ping-Pong bot + +One of ways to get started with the Discord API is to write a basic +ping-pong bot. This bot will respond to a simple command "ping." +We will expand on this to create more diverse commands later, but for +now, it is a good starting point. + +## Creating a Discord Bot + +Before writing your bot, it is necessary to create a bot account via the +Discord Applications Portal first. + +1. Visit the [Discord Applications Portal]. +2. Create a New Application. +3. Give the application a name (this will be the bot's initial username). +4. Create the Application. + + ![Step 4](images/intro-create-app.png) + +5. In the application review page, click **Create a Bot User**. + + ![Step 5](images/intro-create-bot.png) + +6. Confirm the popup. +7. If this bot will be public, check "Public Bot." **Do not tick any + other options!** + +[Discord Applications Portal]: https://discordapp.com/developers/applications/me + +## Adding your bot to a server + +Bots **cannot** use invite links; they must be explicitly invited +through the OAuth2 flow. + +1. Open your bot's application on the [Discord Applications Portal]. +2. Retrieve the application's **Client ID**. + + ![Step 2](images/intro-client-id.png) + +3. Create an OAuth2 authorization URL + + - `https://discordapp.com/oauth2/authorize?client_id=&scope=bot` + +4. Open the authorization URL in your browser. +5. Select a server. +6. Click on authorize. + + > [!NOTE] + > Only servers where you have the `MANAGE_SERVER` permission will be + > present in this list. + + ![Step 6](images/intro-add-bot.png) + +## Connecting to Discord + +If you have not already created a project and installed Discord.Net, +do that now. + +For more information, see @Guides.GettingStarted.Installation. + +### Async + +Discord.Net uses .NET's [Task-based Asynchronous Pattern (TAP)] +extensively - nearly every operation is asynchronous. It is highly +recommended that these operations are awaited in a +properly established async context whenever possible. + +To establish an async context, we will be creating an async main method +in your console application, and rewriting the static main method to +invoke the new async main. + +[!code-csharp[Async Context](samples/first-bot/async-context.cs)] + +As a result of this, your program will now start and immediately +jump into an async context. This will allow us to create a connection +to Discord later on without having to worry about setting up the +correct async implementation. + +> [!WARNING] +> If your application throws any exceptions within an async context, +> they will be thrown all the way back up to the first non-async method; +> since our first non-async method is the program's `Main` method, this +> means that **all** unhandled exceptions will be thrown up there, which +> will crash your application. +> +> Discord.Net will prevent exceptions in event handlers from crashing +> your program, but any exceptions in your async main **will** cause +> the application to crash. + +[Task-based Asynchronous Pattern (TAP)]: https://docs.microsoft.com/en-us/dotnet/articles/csharp/async + +### Creating a logging method + +Before we create and configure a Discord client, we will add a method +to handle Discord.Net's log events. + +To allow agnostic support of as many log providers as possible, we +log information through a `Log` event with a proprietary `LogMessage` +parameter. See the [API Documentation] for this event. + +If you are using your own logging framework, this is where you would +invoke it. For the sake of simplicity, we will only be logging to +the console. + +You may learn more about this concept in @Guides.Concepts.Logging. + +[!code-csharp[Async Context](samples/first-bot/logging.cs)] + +[API Documentation]: xref:Discord.Rest.BaseDiscordClient.Log + +### Creating a Discord Client + +Finally, we can create a new connection to Discord. + +Since we are writing a bot, we will be using a [DiscordSocketClient] +along with socket entities. See @Guides.GettingStarted.Terminology +if you are unsure of the differences. + +To establish a new connection, we will create an instance of +[DiscordSocketClient] in the new async main. You may pass in an +optional @Discord.WebSocket.DiscordSocketConfig if necessary. For most +users, the default will work fine. + +Before connecting, we should hook the client's `Log` event to the +log handler that we had just created. Events in Discord.Net work +similarly to any other events in C#. + +Next, you will need to "login to Discord" with the [LoginAsync] +method with the application's "token." + +> [!NOTE] +> Pay attention to what you are copying from the developer portal! +> A token is not the same as the application's "client secret." + +![Token](images/intro-token.png) + +> [!IMPORTANT] +> Your bot's token can be used to gain total access to your bot, so +> **do __NOT__ share this token with anyone else!** It may behoove you +> to store this token in an external source if you plan on distributing +> the source code for your bot. + +We may now invoke the client's [StartAsync] method, which will +start connection/reconnection logic. It is important to note that +**this method will return as soon as connection logic has been started!** + +Any methods that rely on the client's state should go in an event +handler. This means that you should **not** directly be interacting with +the client before it is fully ready. + +Finally, we will want to block the async main method from returning +when running the application. To do this, we can await an infinite delay +or any other blocking method, such as reading from the console. + +The following lines can now be added: + +[!code-csharp[Create client](samples/first-bot/client.cs)] + +At this point, feel free to start your program and see your bot come +online in Discord. + +> [!TIP] +> Encountering a `PlatformNotSupportedException` when starting your bot? +> This means that you are targeting a platform where .NET's default +> WebSocket client is not supported. Refer to the [installation guide] +> for how to fix this. + +[DiscordSocketClient]: xref:Discord.WebSocket.DiscordSocketClient +[LoginAsync]: xref:Discord.Rest.BaseDiscordClient.LoginAsync* +[StartAsync]: xref:Discord.WebSocket.DiscordSocketClient.StartAsync* +[installation guide]: xref:Guides.GettingStarted.Installation#installing-on-net-standard-11 + +### Handling a 'ping' + +> [!WARNING] +> Please note that this is *not* a proper way to create a command. +> Use the `CommandService` provided by the library instead, as explained +> in the [Command Guide](xref:Guides.Commands.Intro) section. + +Now that we have learned to open a connection to Discord, we can +begin handling messages that the users are sending. To start out, our +bot will listen for any message whose content is equal to `!ping` and +will respond back with "Pong!". + +Since we want to listen for new messages, the event to hook into +is [MessageReceived]. + +In your program, add a method that matches the signature of the +`MessageReceived` event - it must be a method (`Func`) that returns +the type `Task` and takes a single parameter, a [SocketMessage]. Also, +since we will be sending data to Discord in this method, we will flag +it as `async`. + +In this method, we will add an `if` block to determine if the message +content fits the rules of our scenario - recall that it must be equal +to `!ping`. + +Inside the branch of this condition, we will want to send a message, +`Pong!`, back to the channel from which the message comes from. To +find the channel, look for the `Channel` property on the message +parameter. + +Next, we will want to send a message to this channel. Since the +channel object is of type [ISocketMessageChannel], we can invoke the +[SendMessageAsync] instance method. For the message content, send back +a string, "Pong!". + +You should have now added the following lines, + +[!code-csharp[Message](samples/first-bot/message.cs)] + +Now that your first bot is complete. You may continue to add on to this +if you desire, but for any bots that will be carrying out multiple +commands, it is strongly recommended to use the command framework as +shown below. + +> [!NOTE] +> For your reference, you may view the [completed program]. + +[MessageReceived]: xref:Discord.WebSocket.BaseSocketClient.MessageReceived +[SocketMessage]: xref:Discord.WebSocket.SocketMessage +[ISocketMessageChannel]: xref:Discord.WebSocket.ISocketMessageChannel +[SendMessageAsync]: xref:Discord.WebSocket.ISocketMessageChannel.SendMessageAsync* +[completed program]: samples/first-bot/complete.cs + +# Building a bot with commands + +@Guides.Commands.Intro will guide you through how to setup a program +that is ready for [CommandService], a service that is ready for +advanced command usage. + +For reference, view an [annotated example] of this structure. + +[annotated example]: samples/first-bot/structure.cs + +It is important to know that the recommended design pattern of bots +should be to separate... + +1. the program (initialization and command handler) +2. the modules (handle commands) +3. the services (persistent storage, pure functions, data manipulation) + +[CommandService]: xref:Discord.Commands.CommandService \ No newline at end of file diff --git a/docs/guides/getting_started/installing.md b/docs/guides/getting_started/installing.md index 5d4c85d81..c0980101e 100644 --- a/docs/guides/getting_started/installing.md +++ b/docs/guides/getting_started/installing.md @@ -1,38 +1,48 @@ --- +uid: Guides.GettingStarted.Installation title: Installing Discord.Net --- -Discord.Net is distributed through the NuGet package manager, and it -is recommended to use NuGet to get started. +# Discord.Net Installation -Optionally, you may compile from source and install yourself. +Discord.Net is distributed through the NuGet package manager, so it is +recommended for you to install the library that way. -# Supported Platforms +Alternatively, you may compile from the source and install the library +yourself. + +## Supported Platforms Currently, Discord.Net targets [.NET Standard] 1.3 and offers support for .NET Standard 1.1. If your application will be targeting .NET Standard 1.1, please see the [additional steps]. -Since Discord.Net is built on the .NET Standard, it is also -recommended to create applications using [.NET Core], though not +Since Discord.Net is built on top of .NET Standard, it is also +recommended to create applications using [.NET Core], although it is not required. When using .NET Framework, it is suggested to target `.NET Framework 4.6.1` or higher. +> [!WARNING] +> Using this library with [Mono] is not recommended until further +> notice. It is known to have issues with the library's WebSockets +> implementation and may crash the application upon startup. + +[Mono]: https://www.mono-project.com/ [.NET Standard]: https://docs.microsoft.com/en-us/dotnet/articles/standard/library [.NET Core]: https://docs.microsoft.com/en-us/dotnet/articles/core/ [additional steps]: #installing-on-net-standard-11 -# Installing with NuGet +## Installing with NuGet -Release builds of Discord.Net 1.0 will be published to the +Release builds of Discord.Net will be published to the [official NuGet feed]. -Development builds of Discord.Net 1.0, as well as addons *(TODO)* are -published to our development [MyGet feed]. +Development builds of Discord.Net, as well as add-ons, will be +published to our [MyGet feed]. Direct feed link: `https://www.myget.org/F/discord-net/api/v3/index.json` -Not sure how to add a direct feed? See how [with Visual Studio] or +Not sure how to add a direct feed? See how [with Visual Studio] or [without Visual Studio]. [official NuGet feed]: https://nuget.org @@ -40,85 +50,101 @@ Not sure how to add a direct feed? See how [with Visual Studio] or [with Visual Studio]: https://docs.microsoft.com/en-us/nuget/tools/package-manager-ui#package-sources [without Visual Studio]: #configuring-nuget-without-visual-studio -## Using Visual Studio +### [Using Visual Studio](#tab/vs-install) > [!TIP] ->Don't forget to change your package source if you're installing from -the developer feed. ->Also make sure to check "Enable Prereleases" if installing a dev -build! - -1. Create a solution for your bot. -2. In Solution Explorer, find the "Dependencies" element under your -bot's project. +> Don't forget to change your package source if you're installing from +> the developer feed. +> Also make sure to check "Enable Prereleases" if installing a dev +> build! + +1. Create a new solution for your bot. +2. In the Solution Explorer, find the "Dependencies" element under your + bot's project. 3. Right click on "Dependencies", and select "Manage NuGet packages." -![Step 3](images/install-vs-deps.png) + ![Step 3](images/install-vs-deps.png) 4. In the "Browse" tab, search for `Discord.Net`. 5. Install the `Discord.Net` package. -![Step 5](images/install-vs-nuget.png) + ![Step 5](images/install-vs-nuget.png) -## Using JetBrains Rider +### [Using JetBrains Rider](#tab/rider-install) > [!TIP] -Make sure to check the "Prerelease" box if installing a dev build! +> Make sure to check the "Prerelease" box if installing a dev build! 1. Create a new solution for your bot. 2. Open the NuGet window (Tools > NuGet > Manage NuGet packages for -Solution). + Solution). ![Step 2](images/install-rider-nuget-manager.png) 3. In the "Packages" tab, search for `Discord.Net`. ![Step 3](images/install-rider-search.png) 4. Install by adding the package to your project. ![Step 4](images/install-rider-add.png) -## Using Visual Studio Code +### [Using Visual Studio Code](#tab/vs-code) > [!TIP] -Don't forget to add the package source to a [NuGet.Config file] if -you're installing from the developer feed. +> Don't forget to add the package source to a [NuGet.Config file] if +> you're installing from the developer feed. 1. Create a new project for your bot. 2. Add `Discord.Net` to your .csproj. -[!code-xml[Sample .csproj](samples/project.csproj)] +[!code[Sample .csproj](samples/project.xml)] + +[NuGet.Config file]: #configuring-nuget-without-visual-studio + +### [Using dotnet CLI](#tab/dotnet-cli) + +> [!TIP] +> Don't forget to add the package source to a [NuGet.Config file] if +> you're installing from the developer feed. + +1. Open command-line and navigate to where your .csproj is located. +2. Enter `dotnet add package Discord.Net`. [NuGet.Config file]: #configuring-nuget-without-visual-studio -# Compiling from Source +*** + +## Compiling from Source -In order to compile Discord.Net, you require the following: +In order to compile Discord.Net, you will need the following: ### Using Visual Studio - [Visual Studio 2017](https://www.visualstudio.com/) -- [.NET Core SDK 1.0](https://www.microsoft.com/net/download/core#/sdk) +- [.NET Core SDK] The .NET Core and Docker (Preview) workload is required during Visual Studio installation. ### Using Command Line -- [.NET Core SDK 1.0](https://www.microsoft.com/net/download/core#/sdk) +- [.NET Core SDK] -# Additional Information +[.NET Core SDK]: https://www.microsoft.com/net/download/ -## Installing on .NET Standard 1.1 +## Additional Information + +### Installing on .NET Standard 1.1 For applications targeting a runtime corresponding with .NET Standard -1.1 or 1.2, the builtin WebSocket and UDP provider will not work. For -applications which utilize a WebSocket connection to Discord -(WebSocket or RPC), third-party provider packages will need to be +1.1 or 1.2, the built-in WebSocket and UDP provider will not work. For +applications which utilize a WebSocket connection to Discord, such as +WebSocket or RPC, third-party provider packages will need to be installed and configured. -First, install the following packages through NuGet, or compile +> [!NOTE] +> `Discord.Net.Providers.UDPClient` is _only_ required if your +> bot will be utilizing voice chat. + +First, install the following packages through NuGet, or compile them yourself, if you prefer: - Discord.Net.Providers.WS4Net - Discord.Net.Providers.UDPClient -Note that `Discord.Net.Providers.UDPClient` is _only_ required if your -bot will be utilizing voice chat. - Next, you will need to configure your [DiscordSocketClient] to use these custom providers over the default ones. @@ -131,16 +157,16 @@ are passing into your client. [DiscordSocketClient]: xref:Discord.WebSocket.DiscordSocketClient [DiscordSocketConfig]: xref:Discord.WebSocket.DiscordSocketConfig -## Configuring NuGet without Visual Studio +### Configuring NuGet without Visual Studio If you plan on deploying your bot or developing outside of Visual Studio, you will need to create a local NuGet configuration file for your project. -To do this, create a file named `nuget.config` alongside the root of -your application, where the project solution is located. +To do this, create a file named `NuGet.Config` alongside the root of +your application, where the project is located. Paste the following snippets into this configuration file, adding any -additional feeds as necessary. +additional feeds if necessary. -[!code-xml[NuGet Configuration](samples/nuget.config)] +[!code[NuGet Configuration](samples/nuget.config)] diff --git a/docs/guides/getting_started/intro.md b/docs/guides/getting_started/intro.md deleted file mode 100644 index db086df21..000000000 --- a/docs/guides/getting_started/intro.md +++ /dev/null @@ -1,237 +0,0 @@ ---- -title: Getting Started ---- - -# Making a Ping-Pong bot - -One of the first steps to getting started with the Discord API is to -write a basic ping-pong bot. We will expand on this to create more -diverse commands later, but for now, it is a good starting point. - -## Creating a Discord Bot - -Before you can begin writing your bot, it is necessary to create a bot -account on Discord. - -1. Visit the [Discord Applications Portal]. -2. Create a New Application. -3. Give the application a name (this will be the bot's initial -username). -4. Create the Application. - - ![Step 4](images/intro-create-app.png) - -5. In the application review page, click **Create a Bot User**. - - ![Step 5](images/intro-create-bot.png) - -6. Confirm the popup. -7. If this bot will be public, check "Public Bot." **Do not tick any -other options!** - -[Discord Applications Portal]: https://discordapp.com/developers/applications/me - -## Adding your bot to a server - -Bots **cannot** use invite links, they must be explicitly invited -through the OAuth2 flow. - -1. Open your bot's application on the [Discord Applications Portal]. -2. Retrieve the app's **Client ID**. - - ![Step 2](images/intro-client-id.png) - -3. Create an OAuth2 authorization URL -`https://discordapp.com/oauth2/authorize?client_id=&scope=bot` -4. Open the authorization URL in your browser. -5. Select a server. -6. Click on authorize. - - >[!NOTE] - Only servers where you have the `MANAGE_SERVER` permission will be - present in this list. - - ![Step 6](images/intro-add-bot.png) - - -## Connecting to Discord - -If you have not already created a project and installed Discord.Net, -do that now. (see the [Installing](installing.md) section) - -### Async - -Discord.Net uses .NET's [Task-based Asynchronous Pattern (TAP)] -extensively - nearly every operation is asynchronous. - -It is highly recommended that these operations are awaited in a -properly established async context whenever possible. Establishing an -async context can be problematic, but not hard. - -To do so, we will be creating an async main in your console -application, and rewriting the static main method to invoke the new -async main. - -[!code-csharp[Async Context](samples/intro/async-context.cs)] - -As a result of this, your program will now start and immediately -jump into an async context. This will allow us to create a connection -to Discord later on without needing to worry about setting up the -correct async implementation. - ->[!TIP] -If your application throws any exceptions within an async context, -they will be thrown all the way back up to the first non-async method; -since our first non-async method is the program's `Main` method, this -means that **all** unhandled exceptions will be thrown up there, which -will crash your application. Discord.Net will prevent exceptions in -event handlers from crashing your program, but any exceptions in your -async main **will** cause the application to crash. - -[Task-based Asynchronous Pattern (TAP)]: https://docs.microsoft.com/en-us/dotnet/articles/csharp/async - -### Creating a logging method - -Before we create and configure a Discord client, we will add a method -to handle Discord.Net's log events. - -To allow agnostic support of as many log providers as possible, we -log information through a `Log` event with a proprietary `LogMessage` -parameter. See the [API Documentation] for this event. - -If you are using your own logging framework, this is where you would -invoke it. For the sake of simplicity, we will only be logging to -the Console. - -[!code-csharp[Async Context](samples/intro/logging.cs)] - -[API Documentation]: xref:Discord.Rest.BaseDiscordClient#Discord_Rest_BaseDiscordClient_Log - -### Creating a Discord Client - -Finally, we can create a connection to Discord. Since we are writing -a bot, we will be using a [DiscordSocketClient] along with socket -entities. See the [terminology](terminology.md) if you're unsure of -the differences. - -To do so, create an instance of [DiscordSocketClient] in your async -main, passing in a configuration object only if necessary. For most -users, the default will work fine. - -Before connecting, we should hook the client's `Log` event to the -log handler that was just created. Events in Discord.Net work -similarly to other events in C#, so hook this event the way that -you typically would. - -Next, you will need to "login to Discord" with the `LoginAsync` -method. - -You may create a variable to hold your bot's token (this can be found -on your bot's application page on the [Discord Applications Portal]). - -![Token](images/intro-token.png) - ->[!IMPORTANT] -Your bot's token can be used to gain total access to your bot, so -**do __NOT__ share this token with anyone else!** It may behoove you -to store this token in an external file if you plan on distributing -the source code for your bot. - -We may now invoke the client's `StartAsync` method, which will -start connection/reconnection logic. It is important to note that -**this method returns as soon as connection logic has been started!** - -Any methods that rely on the client's state should go in an event -handler. - -Finally, we will want to block the async main method from returning -until after the application is exited. To do this, we can await an -infinite delay or any other blocking method, such as reading from -the console. - -The following lines can now be added: - -[!code-csharp[Create client](samples/intro/client.cs)] - -At this point, feel free to start your program and see your bot come -online in Discord. - ->[!TIP] -Encountering a `PlatformNotSupportedException` when starting your bot? -This means that you are targeting a platform where .NET's default -WebSocket client is not supported. Refer to the [installation guide] -for how to fix this. - -[DiscordSocketClient]: xref:Discord.WebSocket.DiscordSocketClient -[installation guide]: installing.md#installing-on-net-standard-11 - -### Handling a 'ping' - ->[!WARNING] -Please note that this is *not* a proper way to create a command. -Use the `CommandService` provided by the library instead, as explained -in the [Command Guide] section. - -Now that we have learned how to open a connection to Discord, we can -begin handling messages that users are sending. - -To start out, our bot will listen for any message where the content -is equal to `!ping` and respond back with "Pong!". - -Since we want to listen for new messages, the event to hook into -is [MessageReceived]. - -In your program, add a method that matches the signature of the -`MessageReceived` event - it must be a method (`Func`) that returns -the type `Task` and takes a single parameter, a [SocketMessage]. Also, -since we will be sending data to Discord in this method, we will flag -it as `async`. - -In this method, we will add an `if` block to determine if the message -content fits the rules of our scenario - recall that it must be equal -to `!ping`. - -Inside the branch of this condition, we will want to send a message -back to the channel from which the message comes from - "Pong!". To -find the channel, look for the `Channel` property on the message -parameter. - -Next, we will want to send a message to this channel. Since the -channel object is of type [SocketMessageChannel], we can invoke the -`SendMessageAsync` instance method. For the message content, send back -a string containing "Pong!". - -You should have now added the following lines: - -[!code-csharp[Message](samples/intro/message.cs)] - -Now your first bot is complete. You may continue to add on to this -if you desire, but for any bots that will be carrying out multiple -commands, it is strongly recommended to use the command framework as -shown below. - -For your reference, you may view the [completed program]. - -[MessageReceived]: xref:Discord.WebSocket.DiscordSocketClient#Discord_WebSocket_DiscordSocketClient_MessageReceived -[SocketMessage]: xref:Discord.WebSocket.SocketMessage -[SocketMessageChannel]: xref:Discord.WebSocket.ISocketMessageChannel -[completed program]: samples/intro/complete.cs -[Command Guide]: ../commands/commands.md - -# Building a bot with commands - -This section will show you how to write a program that is ready for -[Commands](../commands/commands.md). Note that we will not be -explaining _how_ to write Commands or Services, it will only be -covering the general structure. - -For reference, view an [annotated example] of this structure. - -[annotated example]: samples/intro/structure.cs - -It is important to know that the recommended design pattern of bots -should be to separate the program (initialization and command handler), -the modules (handle commands), and the services (persistent storage, -pure functions, data manipulation). - -**todo:** diagram of bot structure \ No newline at end of file diff --git a/docs/guides/getting_started/samples/first-bot/async-context.cs b/docs/guides/getting_started/samples/first-bot/async-context.cs new file mode 100644 index 000000000..3c98c9e46 --- /dev/null +++ b/docs/guides/getting_started/samples/first-bot/async-context.cs @@ -0,0 +1,9 @@ +public class Program +{ + public static void Main(string[] args) + => new Program().MainAsync().GetAwaiter().GetResult(); + + public async Task MainAsync() + { + } +} \ No newline at end of file diff --git a/docs/guides/getting_started/samples/intro/client.cs b/docs/guides/getting_started/samples/first-bot/client.cs similarity index 71% rename from docs/guides/getting_started/samples/intro/client.cs rename to docs/guides/getting_started/samples/first-bot/client.cs index a73082052..b3bf39865 100644 --- a/docs/guides/getting_started/samples/intro/client.cs +++ b/docs/guides/getting_started/samples/first-bot/client.cs @@ -1,17 +1,17 @@ -// Program.cs -using Discord.WebSocket; -// ... private DiscordSocketClient _client; + public async Task MainAsync() { _client = new DiscordSocketClient(); _client.Log += Log; - string token = "abcdefg..."; // Remember to keep this private! + // Remember to keep this private or to read this + // from an external source! + string token = "abcdefg..."; await _client.LoginAsync(TokenType.Bot, token); await _client.StartAsync(); // Block this task until the program is closed. await Task.Delay(-1); -} +} \ No newline at end of file diff --git a/docs/guides/getting_started/samples/first-bot/complete.cs b/docs/guides/getting_started/samples/first-bot/complete.cs new file mode 100644 index 000000000..965b57a5e --- /dev/null +++ b/docs/guides/getting_started/samples/first-bot/complete.cs @@ -0,0 +1,38 @@ +public class Program +{ + private DiscordSocketClient _client; + + public static void Main(string[] args) + => new Program().MainAsync().GetAwaiter().GetResult(); + + public async Task MainAsync() + { + _client = new DiscordSocketClient(); + + _client.Log += Log; + _client.MessageReceived += MessageReceivedAsync; + + // Remember to keep this private or to read this + // from an external source! + string token = "abcdefg..."; + await _client.LoginAsync(TokenType.Bot, token); + await _client.StartAsync(); + + // Block this task until the program is closed. + await Task.Delay(-1); + } + + private async Task MessageReceivedAsync(SocketMessage message) + { + if (message.Content == "!ping") + { + await message.Channel.SendMessageAsync("Pong!"); + } + } + + private Task Log(LogMessage msg) + { + Console.WriteLine(msg.ToString()); + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/docs/guides/getting_started/samples/first-bot/logging.cs b/docs/guides/getting_started/samples/first-bot/logging.cs new file mode 100644 index 000000000..c6ffc406e --- /dev/null +++ b/docs/guides/getting_started/samples/first-bot/logging.cs @@ -0,0 +1,5 @@ +private Task Log(LogMessage msg) +{ + Console.WriteLine(msg.ToString()); + return Task.CompletedTask; +} \ No newline at end of file diff --git a/docs/guides/getting_started/samples/intro/message.cs b/docs/guides/getting_started/samples/first-bot/message.cs similarity index 92% rename from docs/guides/getting_started/samples/intro/message.cs rename to docs/guides/getting_started/samples/first-bot/message.cs index d6fd90778..f636d6f35 100644 --- a/docs/guides/getting_started/samples/intro/message.cs +++ b/docs/guides/getting_started/samples/first-bot/message.cs @@ -1,6 +1,6 @@ public async Task MainAsync() { - // client.Log ... + // ... _client.MessageReceived += MessageReceived; // ... } diff --git a/docs/guides/getting_started/samples/intro/structure.cs b/docs/guides/getting_started/samples/first-bot/structure.cs similarity index 100% rename from docs/guides/getting_started/samples/intro/structure.cs rename to docs/guides/getting_started/samples/first-bot/structure.cs diff --git a/docs/guides/getting_started/samples/intro/async-context.cs b/docs/guides/getting_started/samples/intro/async-context.cs deleted file mode 100644 index c01ddec55..000000000 --- a/docs/guides/getting_started/samples/intro/async-context.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace MyBot -{ - public class Program - { - public static void Main(string[] args) - => new Program().MainAsync().GetAwaiter().GetResult(); - - public async Task MainAsync() - { - } - } -} \ No newline at end of file diff --git a/docs/guides/getting_started/samples/intro/complete.cs b/docs/guides/getting_started/samples/intro/complete.cs deleted file mode 100644 index 23b59ce6f..000000000 --- a/docs/guides/getting_started/samples/intro/complete.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Discord; -using Discord.WebSocket; -using System; -using System.Threading.Tasks; - -namespace MyBot -{ - public class Program - { - private DiscordSocketClient _client; - - public static void Main(string[] args) - => new Program().MainAsync().GetAwaiter().GetResult(); - - public async Task MainAsync() - { - _client = new DiscordSocketClient(); - - _client.Log += Log; - _client.MessageReceived += MessageReceived; - - string token = "abcdefg..."; // Remember to keep this private! - await _client.LoginAsync(TokenType.Bot, token); - await _client.StartAsync(); - - // Block this task until the program is closed. - await Task.Delay(-1); - } - - private async Task MessageReceived(SocketMessage message) - { - if (message.Content == "!ping") - { - await message.Channel.SendMessageAsync("Pong!"); - } - } - - private Task Log(LogMessage msg) - { - Console.WriteLine(msg.ToString()); - return Task.CompletedTask; - } - } -} diff --git a/docs/guides/getting_started/samples/intro/logging.cs b/docs/guides/getting_started/samples/intro/logging.cs deleted file mode 100644 index 4fb85a063..000000000 --- a/docs/guides/getting_started/samples/intro/logging.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Discord; -using System; -using System.Threading.Tasks; - -namespace MyBot -{ - public class Program - { - public static void Main(string[] args) - => new Program().MainAsync().GetAwaiter().GetResult(); - - public async Task MainAsync() - { - } - - private Task Log(LogMessage msg) - { - Console.WriteLine(msg.ToString()); - return Task.CompletedTask; - } - } -} \ No newline at end of file diff --git a/docs/guides/getting_started/samples/project.csproj b/docs/guides/getting_started/samples/project.csproj deleted file mode 100644 index feb0b0c40..000000000 --- a/docs/guides/getting_started/samples/project.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - Exe - netcoreapp1.1 - true - - - - - - - diff --git a/docs/guides/getting_started/samples/project.xml b/docs/guides/getting_started/samples/project.xml new file mode 100644 index 000000000..983ac228b --- /dev/null +++ b/docs/guides/getting_started/samples/project.xml @@ -0,0 +1,16 @@ + + + + + Exe + netcoreapp2.0 + + + + + + + diff --git a/docs/guides/getting_started/terminology.md b/docs/guides/getting_started/terminology.md index 74f7a6259..a03dc8fbf 100644 --- a/docs/guides/getting_started/terminology.md +++ b/docs/guides/getting_started/terminology.md @@ -1,5 +1,5 @@ --- -uid: Terminology +uid: Guides.GettingStarted.Terminology title: Terminology --- @@ -7,34 +7,34 @@ title: Terminology ## Preface -Most terms for objects remain the same between 0.9 and 1.0. The major -difference is that the ``Server`` is now called ``Guild`` to stay in -line with Discord internally. +Most terms for objects remain the same between 0.9 and 1.0 and above. +The major difference is that the ``Server`` is now called ``Guild`` +to stay in line with Discord internally. ## Implementation Specific Entities -Discord.Net 1.0 is split into a core library and three different -implementations - `Discord.Net.Core`, `Discord.Net.Rest`, +Discord.Net is split into a core library and three different +implementations - `Discord.Net.Core`, `Discord.Net.Rest`, `Discord.Net.Rpc`, and `Discord.Net.WebSockets`. -As a bot developer, you will only need to use `Discord.Net.WebSockets`, +As a bot developer, you will only need to use `Discord.Net.WebSockets`, but you should be aware of the differences between them. -`Discord.Net.Core` provides a set of interfaces that models Discord's -API. These interfaces are consistent throughout all implementations of -Discord.Net, and if you are writing an implementation-agnostic library -or addon, you can rely on the core interfaces to ensure that your +`Discord.Net.Core` provides a set of interfaces that models Discord's +API. These interfaces are consistent throughout all implementations of +Discord.Net, and if you are writing an implementation-agnostic library +or addon, you can rely on the core interfaces to ensure that your addon will run on all platforms. -`Discord.Net.Rest` provides a set of concrete classes to be used -**strictly** with the REST portion of Discord's API. Entities in this +`Discord.Net.Rest` provides a set of concrete classes to be used +**strictly** with the REST portion of Discord's API. Entities in this implementation are prefixed with `Rest` (e.g. `RestChannel`). -`Discord.Net.Rpc` provides a set of concrete classes that are used -with Discord's RPC API. Entities in this implementation are prefixed +`Discord.Net.Rpc` provides a set of concrete classes that are used +with Discord's RPC API. Entities in this implementation are prefixed with `Rpc` (e.g. `RpcChannel`). -`Discord.Net.WebSocket` provides a set of concrete classes that are +`Discord.Net.WebSocket` provides a set of concrete classes that are used primarily with Discord's WebSocket API or entities that are kept -in cache. When developing bots, you will be using this implementation. +in cache. When developing bots, you will be using this implementation. All entities are prefixed with `Socket` (e.g. `SocketChannel`). \ No newline at end of file diff --git a/docs/guides/introduction/intro.md b/docs/guides/introduction/intro.md new file mode 100644 index 000000000..165a7949a --- /dev/null +++ b/docs/guides/introduction/intro.md @@ -0,0 +1,51 @@ +--- +uid: Guides.Introduction +title: Introduction to Discord.Net +--- + +# Introduction + +## Looking to get started? + +First of all, welcome! You may visit us on our Discord should you +have any questions. Before you delve into using the library, +however, you should have some decent understanding of the language +you are about to use. This library touches on +[Task-based Asynchronous Pattern] \(TAP), [polymorphism], [interface] +and many more advanced topics extensively. Please make sure that you +understand these topics to some extent before proceeding. + +Here are some examples: + +1. [Official quick start guide] +2. [Official template] + +> [!NOTE] +> Please note that you should *not* try to blindly copy paste +> the code. The examples are meant to be a template or a guide. +> It is not meant to be something that will work out of the box. + +[Official template]: https://github.com/foxbot/DiscordBotBase/tree/csharp/src/DiscordBot +[Official quick start guide]: https://github.com/RogueException/Discord.Net/blob/dev/docs/guides/getting_started/samples/first-bot/structure.cs +[Task-based Asynchronous Pattern]: https://docs.microsoft.com/en-us/dotnet/standard/asynchronous-programming-patterns/task-based-asynchronous-pattern-tap +[polymorphism]: https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/polymorphism +[interface]: https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/interfaces/ + +## New to .NET/C#? + +If you are new to the language, using this lib may prove to be +difficult, but don't worry! There are many resources online that can +help you get started in the wonderful world of .NET. Here are some +resources to get you started. + +- [C# Programming Guide (MSDN/Microsoft, Free)](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/) +- [C# Fundamentals For Absolute Beginners (Channel9/Microsoft, Free)](https://channel9.msdn.com/Series/C-Fundamentals-for-Absolute-Beginners) +- [C# Path (Pluralsight, Paid)](https://www.pluralsight.com/paths/csharp) + +## Still have questions? + +Please visit us at `#dotnet_discord-net` on the [Discord API] server. +Describe the problem in details to us, what you've done, and, +if any, the problematic code uploaded onto [Hastebin](https://hastebin.com). + +[Discord API]: https://discord.gg/jkrBmQR \ No newline at end of file diff --git a/docs/guides/migrating/migrating.md b/docs/guides/migrating/migrating.md deleted file mode 100644 index bc628a5f8..000000000 --- a/docs/guides/migrating/migrating.md +++ /dev/null @@ -1,61 +0,0 @@ -# Migrating from 0.9 - -**1.0.0 is the biggest breaking change the library has gone through, due to massive -changes in the design of the library.** - ->A medium to advanced understanding is recommended when working with this library. - -It is recommended to familiarize yourself with the entities in 1.0 before continuing. -Feel free to look through the library's source directly, look through IntelliSense, or -look through our hosted [API Documentation](xref:Discord). - -## Entities - -Most API models function _similarly_ to 0.9, however their names have been changed. -You should also keep in mind that we now separate different types of Channels and Users. - -Before proceeding, please read over @Terminology to understand the naming behind some objects. - -Below is a table that compares most common 0.9 entities to their 1.0 counterparts. - ->This should be used mostly for migration purposes. Please take some time to consider whether ->or not you are using the right "tool for the job" when working with 1.0 - -| 0.9 | 1.0 | Notice | -| --- | --- | ------ | -| Server | @Discord.WebSocket.SocketGuild | -| Channel | @Discord.WebSocket.SocketGuildChannel | Applies only to channels that are members of a Guild | -| Channel.IsPrivate | @Discord.WebSocket.SocketDMChannel -| ChannelType.Text | @Discord.WebSocket.SocketTextChannel | This applies only to Text Channels in Guilds -| ChannelType.Voice | @Discord.WebSocket.SocketVoiceChannel | This applies only to Voice Channels in Guilds -| User | @Discord.WebSocket.SocketGuildUser | This applies only to users belonging to a Guild* -| Profile | @Discord.WebSocket.SocketGuildUser -| Message | @Discord.WebSocket.SocketUserMessage - -\* To retrieve an @Discord.WebSocket.SocketGuildUser, you must retrieve the user from an @Discord.WebSocket.SocketGuild. - -## Event Registration - -Prior to 1.0, events were registered using the standard c# `Handler(EventArgs)` pattern. In 1.0, -events are delegates, but are still registered the same. - -For example, let's look at [DiscordSocketClient.MessageReceived](xref:Discord.WebSocket.DiscordSocketClient#Discord_WebSocket_DiscordSocketClient_MessageReceived) - -To hook an event into MessageReceived, we now use the following code: -[!code-csharp[Event Registration](samples/event.cs)] - -> **All Event Handlers in 1.0 MUST return Task!** - -If your event handler is marked as `async`, it will automatically return `Task`. However, -if you do not need to execute asynchronus code, do _not_ mark your handler as `async`, and instead, -stick a `return Task.CompletedTask` at the bottom. - -[!code-csharp[Sync Event Registration](samples/sync_event.cs)] - -**Event handlers no longer require a sender.** The only arguments your event handler needs to accept -are the parameters used by the event. It is recommended to look at the event in IntelliSense or on the -API docs before implementing it. - -## Async - -Nearly everything in 1.0 is an async Task. You should always await any tasks you invoke. diff --git a/docs/guides/migrating/samples/event.cs b/docs/guides/migrating/samples/event.cs deleted file mode 100644 index 8719942f2..000000000 --- a/docs/guides/migrating/samples/event.cs +++ /dev/null @@ -1,4 +0,0 @@ -_client.MessageReceived += async (msg) => -{ - await msg.Channel.SendMessageAsync(msg.Content); -} \ No newline at end of file diff --git a/docs/guides/migrating/samples/sync_event.cs b/docs/guides/migrating/samples/sync_event.cs deleted file mode 100644 index f4a55cdd3..000000000 --- a/docs/guides/migrating/samples/sync_event.cs +++ /dev/null @@ -1,5 +0,0 @@ -_client.Log += (msg) => -{ - Console.WriteLine(msg.ToString()); - return Task.CompletedTask; -} \ No newline at end of file diff --git a/docs/guides/toc.yml b/docs/guides/toc.yml index 2e3a61e19..639fe9d88 100644 --- a/docs/guides/toc.yml +++ b/docs/guides/toc.yml @@ -1,27 +1,36 @@ +- name: Introduction + topicUid: Guides.Introduction - name: Getting Started items: - name: Installation - href: getting_started/installing.md + topicUid: Guides.GettingStarted.Installation - name: Your First Bot - href: getting_started/intro.md + topicUid: Guides.GettingStarted.FirstBot - name: Terminology - href: getting_started/terminology.md + topicUid: Guides.GettingStarted.Terminology - name: Basic Concepts items: - name: Logging Data - href: concepts/logging.md + topicUid: Guides.Concepts.Logging - name: Working with Events - href: concepts/events.md + topicUid: Guides.Concepts.Events - name: Managing Connections - href: concepts/connections.md + topicUid: Guides.Concepts.ManageConnections - name: Entities - href: concepts/entities.md + topicUid: Guides.Concepts.Entities + - name: Deployment + topicUid: Guides.Concepts.Deployment - name: The Command Service items: - - name: Command Guide - href: commands/commands.md + - name: Introduction + topicUid: Guides.Commands.Intro + - name: TypeReaders + topicUid: Guides.Commands.TypeReaders + - name: Preconditions + topicUid: Guides.Commands.Preconditions + - name: Dependency Injection + topicUid: Guides.Commands.DI + - name: Post-execution Handling + topicUid: Guides.Commands.PostExecution - name: Voice - items: - - name: Voice Guide - href: voice/sending-voice.md -- name: Migrating from 0.9 \ No newline at end of file + topicUid: Guides.Voice.SendingVoice diff --git a/docs/guides/voice/sending-voice.md b/docs/guides/voice/sending-voice.md index 024a98b95..c30805836 100644 --- a/docs/guides/voice/sending-voice.md +++ b/docs/guides/voice/sending-voice.md @@ -1,4 +1,5 @@ --- +uid: Guides.Voice.SendingVoice title: Sending Voice --- @@ -44,7 +45,7 @@ guild. To switch channels within a guild, invoke [ConnectAsync] on another voice channel in the guild. [IAudioClient]: xref:Discord.Audio.IAudioClient -[ConnectAsync]: xref:Discord.IAudioChannel#Discord_IAudioChannel_ConnectAsync_Action_IAudioClient__ +[ConnectAsync]: xref:Discord.IAudioChannel.ConnectAsync* ## Transmitting Audio @@ -98,4 +99,4 @@ you will want to wait for audio to stop playing before continuing on to the next song. You can await `AudioOutStream.FlushAsync` to wait for the audio client's internal buffer to clear out. -[!code-csharp[Sending Audio](samples/audio_ffmpeg.cs)] \ No newline at end of file +[!code-csharp[Sending Audio](samples/audio_ffmpeg.cs)] diff --git a/docs/index.md b/docs/index.md index ef9ecdfdd..8622bc002 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,13 +1,28 @@ +--- +uid: Root.Landing +title: Home +--- # Discord.Net Documentation -Discord.Net is an asynchronous, multiplatform .NET Library used to interface with the [Discord API](https://discordapp.com/). +## What is Discord.Net? -If this is your first time using Discord.Net, you should refer to the [Intro](guides/getting_started/intro.md) for tutorials. -More experienced users might refer to the [API Documentation](api/index.md) for a breakdown of the individuals objects in the library. +Discord.Net is an asynchronous, multi-platform .NET Library used to +interface with the [Discord API](https://discordapp.com/). -For additional resources: - - [Discord API Guild](https://discord.gg/discord-api) - Look for `#dotnet_discord-net` - - [GitHub](https://github.com/RogueException/Discord.Net/tree/dev) - - [NuGet](https://www.nuget.org/packages/Discord.Net/) - - [MyGet Feed](https://www.myget.org/feed/Packages/discord-net) - Addons and nightly builds \ No newline at end of file +## Where to begin? + +If this is your first time using Discord.Net, you should refer to the +[Intro](xref:Guides.Introduction) for tutorials. + +More experienced users might want to refer to the +[API Documentation](xref:API.Docs) for a breakdown of the individual +objects in the library. + +## Additional Resources + +- [Discord API Guild](https://discord.gg/discord-api) - Look for `#dotnet_discord-net` +- [GitHub](https://github.com/RogueException/Discord.Net/) +- [NuGet](https://www.nuget.org/packages/Discord.Net/) +- [MyGet Feed](https://www.myget.org/feed/Packages/discord-net) - Add-ons and nightly builds +- [AppVeyor CI](https://ci.appveyor.com/project/RogueException/discord-net) - Nightly builds via Continuous Integration \ No newline at end of file diff --git a/docs/toc.yml b/docs/toc.yml index c08e708bf..337294b1a 100644 --- a/docs/toc.yml +++ b/docs/toc.yml @@ -1,6 +1,9 @@ - - name: Guides href: guides/ + topicUid: Guides.Introduction +- name: FAQ + href: faq/ + topicUid: FAQ.Basics.GetStarted - name: API Documentation href: api/ - homepage: api/index.md + topicUid: API.Docs \ No newline at end of file diff --git a/src/Discord.Net.Commands/Attributes/AliasAttribute.cs b/src/Discord.Net.Commands/Attributes/AliasAttribute.cs index 6cd0abbb7..acdb600ce 100644 --- a/src/Discord.Net.Commands/Attributes/AliasAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/AliasAttribute.cs @@ -2,11 +2,11 @@ using System; namespace Discord.Commands { - /// Provides aliases for a command. + /// Marks the aliases for a command. [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] public class AliasAttribute : Attribute { - /// The aliases which have been defined for the command. + /// Gets the aliases which have been defined for the command. public string[] Aliases { get; } /// Creates a new with the given aliases. diff --git a/src/Discord.Net.Commands/Attributes/CommandAttribute.cs b/src/Discord.Net.Commands/Attributes/CommandAttribute.cs index 5f8e9ceaf..cce82101b 100644 --- a/src/Discord.Net.Commands/Attributes/CommandAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/CommandAttribute.cs @@ -2,16 +2,29 @@ using System; namespace Discord.Commands { + /// Marks the execution information for a command. [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] public class CommandAttribute : Attribute { + /// + /// Gets the text that has been set to be recognized as a command. + /// public string Text { get; } + /// + /// Specifies the of the command. This affects how the command is executed. + /// public RunMode RunMode { get; set; } = RunMode.Default; + /// public CommandAttribute() { Text = null; } + + /// + /// Initializes a new attribute with the specified name. + /// + /// The name of the command. public CommandAttribute(string text) { Text = text; diff --git a/src/Discord.Net.Commands/Attributes/DontAutoLoadAttribute.cs b/src/Discord.Net.Commands/Attributes/DontAutoLoadAttribute.cs index cc23a6d15..451b87f5e 100644 --- a/src/Discord.Net.Commands/Attributes/DontAutoLoadAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/DontAutoLoadAttribute.cs @@ -2,6 +2,7 @@ using System; namespace Discord.Commands { + /// Prevents the marked module from being loaded automatically. [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] public class DontAutoLoadAttribute : Attribute { diff --git a/src/Discord.Net.Commands/Attributes/DontInjectAttribute.cs b/src/Discord.Net.Commands/Attributes/DontInjectAttribute.cs index c982d93a1..19d946afd 100644 --- a/src/Discord.Net.Commands/Attributes/DontInjectAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/DontInjectAttribute.cs @@ -1,9 +1,12 @@ using System; -namespace Discord.Commands { - - [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] - public class DontInjectAttribute : Attribute { - } - +namespace Discord.Commands +{ + /// + /// Prevents the marked property from being injected into a module. + /// + [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] + public class DontInjectAttribute : Attribute + { + } } diff --git a/src/Discord.Net.Commands/Attributes/GroupAttribute.cs b/src/Discord.Net.Commands/Attributes/GroupAttribute.cs index b1760d149..e1e38cff7 100644 --- a/src/Discord.Net.Commands/Attributes/GroupAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/GroupAttribute.cs @@ -2,15 +2,26 @@ using System; namespace Discord.Commands { + /// + /// Marks the module as a command group. + /// [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] public class GroupAttribute : Attribute { + /// + /// Gets the prefix set for the module. + /// public string Prefix { get; } + /// public GroupAttribute() { Prefix = null; } + /// + /// Initializes a new with the provided prefix. + /// + /// The prefix of the module group. public GroupAttribute(string prefix) { Prefix = prefix; diff --git a/src/Discord.Net.Commands/Attributes/NameAttribute.cs b/src/Discord.Net.Commands/Attributes/NameAttribute.cs index 4a4b2bfed..a6e1f2e5a 100644 --- a/src/Discord.Net.Commands/Attributes/NameAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/NameAttribute.cs @@ -3,11 +3,21 @@ using System; namespace Discord.Commands { // Override public name of command/module + /// + /// Marks the public name of a command, module, or parameter. + /// [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)] public class NameAttribute : Attribute { + /// + /// Gets the name of the command. + /// public string Text { get; } + /// + /// Marks the public name of a command, module, or parameter with the provided name. + /// + /// The public name of the object. public NameAttribute(string text) { Text = text; diff --git a/src/Discord.Net.Commands/Attributes/OverrideTypeReaderAttribute.cs b/src/Discord.Net.Commands/Attributes/OverrideTypeReaderAttribute.cs index 44ab6d214..a70a70f31 100644 --- a/src/Discord.Net.Commands/Attributes/OverrideTypeReaderAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/OverrideTypeReaderAttribute.cs @@ -4,17 +4,25 @@ using System.Reflection; namespace Discord.Commands { + /// + /// Marks the to be read by the specified . + /// [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)] public class OverrideTypeReaderAttribute : Attribute { - private static readonly TypeInfo _typeReaderTypeInfo = typeof(TypeReader).GetTypeInfo(); + private static readonly TypeInfo TypeReaderTypeInfo = typeof(TypeReader).GetTypeInfo(); + /// + /// Gets the specified of the parameter. + /// public Type TypeReader { get; } + /// + /// The to be used with the parameter. public OverrideTypeReaderAttribute(Type overridenTypeReader) { - if (!_typeReaderTypeInfo.IsAssignableFrom(overridenTypeReader.GetTypeInfo())) - throw new ArgumentException($"{nameof(overridenTypeReader)} must inherit from {nameof(TypeReader)}"); + if (!TypeReaderTypeInfo.IsAssignableFrom(overridenTypeReader.GetTypeInfo())) + throw new ArgumentException($"{nameof(overridenTypeReader)} must inherit from {nameof(TypeReader)}."); TypeReader = overridenTypeReader; } diff --git a/src/Discord.Net.Commands/Attributes/ParameterPreconditionAttribute.cs b/src/Discord.Net.Commands/Attributes/ParameterPreconditionAttribute.cs index 3c5e8cf92..efdb2c5b2 100644 --- a/src/Discord.Net.Commands/Attributes/ParameterPreconditionAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/ParameterPreconditionAttribute.cs @@ -3,9 +3,19 @@ using System.Threading.Tasks; namespace Discord.Commands { + /// + /// Requires the parameter to pass the specified precondition before execution can begin. + /// [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = true, Inherited = true)] public abstract class ParameterPreconditionAttribute : Attribute { + /// + /// Checks whether the condition is met before execution of the command. + /// + /// The context of the command. + /// The parameter of the command being checked against. + /// The raw value of the type. + /// The service collection used for dependency injection. public abstract Task CheckPermissionsAsync(ICommandContext context, ParameterInfo parameter, object value, IServiceProvider services); } } diff --git a/src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs b/src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs index 367adebf0..50a63c582 100644 --- a/src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs @@ -1,16 +1,20 @@ -using System; +using System; using System.Threading.Tasks; namespace Discord.Commands { + /// Requires the module or class to pass the specified precondition before execution can begin. [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true, Inherited = true)] public abstract class PreconditionAttribute : Attribute { /// - /// 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 = - /// or not at all will require *all* preconditions to pass, just like normal (A && B). + /// Specify a group that this precondition belongs to. /// + /// + /// of the same group require only one of the preconditions to pass in order to + /// be successful (A || B). Specifying = or not at all will + /// require *all* preconditions to pass, just like normal (A && B). + /// public string Group { get; set; } = null; public abstract Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services); diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs index 104252799..adea63edf 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs @@ -1,46 +1,52 @@ -using System; +using System; using System.Threading.Tasks; namespace Discord.Commands { /// - /// 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. /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] public class RequireBotPermissionAttribute : PreconditionAttribute { + /// + /// Gets the specified of the precondition. + /// public GuildPermission? GuildPermission { get; } + /// + /// Gets the specified of the precondition. + /// public ChannelPermission? ChannelPermission { get; } /// - /// Require that the bot account has a specified GuildPermission + /// Requires the bot account to have a specific . /// - /// This precondition will always fail if the command is being invoked in a private channel. - /// The GuildPermission that the bot must have. Multiple permissions can be specified by ORing the permissions together. + /// + /// This precondition will always fail if the command is being invoked in a . + /// + /// + /// The that the bot must have. Multiple permissions can be specified + /// by ORing the permissions together. + /// public RequireBotPermissionAttribute(GuildPermission permission) { GuildPermission = permission; ChannelPermission = null; } /// - /// Require that the bot account has a specified ChannelPermission. + /// Requires that the bot account to have a specific . /// - /// The ChannelPermission that the bot must have. Multiple permissions can be specified by ORing the permissions together. - /// - /// - /// [Command("permission")] - /// [RequireBotPermission(ChannelPermission.ManageMessages)] - /// public async Task Purge() - /// { - /// } - /// - /// + /// + /// The that the bot must have. Multiple permissions can be + /// specified by ORing the permissions together. + /// public RequireBotPermissionAttribute(ChannelPermission permission) { ChannelPermission = permission; GuildPermission = null; } + /// public override async Task 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(); diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs index 90af035e4..8797f7daf 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs @@ -1,28 +1,40 @@ using System; using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; namespace Discord.Commands { + /// + /// Defines the type of command context (i.e. where the command is being executed). + /// [Flags] public enum ContextType { + /// + /// Specifies the command to be executed within a guild. + /// Guild = 0x01, + /// + /// Specifies the command to be executed within a DM. + /// DM = 0x02, + /// + /// Specifies the command to be executed within a group. + /// Group = 0x04 } /// - /// 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). /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] public class RequireContextAttribute : PreconditionAttribute { - public ContextType Contexts { get; } - /// - /// Require that the command be invoked in a specified context. + /// Gets the context required to execute the command. /// + public ContextType Contexts { get; } + + /// Requires the command to be invoked in the specified context. /// The type of context the command can be invoked in. Multiple contexts can be specified by ORing the contexts together. /// /// diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireNsfwAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireNsfwAttribute.cs index 273c764bd..d97db8290 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireNsfwAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireNsfwAttribute.cs @@ -4,11 +4,12 @@ using System.Threading.Tasks; namespace Discord.Commands { /// - /// Require that the command is invoked in a channel marked NSFW + /// Requires the command to be invoked in a channel marked NSFW. /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] public class RequireNsfwAttribute : PreconditionAttribute { + /// public override Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) { if (context.Channel is ITextChannel text && text.IsNsfw) diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs index 93e3cbe18..3ea35a9fd 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs @@ -4,20 +4,21 @@ using System.Threading.Tasks; namespace Discord.Commands { /// - /// Require that the command is invoked by the owner of the bot. + /// Requires the command to be invoked by the owner of the bot. /// /// This precondition will only work if the bot is a bot account. [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] public class RequireOwnerAttribute : PreconditionAttribute { + /// public override async Task 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)}."); diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs index 14121f35b..072f10e0f 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs @@ -1,47 +1,52 @@ -using System; +using System; using System.Threading.Tasks; namespace Discord.Commands { /// - /// This attribute requires that the user invoking the command has a specified permission. + /// Requires the user invoking the command to have a specified permission. /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] public class RequireUserPermissionAttribute : PreconditionAttribute { + /// + /// Gets the specified of the precondition. + /// public GuildPermission? GuildPermission { get; } + /// + /// Gets the specified of the precondition. + /// public ChannelPermission? ChannelPermission { get; } /// - /// Require that the user invoking the command has a specified GuildPermission + /// Requires that the user invoking the command to have a specific . /// - /// This precondition will always fail if the command is being invoked in a private channel. - /// The GuildPermission that the user must have. Multiple permissions can be specified by ORing the permissions together. + /// + /// This precondition will always fail if the command is being invoked in a . + /// + /// + /// The that the user must have. Multiple permissions can be + /// specified by ORing the permissions together. + /// public RequireUserPermissionAttribute(GuildPermission permission) { GuildPermission = permission; ChannelPermission = null; } /// - /// Require that the user invoking the command has a specified ChannelPermission. + /// Requires that the user invoking the command to have a specific . /// - /// The ChannelPermission that the user must have. Multiple permissions can be specified by ORing the permissions together. - /// - /// - /// [Command("permission")] - /// [RequireUserPermission(ChannelPermission.ReadMessageHistory | ChannelPermission.ReadMessages)] - /// public async Task HasPermission() - /// { - /// await ReplyAsync("You can read messages and the message history!"); - /// } - /// - /// + /// + /// The that the user must have. Multiple permissions can be + /// specified by ORing the permissions together. + /// public RequireUserPermissionAttribute(ChannelPermission permission) { ChannelPermission = permission; GuildPermission = null; } - + + /// public override Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) { var guildUser = context.User as IGuildUser; diff --git a/src/Discord.Net.Commands/Attributes/PriorityAttribute.cs b/src/Discord.Net.Commands/Attributes/PriorityAttribute.cs index 353e96e41..75ffd2585 100644 --- a/src/Discord.Net.Commands/Attributes/PriorityAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/PriorityAttribute.cs @@ -2,14 +2,20 @@ using System; namespace Discord.Commands { - /// Sets priority of commands + /// + /// Sets priority of commands. + /// [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] public class PriorityAttribute : Attribute { - /// The priority which has been set for the command + /// + /// Gets the priority which has been set for the command. + /// public int Priority { get; } - /// Creates a new with the given priority. + /// + /// Initializes a new attribute with the given priority. + /// public PriorityAttribute(int priority) { Priority = priority; diff --git a/src/Discord.Net.Commands/Attributes/RemainderAttribute.cs b/src/Discord.Net.Commands/Attributes/RemainderAttribute.cs index 56938f167..33e07f0d9 100644 --- a/src/Discord.Net.Commands/Attributes/RemainderAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/RemainderAttribute.cs @@ -2,6 +2,9 @@ using System; namespace Discord.Commands { + /// + /// Marks the input to not be parsed by the parser. + /// [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)] public class RemainderAttribute : Attribute { diff --git a/src/Discord.Net.Commands/Builders/CommandBuilder.cs b/src/Discord.Net.Commands/Builders/CommandBuilder.cs index b6d002c70..70045453f 100644 --- a/src/Discord.Net.Commands/Builders/CommandBuilder.cs +++ b/src/Discord.Net.Commands/Builders/CommandBuilder.cs @@ -1,8 +1,7 @@ -using System; +using System; using System.Linq; using System.Threading.Tasks; using System.Collections.Generic; -using Microsoft.Extensions.DependencyInjection; namespace Discord.Commands.Builders { @@ -140,4 +139,4 @@ namespace Discord.Commands.Builders return new CommandInfo(this, info, service); } } -} \ No newline at end of file +} diff --git a/src/Discord.Net.Commands/Builders/ModuleBuilder.cs b/src/Discord.Net.Commands/Builders/ModuleBuilder.cs index 1809c2c63..0ada5a9c2 100644 --- a/src/Discord.Net.Commands/Builders/ModuleBuilder.cs +++ b/src/Discord.Net.Commands/Builders/ModuleBuilder.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Reflection; using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; namespace Discord.Commands.Builders { diff --git a/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs index e9ce9eb86..cbeb6bffd 100644 --- a/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs +++ b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs @@ -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); } } @@ -291,7 +291,7 @@ namespace Discord.Commands //We dont have a cached type reader, create one reader = ReflectionUtils.CreateObject(typeReaderType.GetTypeInfo(), service, services); - service.AddTypeReader(paramType, reader); + service.AddTypeReader(paramType, reader, false); return reader; } diff --git a/src/Discord.Net.Commands/CommandContext.cs b/src/Discord.Net.Commands/CommandContext.cs index 05bde56b1..9e0766a68 100644 --- a/src/Discord.Net.Commands/CommandContext.cs +++ b/src/Discord.Net.Commands/CommandContext.cs @@ -1,13 +1,20 @@ -namespace Discord.Commands +namespace Discord.Commands { + /// The context of a command which may contain the client, user, guild, channel, and message. public class CommandContext : ICommandContext { + /// public IDiscordClient Client { get; } + /// public IGuild Guild { get; } + /// public IMessageChannel Channel { get; } + /// public IUser User { get; } + /// public IUserMessage Message { get; } + /// Indicates whether the channel that the command is executed in is a private channel. public bool IsPrivate => Channel is IPrivateChannel; public CommandContext(IDiscordClient client, IUserMessage msg) diff --git a/src/Discord.Net.Commands/CommandError.cs b/src/Discord.Net.Commands/CommandError.cs index abfc14e1d..40d678137 100644 --- a/src/Discord.Net.Commands/CommandError.cs +++ b/src/Discord.Net.Commands/CommandError.cs @@ -1,26 +1,51 @@ -namespace Discord.Commands +namespace Discord.Commands { + /// Defines the type of error a command can throw. public enum CommandError { //Search + /// + /// Thrown when the command is unknown. + /// UnknownCommand = 1, //Parse + /// + /// Thrown when the command fails to be parsed. + /// ParseFailed, + /// + /// Thrown when the input text has too few or too many arguments. + /// BadArgCount, //Parse (Type Reader) //CastFailed, + /// + /// Thrown when the object cannot be found by the . + /// ObjectNotFound, + /// + /// Thrown when more than one objects is matched by . + /// MultipleMatches, //Preconditions + /// + /// Thrown when the command fails to meet a 's conditions. + /// UnmetPrecondition, //Execute + /// + /// Thrown when an exception occurs mid-command execution. + /// Exception, //Runtime + /// + /// Thrown when the command is not successfully executed on runtime. + /// Unsuccessful } } diff --git a/src/Discord.Net.Commands/CommandException.cs b/src/Discord.Net.Commands/CommandException.cs index d5300841a..f8fbda290 100644 --- a/src/Discord.Net.Commands/CommandException.cs +++ b/src/Discord.Net.Commands/CommandException.cs @@ -2,11 +2,24 @@ using System; namespace Discord.Commands { + /// + /// Describes an exception that occurred during a command execution. + /// public class CommandException : Exception { + /// Gets the command that caused the exception. public CommandInfo Command { get; } + /// Gets the command context of the exception. public ICommandContext Context { get; } + /// + /// Initializes a new instance of the class using a + /// information, a context, and the exception that + /// interrupted the execution. + /// + /// The command information. + /// The context of the command. + /// The exception that interrupted the command execution. public CommandException(CommandInfo command, ICommandContext context, Exception ex) : base($"Error occurred executing {command.GetLogText(context)}.", ex) { diff --git a/src/Discord.Net.Commands/CommandMatch.cs b/src/Discord.Net.Commands/CommandMatch.cs index d922a2229..c15a33228 100644 --- a/src/Discord.Net.Commands/CommandMatch.cs +++ b/src/Discord.Net.Commands/CommandMatch.cs @@ -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 { + /// The command that matches the search result. public CommandInfo Command { get; } + /// The alias of the command. public string Alias { get; } public CommandMatch(CommandInfo command, string alias) diff --git a/src/Discord.Net.Commands/CommandParser.cs b/src/Discord.Net.Commands/CommandParser.cs index d65d99349..573b04d54 100644 --- a/src/Discord.Net.Commands/CommandParser.cs +++ b/src/Discord.Net.Commands/CommandParser.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Immutable; using System.Text; using System.Threading.Tasks; @@ -148,7 +148,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++) diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index de990ab47..b4511a90c 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -6,7 +6,6 @@ using System.Linq; using System.Reflection; using System.Threading; using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; using Discord.Commands.Builders; using Discord.Logging; @@ -17,6 +16,9 @@ namespace Discord.Commands public event Func Log { add { _logEvent.Add(value); } remove { _logEvent.Remove(value); } } internal readonly AsyncEvent> _logEvent = new AsyncEvent>(); + /// + /// Fired when a command is successfully executed without any runtime error. + /// public event Func CommandExecuted { add { _commandExecutedEvent.Add(value); } remove { _commandExecutedEvent.Remove(value); } } internal readonly AsyncEvent> _commandExecutedEvent = new AsyncEvent>(); @@ -34,8 +36,19 @@ namespace Discord.Commands internal readonly Logger _cmdLogger; internal readonly LogManager _logManager; + /// + /// Represents all modules loaded within . + /// public IEnumerable Modules => _moduleDefs.Select(x => x); + + /// + /// Represents all commands loaded within . + /// public IEnumerable Commands => _moduleDefs.SelectMany(x => x.Commands); + + /// + /// Represents all loaded within . + /// public ILookup TypeReaders => _typeReaders.SelectMany(x => x.Value.Select(y => new { y.Key, y.Value })).ToLookup(x => x.Key, x => x.Value); public CommandService() : this(new CommandServiceConfig()) { } @@ -95,8 +108,31 @@ namespace Discord.Commands _moduleLock.Release(); } } - public Task AddModuleAsync(IServiceProvider services = null) => AddModuleAsync(typeof(T), services); - public async Task AddModuleAsync(Type type, IServiceProvider services = null) + + /// + /// Add a command module from a . + /// + /// The type of module. + /// + /// The for your dependency injection solution, if using one - otherwise, pass + /// . + /// + /// + /// A built module. + /// + public Task AddModuleAsync(IServiceProvider services) => AddModuleAsync(typeof(T), services); + /// + /// Adds a command module from a . + /// + /// The type of module. + /// + /// The for your dependency injection solution, if using one - otherwise, pass + /// . + /// + /// + /// A built module. + /// + public async Task AddModuleAsync(Type type, IServiceProvider services) { services = services ?? EmptyServiceProvider.Instance; @@ -106,7 +142,7 @@ namespace Discord.Commands var typeInfo = type.GetTypeInfo(); if (_typedModuleDefs.ContainsKey(type)) - throw new ArgumentException($"This module has already been added."); + throw new ArgumentException("This module has already been added."); var module = (await ModuleClassBuilder.BuildAsync(this, services, typeInfo).ConfigureAwait(false)).FirstOrDefault(); @@ -122,7 +158,18 @@ namespace Discord.Commands _moduleLock.Release(); } } - public async Task> AddModulesAsync(Assembly assembly, IServiceProvider services = null) + /// + /// Add command modules from an . + /// + /// The containing command modules. + /// + /// An for your dependency injection solution, if using one - otherwise, pass + /// . + /// + /// + /// A collection of built modules. + /// + public async Task> AddModulesAsync(Assembly assembly, IServiceProvider services) { services = services ?? EmptyServiceProvider.Instance; @@ -157,7 +204,13 @@ namespace Discord.Commands return module; } - + /// + /// Removes the command module. + /// + /// The to be removed from the service. + /// + /// Returns whether the is successfully removed. + /// public async Task RemoveModuleAsync(ModuleInfo module) { await _moduleLock.WaitAsync().ConfigureAwait(false); @@ -202,28 +255,78 @@ namespace Discord.Commands return true; } - //Type Readers + //Type Readers /// - /// Adds a custom to this for the supplied object type. - /// If is a , a will also be added. + /// Adds a custom to this for the supplied object type. + /// If is a , a nullable will also be + /// added. + /// If a default exists for , a warning will be logged and the + /// default will be replaced. /// - /// The object type to be read by the . - /// An instance of the to be added. + /// The object type to be read by the . + /// An instance of the to be added. public void AddTypeReader(TypeReader reader) => AddTypeReader(typeof(T), reader); /// - /// Adds a custom to this for the supplied object type. - /// If is a , a for the value type will also be added. + /// Adds a custom to this for the supplied object type. + /// If is a , a nullable for the value + /// type will also be added. + /// If a default exists for , a warning will be logged and the + /// default will be replaced. /// - /// A instance for the type to be read. - /// An instance of the to be added. + /// A instance for the type to be read. + /// An instance of the to be added. public void AddTypeReader(Type type, TypeReader reader) { - var readers = _typeReaders.GetOrAdd(type, x => new ConcurrentDictionary()); - readers[reader.GetType()] = reader; + if (_defaultTypeReaders.ContainsKey(type)) + _ = _cmdLogger.WarningAsync($"The default TypeReader for {type.FullName} was replaced by {reader.GetType().FullName}." + + "To suppress this message, use AddTypeReader(reader, true)."); + AddTypeReader(type, reader, true); + } + /// + /// Adds a custom to this for the supplied object type. + /// If is a , a nullable will also be + /// added. + /// + /// The object type to be read by the . + /// An instance of the to be added. + /// + /// Defines whether the should replace the default one for + /// if it exists. + /// + public void AddTypeReader(TypeReader reader, bool replaceDefault) + => AddTypeReader(typeof(T), reader, replaceDefault); + /// + /// Adds a custom to this for the supplied object type. + /// If is a , a nullable for the value + /// type will also be added. + /// + /// A instance for the type to be read. + /// An instance of the to be added. + /// + /// Defines whether the should replace the default one for + /// if it exists. + /// + public void AddTypeReader(Type type, TypeReader reader, bool replaceDefault) + { + if (replaceDefault && _defaultTypeReaders.ContainsKey(type)) + { + _defaultTypeReaders.AddOrUpdate(type, reader, (k, v) => reader); + if (type.GetTypeInfo().IsValueType) + { + var nullableType = typeof(Nullable<>).MakeGenericType(type); + var nullableReader = NullableTypeReader.Create(type, reader); + _defaultTypeReaders.AddOrUpdate(nullableType, nullableReader, (k, v) => nullableReader); + } + } + else + { + var readers = _typeReaders.GetOrAdd(type, x => new ConcurrentDictionary()); + readers[reader.GetType()] = reader; - if (type.GetTypeInfo().IsValueType) - AddNullableTypeReader(type, reader); + if (type.GetTypeInfo().IsValueType) + AddNullableTypeReader(type, reader); + } } internal void AddNullableTypeReader(Type valueType, TypeReader valueTypeReader) { @@ -265,8 +368,20 @@ namespace Discord.Commands } //Execution + /// + /// Searches for the command. + /// + /// The context of the command. + /// The position of which the command starts at. + /// The result containing the matching commands. public SearchResult Search(ICommandContext context, int argPos) => Search(context, context.Message.Content.Substring(argPos)); + /// + /// Searches for the command. + /// + /// The context of the command. + /// The command string. + /// The result containing the matching commands. public SearchResult Search(ICommandContext context, string input) { string searchInput = _caseSensitive ? input : input.ToLowerInvariant(); @@ -278,9 +393,25 @@ namespace Discord.Commands return SearchResult.FromError(CommandError.UnknownCommand, "Unknown command."); } - public Task ExecuteAsync(ICommandContext context, int argPos, IServiceProvider services = null, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) + /// + /// Executes the command. + /// + /// The context of the command. + /// The position of which the command starts at. + /// The service to be used in the command's dependency injection. + /// The handling mode when multiple command matches are found. + /// The result of the command execution. + public Task ExecuteAsync(ICommandContext context, int argPos, IServiceProvider services, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) => ExecuteAsync(context, context.Message.Content.Substring(argPos), services, multiMatchHandling); - public async Task ExecuteAsync(ICommandContext context, string input, IServiceProvider services = null, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) + /// + /// Executes the command. + /// + /// The context of the command. + /// The command string. + /// The service to be used in the command's dependency injection. + /// The handling mode when multiple command matches are found. + /// The result of the command execution. + public async Task ExecuteAsync(ICommandContext context, string input, IServiceProvider services, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) { services = services ?? EmptyServiceProvider.Instance; diff --git a/src/Discord.Net.Commands/Discord.Net.Commands.csproj b/src/Discord.Net.Commands/Discord.Net.Commands.csproj index eaac79a55..9694dd5fc 100644 --- a/src/Discord.Net.Commands/Discord.Net.Commands.csproj +++ b/src/Discord.Net.Commands/Discord.Net.Commands.csproj @@ -4,7 +4,7 @@ Discord.Net.Commands Discord.Commands A Discord.Net extension adding support for bot commands. - netstandard1.1 + netstandard1.1;netstandard1.3 diff --git a/src/Discord.Net.Commands/Extensions/MessageExtensions.cs b/src/Discord.Net.Commands/Extensions/MessageExtensions.cs index 096b03f6b..436f1bb98 100644 --- a/src/Discord.Net.Commands/Extensions/MessageExtensions.cs +++ b/src/Discord.Net.Commands/Extensions/MessageExtensions.cs @@ -1,9 +1,15 @@ -using System; +using System; namespace Discord.Commands { + /// + /// Extension methods for that related to commands. + /// public static class MessageExtensions { + /// + /// Gets whether the message starts with the provided . + /// public static bool HasCharPrefix(this IUserMessage msg, char c, ref int argPos) { var text = msg.Content; @@ -14,6 +20,9 @@ namespace Discord.Commands } return false; } + /// + /// Gets whether the message starts with the provided . + /// public static bool HasStringPrefix(this IUserMessage msg, string str, ref int argPos, StringComparison comparisonType = StringComparison.Ordinal) { var text = msg.Content; @@ -24,6 +33,9 @@ namespace Discord.Commands } return false; } + /// + /// Gets whether the message starts with the user's mention string. + /// public static bool HasMentionPrefix(this IUserMessage msg, IUser user, ref int argPos) { var text = msg.Content; @@ -43,4 +55,4 @@ namespace Discord.Commands return false; } } -} \ No newline at end of file +} diff --git a/src/Discord.Net.Commands/Info/CommandInfo.cs b/src/Discord.Net.Commands/Info/CommandInfo.cs index f0d406e8d..de1462a3f 100644 --- a/src/Discord.Net.Commands/Info/CommandInfo.cs +++ b/src/Discord.Net.Commands/Info/CommandInfo.cs @@ -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 { + /// + /// Provides the information of a command. + /// + /// + /// This object contains the information of a command. This can include the module of the command, various + /// descriptions regarding the command, and its . + /// [DebuggerDisplay("{Name,nq}")] public class CommandInfo { @@ -21,17 +27,59 @@ namespace Discord.Commands private readonly CommandService _commandService; private readonly Func _action; + /// + /// Gets the module that the command belongs in. + /// public ModuleInfo Module { get; } + /// + /// Gets the name of the command. If none is set, the first alias is used. + /// public string Name { get; } + /// + /// Gets the summary of the command. + /// + /// + /// This field returns the summary of the command. and can be + /// useful in help commands and various implementation that fetches details of the command for the user. + /// public string Summary { get; } + /// + /// Gets the remarks of the command. + /// + /// + /// This field returns the summary of the command. and can be + /// useful in help commands and various implementation that fetches details of the command for the user. + /// public string Remarks { get; } + /// + /// Gets the priority of the command. This is used when there are multiple overloads of the command. + /// public int Priority { get; } + /// + /// Indicates whether the command accepts a [] for its + /// parameter. + /// public bool HasVarArgs { get; } + /// + /// Gets the that is being used for the command. + /// public RunMode RunMode { get; } + /// + /// Gets a list of aliases defined by the of the command. + /// public IReadOnlyList Aliases { get; } + /// + /// Gets a list of information about the parameters of the command. + /// public IReadOnlyList Parameters { get; } + /// + /// Gets a list of preconditions defined by the of the command. + /// public IReadOnlyList Preconditions { get; } + /// + /// Gets a list of attributes of the command. + /// public IReadOnlyList Attributes { get; } internal CommandInfo(CommandBuilder builder, ModuleInfo module, CommandService service) @@ -98,11 +146,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; @@ -121,7 +169,7 @@ namespace Discord.Commands string input = searchResult.Text.Substring(startIndex); return await CommandParser.ParseArgsAsync(this, context, _commandService._ignoreExtraArgs, services, input, 0).ConfigureAwait(false); } - + public Task ExecuteAsync(ICommandContext context, ParseResult parseResult, IServiceProvider services) { if (!parseResult.IsSuccess) @@ -165,11 +213,11 @@ namespace Discord.Commands switch (RunMode) { case RunMode.Sync: //Always sync - return await ExecuteAsyncInternalAsync(context, args, services).ConfigureAwait(false); + return await ExecuteInternalAsync(context, args, services).ConfigureAwait(false); case RunMode.Async: //Always async var t2 = Task.Run(async () => { - await ExecuteAsyncInternalAsync(context, args, services).ConfigureAwait(false); + await ExecuteInternalAsync(context, args, services).ConfigureAwait(false); }); break; } @@ -181,7 +229,7 @@ namespace Discord.Commands } } - private async Task ExecuteAsyncInternalAsync(ICommandContext context, object[] args, IServiceProvider services) + private async Task ExecuteInternalAsync(ICommandContext context, object[] args, IServiceProvider services) { await Module.Service._cmdLogger.DebugAsync($"Executing {GetLogText(context)}").ConfigureAwait(false); try @@ -245,11 +293,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) { diff --git a/src/Discord.Net.Commands/Info/ModuleInfo.cs b/src/Discord.Net.Commands/Info/ModuleInfo.cs index 5a7f9208e..54442afb7 100644 --- a/src/Discord.Net.Commands/Info/ModuleInfo.cs +++ b/src/Discord.Net.Commands/Info/ModuleInfo.cs @@ -2,7 +2,6 @@ using System; using System.Linq; using System.Collections.Generic; using System.Collections.Immutable; -using System.Reflection; using Discord.Commands.Builders; namespace Discord.Commands diff --git a/src/Discord.Net.Commands/Info/ParameterInfo.cs b/src/Discord.Net.Commands/Info/ParameterInfo.cs index 4a56415e5..150a98144 100644 --- a/src/Discord.Net.Commands/Info/ParameterInfo.cs +++ b/src/Discord.Net.Commands/Info/ParameterInfo.cs @@ -2,11 +2,12 @@ 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 { + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class ParameterInfo { private readonly TypeReader _reader; @@ -65,4 +66,4 @@ namespace Discord.Commands public override string ToString() => Name; private string DebuggerDisplay => $"{Name}{(IsOptional ? " (Optional)" : "")}{(IsRemainder ? " (Remainder)" : "")}"; } -} \ No newline at end of file +} diff --git a/src/Discord.Net.Commands/Map/CommandMap.cs b/src/Discord.Net.Commands/Map/CommandMap.cs index bcff800d3..dfc366333 100644 --- a/src/Discord.Net.Commands/Map/CommandMap.cs +++ b/src/Discord.Net.Commands/Map/CommandMap.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; namespace Discord.Commands { @@ -6,7 +6,6 @@ namespace Discord.Commands { private readonly CommandService _service; private readonly CommandMapNode _root; - private static readonly string[] _blankAliases = new[] { "" }; public CommandMap(CommandService service) { diff --git a/src/Discord.Net.Commands/Map/CommandMapNode.cs b/src/Discord.Net.Commands/Map/CommandMapNode.cs index 863409207..db69af415 100644 --- a/src/Discord.Net.Commands/Map/CommandMapNode.cs +++ b/src/Discord.Net.Commands/Map/CommandMapNode.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; @@ -7,7 +7,7 @@ namespace Discord.Commands { internal class CommandMapNode { - private static readonly char[] _whitespaceChars = new[] { ' ', '\r', '\n' }; + private static readonly char[] WhitespaceChars = new[] { ' ', '\r', '\n' }; private readonly ConcurrentDictionary _nodes; private readonly string _name; @@ -100,7 +100,7 @@ namespace Discord.Commands } //Check if this is the last command segment before args - nextSegment = NextSegment(text, index, _whitespaceChars, service._separatorChar); + nextSegment = NextSegment(text, index, WhitespaceChars, service._separatorChar); if (nextSegment != -1) { name = text.Substring(index, nextSegment - index); diff --git a/src/Discord.Net.Commands/ModuleBase.cs b/src/Discord.Net.Commands/ModuleBase.cs index c35a3cf67..77d945899 100644 --- a/src/Discord.Net.Commands/ModuleBase.cs +++ b/src/Discord.Net.Commands/ModuleBase.cs @@ -11,15 +11,26 @@ namespace Discord.Commands { public T Context { get; private set; } - protected virtual async Task ReplyAsync(string message, bool isTTS = false, Embed embed = null, RequestOptions options = null) + /// + /// Sends a message to the source channel + /// + /// Contents of the message; optional only if is specified + /// Specifies if Discord should read this message aloud using TTS + /// An embed to be displayed alongside the message + protected virtual async Task ReplyAsync(string message = null, bool isTTS = false, Embed embed = null, RequestOptions options = null) { return await Context.Channel.SendMessageAsync(message, isTTS, embed, options).ConfigureAwait(false); } - + /// + /// The method to execute before executing the command. + /// protected virtual void BeforeExecute(CommandInfo command) { } - + /// + /// The method to execute after executing the command. + /// + /// protected virtual void AfterExecute(CommandInfo command) { } @@ -32,7 +43,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); diff --git a/src/Discord.Net.Commands/MultiMatchHandling.cs b/src/Discord.Net.Commands/MultiMatchHandling.cs index 89dcf1c06..5dc84e266 100644 --- a/src/Discord.Net.Commands/MultiMatchHandling.cs +++ b/src/Discord.Net.Commands/MultiMatchHandling.cs @@ -1,8 +1,10 @@ -namespace Discord.Commands +namespace Discord.Commands { public enum MultiMatchHandling { + /// Indicates that when multiple results are found, an exception should be thrown. Exception, + /// Indicates that when multiple results are found, the best result should be chosen. Best } } diff --git a/src/Discord.Net.Commands/Readers/EnumTypeReader.cs b/src/Discord.Net.Commands/Readers/EnumTypeReader.cs index c097e6189..d0f93aed7 100644 --- a/src/Discord.Net.Commands/Readers/EnumTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/EnumTypeReader.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; @@ -53,14 +53,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}.")); } } } diff --git a/src/Discord.Net.Commands/Readers/NullableTypeReader.cs b/src/Discord.Net.Commands/Readers/NullableTypeReader.cs index 109689e15..1a64dc198 100644 --- a/src/Discord.Net.Commands/Readers/NullableTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/NullableTypeReader.cs @@ -28,7 +28,7 @@ namespace Discord.Commands { 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); } } } diff --git a/src/Discord.Net.Commands/Readers/PrimitiveTypeReader.cs b/src/Discord.Net.Commands/Readers/PrimitiveTypeReader.cs index b19a6bd69..011854a23 100644 --- a/src/Discord.Net.Commands/Readers/PrimitiveTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/PrimitiveTypeReader.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading.Tasks; namespace Discord.Commands @@ -24,7 +24,7 @@ namespace Discord.Commands public PrimitiveTypeReader(TryParseDelegate 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 +34,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}.")); } } } diff --git a/src/Discord.Net.Commands/Readers/RoleTypeReader.cs b/src/Discord.Net.Commands/Readers/RoleTypeReader.cs index 703374c05..838ff32b4 100644 --- a/src/Discord.Net.Commands/Readers/RoleTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/RoleTypeReader.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -11,15 +11,13 @@ namespace Discord.Commands { public override Task ReadAsync(ICommandContext context, string input, IServiceProvider services) { - ulong id; - if (context.Guild != null) { var results = new Dictionary(); 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) diff --git a/src/Discord.Net.Commands/Readers/UserTypeReader.cs b/src/Discord.Net.Commands/Readers/UserTypeReader.cs index 8fc330d4c..7a9e177ff 100644 --- a/src/Discord.Net.Commands/Readers/UserTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/UserTypeReader.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Globalization; @@ -15,13 +15,12 @@ namespace Discord.Commands var results = new Dictionary(); IAsyncEnumerable channelUsers = context.Channel.GetUsersAsync(CacheMode.CacheOnly).Flatten(); // it's better IReadOnlyCollection guildUsers = ImmutableArray.Create(); - 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 +45,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 +58,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 +69,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 as IGuildUser).Nickname, StringComparison.OrdinalIgnoreCase))) AddResult(results, guildUser as T, (guildUser as IGuildUser).Nickname == input ? 0.60f : 0.50f); diff --git a/src/Discord.Net.Commands/Results/ExecuteResult.cs b/src/Discord.Net.Commands/Results/ExecuteResult.cs index bad39e230..7eb252be5 100644 --- a/src/Discord.Net.Commands/Results/ExecuteResult.cs +++ b/src/Discord.Net.Commands/Results/ExecuteResult.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Diagnostics; namespace Discord.Commands diff --git a/src/Discord.Net.Commands/Results/IResult.cs b/src/Discord.Net.Commands/Results/IResult.cs index 928d1139e..eacea3cfe 100644 --- a/src/Discord.Net.Commands/Results/IResult.cs +++ b/src/Discord.Net.Commands/Results/IResult.cs @@ -1,9 +1,13 @@ -namespace Discord.Commands +namespace Discord.Commands { + /// Represents information of the command execution result. public interface IResult { + /// Describes the error type that may have occurred during the operation. CommandError? Error { get; } + /// Describes the reason for the error. string ErrorReason { get; } + /// Indicates whether the operation was successful or not. bool IsSuccess { get; } } } diff --git a/src/Discord.Net.Commands/Results/ParseResult.cs b/src/Discord.Net.Commands/Results/ParseResult.cs index d4a9af521..96ec667fe 100644 --- a/src/Discord.Net.Commands/Results/ParseResult.cs +++ b/src/Discord.Net.Commands/Results/ParseResult.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Diagnostics; namespace Discord.Commands @@ -9,9 +9,12 @@ namespace Discord.Commands public IReadOnlyList ArgValues { get; } public IReadOnlyList ParamValues { get; } + /// public CommandError? Error { get; } + /// public string ErrorReason { get; } + /// public bool IsSuccess => !Error.HasValue; private ParseResult(IReadOnlyList argValues, IReadOnlyList paramValues, CommandError? error, string errorReason) @@ -21,7 +24,7 @@ namespace Discord.Commands Error = error; ErrorReason = errorReason; } - + public static ParseResult FromSuccess(IReadOnlyList argValues, IReadOnlyList paramValues) { for (int i = 0; i < argValues.Count; i++) diff --git a/src/Discord.Net.Commands/Results/PreconditionGroupResult.cs b/src/Discord.Net.Commands/Results/PreconditionGroupResult.cs index 1d7f29122..cb7b860cd 100644 --- a/src/Discord.Net.Commands/Results/PreconditionGroupResult.cs +++ b/src/Discord.Net.Commands/Results/PreconditionGroupResult.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Diagnostics; namespace Discord.Commands @@ -14,11 +14,11 @@ namespace Discord.Commands PreconditionResults = (preconditions ?? new List(0)).ToReadOnlyCollection(); } - public static new PreconditionGroupResult FromSuccess() + public new static PreconditionGroupResult FromSuccess() => new PreconditionGroupResult(null, null, null); public static PreconditionGroupResult FromError(string reason, ICollection preconditions) => new PreconditionGroupResult(CommandError.UnmetPrecondition, reason, preconditions); - public static new PreconditionGroupResult FromError(IResult result) //needed? + public new static PreconditionGroupResult FromError(IResult result) //needed? => new PreconditionGroupResult(result.Error, result.ErrorReason, null); public override string ToString() => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; diff --git a/src/Discord.Net.Commands/Results/PreconditionResult.cs b/src/Discord.Net.Commands/Results/PreconditionResult.cs index ca65a373e..dd6adf31f 100644 --- a/src/Discord.Net.Commands/Results/PreconditionResult.cs +++ b/src/Discord.Net.Commands/Results/PreconditionResult.cs @@ -1,28 +1,55 @@ -using System.Diagnostics; +using System.Diagnostics; namespace Discord.Commands { + /// + /// Represents a result type for command preconditions. + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class PreconditionResult : IResult { + /// public CommandError? Error { get; } + /// public string ErrorReason { get; } + /// public bool IsSuccess => !Error.HasValue; + /// + /// Initializes a new class with the command type + /// and reason. + /// + /// The type of failure. + /// The reason of failure. protected PreconditionResult(CommandError? error, string errorReason) { Error = error; ErrorReason = errorReason; } + /// + /// Returns a with no errors. + /// public static PreconditionResult FromSuccess() => new PreconditionResult(null, null); + /// + /// Returns a with and the + /// specified reason. + /// + /// The reason of failure. public static PreconditionResult FromError(string reason) => new PreconditionResult(CommandError.UnmetPrecondition, reason); + /// + /// Returns a with the specified type. + /// + /// The result of failure. public static PreconditionResult FromError(IResult result) => new PreconditionResult(result.Error, result.ErrorReason); + /// + /// Returns a string indicating whether the is successful. + /// public override string ToString() => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; private string DebuggerDisplay => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; } diff --git a/src/Discord.Net.Commands/Results/RuntimeResult.cs b/src/Discord.Net.Commands/Results/RuntimeResult.cs index 2a326a7a3..a7febd68e 100644 --- a/src/Discord.Net.Commands/Results/RuntimeResult.cs +++ b/src/Discord.Net.Commands/Results/RuntimeResult.cs @@ -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 { + /// + /// Initializes a new class with the type of error and reason. + /// + /// The type of failure, or if none. + /// The reason of failure. protected RuntimeResult(CommandError? error, string reason) { Error = error; Reason = reason; } + /// public CommandError? Error { get; } + /// Describes the execution reason or result. public string Reason { get; } + /// public bool IsSuccess => !Error.HasValue; + /// string IResult.ErrorReason => Reason; public override string ToString() => Reason ?? (IsSuccess ? "Successful" : "Unsuccessful"); diff --git a/src/Discord.Net.Commands/Results/SearchResult.cs b/src/Discord.Net.Commands/Results/SearchResult.cs index 87d900d4d..65f627ab0 100644 --- a/src/Discord.Net.Commands/Results/SearchResult.cs +++ b/src/Discord.Net.Commands/Results/SearchResult.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Diagnostics; namespace Discord.Commands @@ -9,9 +9,12 @@ namespace Discord.Commands public string Text { get; } public IReadOnlyList Commands { get; } + /// public CommandError? Error { get; } + /// public string ErrorReason { get; } + /// public bool IsSuccess => !Error.HasValue; private SearchResult(string text, IReadOnlyList commands, CommandError? error, string errorReason) diff --git a/src/Discord.Net.Commands/Results/TypeReaderResult.cs b/src/Discord.Net.Commands/Results/TypeReaderResult.cs index 68bc359c6..49dd96b63 100644 --- a/src/Discord.Net.Commands/Results/TypeReaderResult.cs +++ b/src/Discord.Net.Commands/Results/TypeReaderResult.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; @@ -26,9 +26,12 @@ namespace Discord.Commands { public IReadOnlyCollection Values { get; } + /// public CommandError? Error { get; } + /// public string ErrorReason { get; } + /// public bool IsSuccess => !Error.HasValue; private TypeReaderResult(IReadOnlyCollection values, CommandError? error, string errorReason) diff --git a/src/Discord.Net.Commands/RunMode.cs b/src/Discord.Net.Commands/RunMode.cs index ecb6a4b58..b5ab1116b 100644 --- a/src/Discord.Net.Commands/RunMode.cs +++ b/src/Discord.Net.Commands/RunMode.cs @@ -1,9 +1,18 @@ -namespace Discord.Commands +namespace Discord.Commands { public enum RunMode { + /// + /// The default behaviour set in . + /// Default, + /// + /// Executes the command on the same thread as gateway one. + /// Sync, + /// + /// Executes the command on a different thread from the gateway one. + /// Async } } diff --git a/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs b/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs index 30dd7c36b..1eeff29b5 100644 --- a/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs +++ b/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs @@ -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,14 +45,14 @@ 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(); + var result = new List(); while (ownerType != _objectTypeInfo) { foreach (var prop in ownerType.DeclaredProperties) @@ -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."); diff --git a/src/Discord.Net.Core/Audio/AudioStream.cs b/src/Discord.Net.Core/Audio/AudioStream.cs index 97820ea73..532a6bb7f 100644 --- a/src/Discord.Net.Core/Audio/AudioStream.cs +++ b/src/Discord.Net.Core/Audio/AudioStream.cs @@ -11,10 +11,8 @@ 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"); - } + 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,11 +28,13 @@ namespace Discord.Audio public virtual Task ClearAsync(CancellationToken cancellationToken) { return Task.Delay(0); } - public override long Length { get { throw new NotSupportedException(); } } + public override long Length => + throw new NotSupportedException(); + 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(); } diff --git a/src/Discord.Net.Core/CDN.cs b/src/Discord.Net.Core/CDN.cs index 070b965ee..0c713f135 100644 --- a/src/Discord.Net.Core/CDN.cs +++ b/src/Discord.Net.Core/CDN.cs @@ -1,11 +1,20 @@ -using System; +using System; namespace Discord { + /// + /// Represents a class containing the strings related to various Content Delivery Networks (CDNs). + /// public static class CDN { + /// + /// Returns the Discord developer application icon. + /// public static string GetApplicationIconUrl(ulong appId, string iconId) => iconId != null ? $"{DiscordConfig.CDNUrl}app-icons/{appId}/{iconId}.jpg" : null; + /// + /// Returns the user avatar URL based on the and . + /// public static string GetUserAvatarUrl(ulong userId, string avatarId, ushort size, ImageFormat format) { if (avatarId == null) @@ -13,32 +22,66 @@ namespace Discord string extension = FormatToExtension(format, avatarId); return $"{DiscordConfig.CDNUrl}avatars/{userId}/{avatarId}.{extension}?size={size}"; } + /// + /// Returns the default user avatar URL. + /// + /// The discriminator value of a user. + public static string GetDefaultUserAvatarUrl(ushort discriminator) + { + return $"{DiscordConfig.CDNUrl}embed/avatars/{discriminator % 5}.png"; + } + /// + /// Returns the icon URL associated with the given guild ID. + /// public static string GetGuildIconUrl(ulong guildId, string iconId) => iconId != null ? $"{DiscordConfig.CDNUrl}icons/{guildId}/{iconId}.jpg" : null; + /// + /// Returns the guild splash URL associated with the given guild and splash ID. + /// public static string GetGuildSplashUrl(ulong guildId, string splashId) => splashId != null ? $"{DiscordConfig.CDNUrl}splashes/{guildId}/{splashId}.jpg" : null; + /// + /// Returns the channel icon URL associated with the given guild and icon ID. + /// public static string GetChannelIconUrl(ulong channelId, string iconId) => iconId != null ? $"{DiscordConfig.CDNUrl}channel-icons/{channelId}/{iconId}.jpg" : null; + /// + /// Returns the emoji URL based on the emoji ID. + /// public static string GetEmojiUrl(ulong emojiId, bool animated) => $"{DiscordConfig.CDNUrl}emojis/{emojiId}.{(animated ? "gif" : "png")}"; + /// + /// Returns the rich presence asset URL based on the asset ID and . + /// 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}"; } + /// + /// Returns the Spotify album URL based on the album art ID. + /// + public static string GetSpotifyAlbumArtUrl(string albumArtId) + => $"https://i.scdn.co/image/{albumArtId}"; + private static string FormatToExtension(ImageFormat format, string imageId) { if (format == ImageFormat.Auto) format = imageId.StartsWith("a_") ? ImageFormat.Gif : ImageFormat.Png; switch (format) { - case ImageFormat.Gif: return "gif"; - case ImageFormat.Jpeg: return "jpeg"; - case ImageFormat.Png: return "png"; - case ImageFormat.WebP: return "webp"; - default: throw new ArgumentException(nameof(format)); + case ImageFormat.Gif: + return "gif"; + case ImageFormat.Jpeg: + return "jpeg"; + case ImageFormat.Png: + return "png"; + case ImageFormat.WebP: + return "webp"; + default: + throw new ArgumentException(nameof(format)); } } } diff --git a/src/Discord.Net.Core/Commands/ICommandContext.cs b/src/Discord.Net.Core/Commands/ICommandContext.cs index ac1424339..8b682ba1c 100644 --- a/src/Discord.Net.Core/Commands/ICommandContext.cs +++ b/src/Discord.Net.Core/Commands/ICommandContext.cs @@ -1,11 +1,29 @@ -namespace Discord.Commands +namespace Discord.Commands { + /// + /// Represents the context of a command. This may include the client, guild, channel, user, and message. + /// public interface ICommandContext { + /// + /// Gets the that the command is executed with. + /// IDiscordClient Client { get; } + /// + /// Gets the that the command is executed in. + /// IGuild Guild { get; } + /// + /// Gets the that the command is executed in. + /// IMessageChannel Channel { get; } + /// + /// Gets the who executed the command. + /// IUser User { get; } + /// + /// Gets the that the command is interpreted from. + /// IUserMessage Message { get; } } } diff --git a/src/Discord.Net.Core/ConnectionState.cs b/src/Discord.Net.Core/ConnectionState.cs index 42c505ccd..fadbc4065 100644 --- a/src/Discord.Net.Core/ConnectionState.cs +++ b/src/Discord.Net.Core/ConnectionState.cs @@ -1,10 +1,15 @@ -namespace Discord +namespace Discord { + /// Specifies the connection state of a client. public enum ConnectionState : byte { + /// The client has disconnected from Discord. Disconnected, + /// The client is connecting to Discord. Connecting, + /// The client has established a connection to Discord. Connected, + /// The client is disconnecting from Discord. Disconnecting } } diff --git a/src/Discord.Net.Core/DiscordConfig.cs b/src/Discord.Net.Core/DiscordConfig.cs index fd2fe92e8..b3cdddfa5 100644 --- a/src/Discord.Net.Core/DiscordConfig.cs +++ b/src/Discord.Net.Core/DiscordConfig.cs @@ -1,33 +1,75 @@ -using System.Reflection; +using System.Reflection; namespace Discord { + /// + /// Defines various behaviors of Discord.Net. + /// public class DiscordConfig { - public const int APIVersion = 6; + /// + /// Returns the gateway version Discord.Net uses. + /// + public const int APIVersion = 6; + /// + /// Gets the Discord.Net version, including the build number. + /// public static string Version { get; } = typeof(DiscordConfig).GetTypeInfo().Assembly.GetCustomAttribute()?.InformationalVersion ?? typeof(DiscordConfig).GetTypeInfo().Assembly.GetName().Version.ToString(3) ?? "Unknown"; - + + /// + /// Gets the user agent that Discord.Net uses in its clients. + /// public static string UserAgent { get; } = $"DiscordBot (https://github.com/RogueException/Discord.Net, v{Version})"; + /// + /// Returns the base Discord API URL. + /// public static readonly string APIUrl = $"https://discordapp.com/api/v{APIVersion}/"; + /// + /// Returns the base Discord CDN URL. + /// public const string CDNUrl = "https://cdn.discordapp.com/"; + /// + /// Returns the base Discord invite URL. + /// public const string InviteUrl = "https://discord.gg/"; + /// + /// Returns the default timeout for requests. + /// public const int DefaultRequestTimeout = 15000; + /// + /// Returns the max length for a Discord message. + /// public const int MaxMessageSize = 2000; + /// + /// Returns the max messages allowed to be in a request. + /// public const int MaxMessagesPerBatch = 100; + /// + /// Returns the max users allowed to be in a request. + /// public const int MaxUsersPerBatch = 1000; + /// + /// Returns the max guilds allowed to be in a request. + /// public const int MaxGuildsPerBatch = 100; - /// Gets or sets how a request should act in the case of an error, by default. + /// + /// Gets or sets how a request should act in the case of an error, by default. + /// public RetryMode DefaultRetryMode { get; set; } = RetryMode.AlwaysRetry; - /// Gets or sets the minimum log level severity that will be sent to the Log event. + /// + /// Gets or sets the minimum log level severity that will be sent to the Log event. + /// public LogSeverity LogLevel { get; set; } = LogSeverity.Info; - /// Gets or sets whether the initial log entry should be printed. + /// + /// Gets or sets whether the initial log entry should be printed. + /// internal bool DisplayInitialLog { get; set; } = true; } } diff --git a/src/Discord.Net.Core/Entities/Activities/ActivityType.cs b/src/Discord.Net.Core/Entities/Activities/ActivityType.cs index c7db7b247..67235c499 100644 --- a/src/Discord.Net.Core/Entities/Activities/ActivityType.cs +++ b/src/Discord.Net.Core/Entities/Activities/ActivityType.cs @@ -1,10 +1,25 @@ -namespace Discord +namespace Discord { + /// + /// Specifies a Discord user's activity type. + /// public enum ActivityType { + /// + /// The user is playing a game. + /// Playing = 0, + /// + /// The user is streaming online. + /// Streaming = 1, + /// + /// The user is listening to a song. + /// Listening = 2, + /// + /// The user is watching a media. + /// Watching = 3 } } diff --git a/src/Discord.Net.Core/Entities/Activities/Game.cs b/src/Discord.Net.Core/Entities/Activities/Game.cs index fe32470ee..471cc9f64 100644 --- a/src/Discord.Net.Core/Entities/Activities/Game.cs +++ b/src/Discord.Net.Core/Entities/Activities/Game.cs @@ -1,20 +1,31 @@ -using System.Diagnostics; +using System.Diagnostics; namespace Discord { + /// + /// A user's game status. + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class Game : IActivity { + /// public string Name { get; internal set; } + /// public ActivityType Type { get; internal set; } internal Game() { } + /// + /// Creates a with the provided and . + /// + /// The name of the game. + /// The type of activity. Default is . public Game(string name, ActivityType type = ActivityType.Playing) { Name = name; Type = type; } + /// Returns the name of the . public override string ToString() => Name; private string DebuggerDisplay => Name; } diff --git a/src/Discord.Net.Core/Entities/Activities/GameAsset.cs b/src/Discord.Net.Core/Entities/Activities/GameAsset.cs index 385f37214..b0c8ea975 100644 --- a/src/Discord.Net.Core/Entities/Activities/GameAsset.cs +++ b/src/Discord.Net.Core/Entities/Activities/GameAsset.cs @@ -1,15 +1,27 @@ -namespace Discord +namespace Discord { + /// + /// An asset for a object containing the text and image. + /// public class GameAsset { internal GameAsset() { } - internal ulong ApplicationId { get; set; } - + internal ulong? ApplicationId { get; set; } + + /// + /// Gets the description of the asset. + /// public string Text { get; internal set; } + /// + /// Gets the image ID of the asset. + /// public string ImageId { get; internal set; } - + + /// + /// Returns the image URL of the asset, or when the application ID does not exist. + /// public string GetImageUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) - => CDN.GetRichAssetUrl(ApplicationId, ImageId, size, format); + => ApplicationId.HasValue ? CDN.GetRichAssetUrl(ApplicationId.Value, ImageId, size, format) : null; } -} \ No newline at end of file +} diff --git a/src/Discord.Net.Core/Entities/Activities/GameParty.cs b/src/Discord.Net.Core/Entities/Activities/GameParty.cs index 54e6deef4..c3449df36 100644 --- a/src/Discord.Net.Core/Entities/Activities/GameParty.cs +++ b/src/Discord.Net.Core/Entities/Activities/GameParty.cs @@ -1,11 +1,20 @@ namespace Discord { + /// + /// Party information for a object. + /// public class GameParty { internal GameParty() { } + /// + /// Gets the ID of the party. + /// public string Id { get; internal set; } public long Members { get; internal set; } + /// + /// Gets the party's current and maximum size. + /// public long Capacity { get; internal set; } } } diff --git a/src/Discord.Net.Core/Entities/Activities/GameSecrets.cs b/src/Discord.Net.Core/Entities/Activities/GameSecrets.cs index e9d988ba9..595b8851c 100644 --- a/src/Discord.Net.Core/Entities/Activities/GameSecrets.cs +++ b/src/Discord.Net.Core/Entities/Activities/GameSecrets.cs @@ -1,9 +1,21 @@ -namespace Discord +namespace Discord { + /// + /// Party secret for a object. + /// public class GameSecrets { + /// + /// Gets the secret for a specific instanced match. + /// public string Match { get; } + /// + /// Gets the secret for joining a party. + /// public string Join { get; } + /// + /// Gets the secret for spectating a game. + /// public string Spectate { get; } internal GameSecrets(string match, string join, string spectate) @@ -13,4 +25,4 @@ Spectate = spectate; } } -} \ No newline at end of file +} diff --git a/src/Discord.Net.Core/Entities/Activities/GameTimestamps.cs b/src/Discord.Net.Core/Entities/Activities/GameTimestamps.cs index 8c8c992fa..a41388afb 100644 --- a/src/Discord.Net.Core/Entities/Activities/GameTimestamps.cs +++ b/src/Discord.Net.Core/Entities/Activities/GameTimestamps.cs @@ -1,10 +1,19 @@ -using System; +using System; namespace Discord { + /// + /// Timestamps for a object. + /// public class GameTimestamps { + /// + /// Gets when the activity started. + /// public DateTimeOffset? Start { get; } + /// + /// Gets when the activity ends. + /// public DateTimeOffset? End { get; } internal GameTimestamps(DateTimeOffset? start, DateTimeOffset? end) @@ -13,4 +22,4 @@ namespace Discord End = end; } } -} \ No newline at end of file +} diff --git a/src/Discord.Net.Core/Entities/Activities/IActivity.cs b/src/Discord.Net.Core/Entities/Activities/IActivity.cs index 1f158217d..30d936952 100644 --- a/src/Discord.Net.Core/Entities/Activities/IActivity.cs +++ b/src/Discord.Net.Core/Entities/Activities/IActivity.cs @@ -1,8 +1,17 @@ -namespace Discord +namespace Discord { + /// + /// A user's activity status, typically a . + /// public interface IActivity { + /// + /// Gets the name of the activity. + /// string Name { get; } + /// + /// Gets the type of the activity. + /// ActivityType Type { get; } } } diff --git a/src/Discord.Net.Core/Entities/Activities/RichGame.cs b/src/Discord.Net.Core/Entities/Activities/RichGame.cs index e66eac1d2..2455fd557 100644 --- a/src/Discord.Net.Core/Entities/Activities/RichGame.cs +++ b/src/Discord.Net.Core/Entities/Activities/RichGame.cs @@ -1,22 +1,52 @@ -using System.Diagnostics; +using System.Diagnostics; namespace Discord { + /// + /// A user's Rich Presence status. + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class RichGame : Game { internal RichGame() { } - public string Details { get; internal set;} - public string State { get; internal set;} + /// + /// Gets what the player is currently doing. + /// + public string Details { get; internal set; } + /// + /// Gets the user's current party status. + /// + public string State { get; internal set; } + /// + /// Gets the application ID for the game. + /// public ulong ApplicationId { get; internal set; } + /// + /// Gets the small image for the presence and their hover texts. + /// public GameAsset SmallAsset { get; internal set; } + /// + /// Gets the large image for the presence and their hover texts. + /// public GameAsset LargeAsset { get; internal set; } + /// + /// Gets the information for the current party of the player. + /// public GameParty Party { get; internal set; } + /// + /// Gets the secrets for Rich Presence joining and spectating. + /// public GameSecrets Secrets { get; internal set; } + /// + /// Gets the timestamps for start and/or end of the game. + /// public GameTimestamps Timestamps { get; internal set; } - + + /// + /// Returns the name of the Rich Presence. + /// public override string ToString() => Name; private string DebuggerDisplay => $"{Name} (Rich)"; } -} \ No newline at end of file +} diff --git a/src/Discord.Net.Core/Entities/Activities/SpotifyGame.cs b/src/Discord.Net.Core/Entities/Activities/SpotifyGame.cs new file mode 100644 index 000000000..ef75d7678 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Activities/SpotifyGame.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Discord +{ + /// + /// A user's activity for listening to a song on Spotify. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class SpotifyGame : Game + { + /// + /// Gets the song's artist(s). + /// + public IEnumerable Artists { get; internal set; } + /// + /// Gets the Spotify album art of the song. + /// + public string AlbumArt { get; internal set; } + /// + /// Gets the Spotify album title of the song. + /// + public string AlbumTitle { get; internal set; } + /// + /// Gets the track title of the song. + /// + public string TrackTitle { get; internal set; } + /// + /// Gets the synchronization ID of the song. + /// + public string SyncId { get; internal set; } + /// + /// Gets the session ID of the song. + /// + public string SessionId { get; internal set; } + /// + /// Gets the duration of the song. + /// + public TimeSpan? Duration { get; internal set; } + + internal SpotifyGame() { } + + /// Gets the name of the song. + public override string ToString() => Name; + private string DebuggerDisplay => $"{Name} (Spotify)"; + } +} diff --git a/src/Discord.Net.Core/Entities/Activities/StreamingGame.cs b/src/Discord.Net.Core/Entities/Activities/StreamingGame.cs index afbc24cd9..127ae0b7f 100644 --- a/src/Discord.Net.Core/Entities/Activities/StreamingGame.cs +++ b/src/Discord.Net.Core/Entities/Activities/StreamingGame.cs @@ -1,12 +1,23 @@ -using System.Diagnostics; +using System.Diagnostics; namespace Discord { + /// + /// A user's activity for streaming on services such as Twitch. + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class StreamingGame : Game { + /// + /// Gets the URL of the stream. + /// public string Url { get; internal set; } + /// + /// Creates a new based on the on the stream URL. + /// + /// The name of the stream. + /// The URL of the stream. public StreamingGame(string name, string url) { Name = name; @@ -14,7 +25,10 @@ namespace Discord Type = ActivityType.Streaming; } + /// + /// Gets the name of the stream. + /// public override string ToString() => Name; private string DebuggerDisplay => $"{Name} ({Url})"; } -} \ No newline at end of file +} diff --git a/src/Discord.Net.Core/Entities/CacheMode.cs b/src/Discord.Net.Core/Entities/CacheMode.cs index a047bd616..503a80479 100644 --- a/src/Discord.Net.Core/Entities/CacheMode.cs +++ b/src/Discord.Net.Core/Entities/CacheMode.cs @@ -1,8 +1,17 @@ -namespace Discord +namespace Discord { + /// + /// Specifies the cache mode that should be used. + /// public enum CacheMode { + /// + /// Allows the object to be downloaded if it does not exist in the current cache. + /// AllowDownload, + /// + /// Only allows the object to be pulled from the existing cache. + /// CacheOnly } } diff --git a/src/Discord.Net.Core/Entities/Channels/Direction.cs b/src/Discord.Net.Core/Entities/Channels/Direction.cs index 5d8d5e621..6d5f7cd6b 100644 --- a/src/Discord.Net.Core/Entities/Channels/Direction.cs +++ b/src/Discord.Net.Core/Entities/Channels/Direction.cs @@ -1,9 +1,21 @@ -namespace Discord +namespace Discord { + /// + /// Specifies the direction of where message(s) should be gotten from. + /// public enum Direction { + /// + /// The message(s) should be retrieved before a message. + /// Before, + /// + /// The message(s) should be retrieved after a message. + /// After, + /// + /// The message(s) should be retrieved around a message. + /// Around } } diff --git a/src/Discord.Net.Core/Entities/Channels/GuildChannelProperties.cs b/src/Discord.Net.Core/Entities/Channels/GuildChannelProperties.cs index 2ac6c8d52..be1bac363 100644 --- a/src/Discord.Net.Core/Entities/Channels/GuildChannelProperties.cs +++ b/src/Discord.Net.Core/Entities/Channels/GuildChannelProperties.cs @@ -1,33 +1,35 @@ -namespace Discord +namespace Discord { /// - /// Modify an IGuildChannel with the specified changes. + /// Properties that are used to modify an with the specified changes. /// /// - /// - /// await (Context.Channel as ITextChannel)?.ModifyAsync(x => - /// { - /// x.Name = "do-not-enter"; - /// }); - /// + /// + /// await (Context.Channel as ITextChannel)?.ModifyAsync(x => + /// { + /// x.Name = "do-not-enter"; + /// }); + /// /// public class GuildChannelProperties { /// - /// Set the channel to this name + /// Gets or sets the channel to this name. /// /// - /// When modifying an ITextChannel, the Name MUST be alphanumeric with dashes. - /// It must match the following RegEx: [a-z0-9-_]{2,100} + /// When modifying an , the + /// MUST be alphanumeric with dashes. It must match the following RegEx: [a-z0-9-_]{2,100} /// - /// A BadRequest will be thrown if the name does not match the above RegEx. + /// + /// A BadRequest will be thrown if the name does not match the above RegEx. + /// public Optional Name { get; set; } /// - /// Move the channel to the following position. This is 0-based! + /// Moves the channel to the following position. This is 0-based! /// public Optional Position { get; set; } /// - /// Sets the category for this channel + /// Gets or sets the category for this channel. /// public Optional CategoryId { get; set; } } diff --git a/src/Discord.Net.Core/Entities/Channels/IAudioChannel.cs b/src/Discord.Net.Core/Entities/Channels/IAudioChannel.cs index afb81d92f..6295a6829 100644 --- a/src/Discord.Net.Core/Entities/Channels/IAudioChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IAudioChannel.cs @@ -1,12 +1,17 @@ -using Discord.Audio; +using Discord.Audio; using System; using System.Threading.Tasks; namespace Discord { + /// + /// Represents a generic audio channel. + /// public interface IAudioChannel : IChannel { - /// Connects to this audio channel. + /// + /// Connects to this audio channel. + /// Task ConnectAsync(Action configAction = null); } } diff --git a/src/Discord.Net.Core/Entities/Channels/ICategoryChannel.cs b/src/Discord.Net.Core/Entities/Channels/ICategoryChannel.cs index 0f7f5aa62..838908b68 100644 --- a/src/Discord.Net.Core/Entities/Channels/ICategoryChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/ICategoryChannel.cs @@ -1,11 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - namespace Discord { + /// + /// Represents a generic category channel. + /// public interface ICategoryChannel : IGuildChannel { } diff --git a/src/Discord.Net.Core/Entities/Channels/IChannel.cs b/src/Discord.Net.Core/Entities/Channels/IChannel.cs index ea930e112..85138ad60 100644 --- a/src/Discord.Net.Core/Entities/Channels/IChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IChannel.cs @@ -1,17 +1,26 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading.Tasks; namespace Discord { + /// + /// Represents a generic Discord channel. + /// public interface IChannel : ISnowflakeEntity { - /// Gets the name of this channel. + /// + /// Gets the name of this channel. + /// string Name { get; } - /// Gets a collection of all users in this channel. + /// + /// Gets a collection of all users in this channel. + /// IAsyncEnumerable> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); - /// Gets a user in this channel with the provided id. + /// + /// Gets a user in this channel with the provided ID. + /// Task GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); } } diff --git a/src/Discord.Net.Core/Entities/Channels/IDMChannel.cs b/src/Discord.Net.Core/Entities/Channels/IDMChannel.cs index 1608d1543..bde44b2ed 100644 --- a/src/Discord.Net.Core/Entities/Channels/IDMChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IDMChannel.cs @@ -1,13 +1,20 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; namespace Discord { + /// + /// Represents a generic DM channel. + /// public interface IDMChannel : IMessageChannel, IPrivateChannel { - /// Gets the recipient of all messages in this channel. + /// + /// Gets the recipient of all messages in this channel. + /// IUser Recipient { get; } - /// Closes this private channel, removing it from your channel list. + /// + /// Closes this private channel, removing it from your channel list. + /// Task CloseAsync(RequestOptions options = null); } -} \ No newline at end of file +} diff --git a/src/Discord.Net.Core/Entities/Channels/IGroupChannel.cs b/src/Discord.Net.Core/Entities/Channels/IGroupChannel.cs index d6cb2c182..75795f582 100644 --- a/src/Discord.Net.Core/Entities/Channels/IGroupChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IGroupChannel.cs @@ -1,10 +1,15 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; namespace Discord { + /// + /// Represents a private generic group channel. + /// public interface IGroupChannel : IMessageChannel, IPrivateChannel, IAudioChannel { - /// Leaves this group. + /// + /// Leaves this group. + /// Task LeaveAsync(RequestOptions options = null); } -} \ No newline at end of file +} diff --git a/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs b/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs index c9841cb15..9a2651dfa 100644 --- a/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs @@ -1,52 +1,98 @@ -using System; +using System; using System.Collections.Generic; using System.Threading.Tasks; namespace Discord { + /// + /// Represents a guild channel (text, voice, category). + /// public interface IGuildChannel : IChannel, IDeletable { - /// Gets the position of this channel in the guild's channel list, relative to others of the same type. + /// + /// Gets the position of this channel in the guild's channel list, relative to others of the same type. + /// int Position { get; } - /// Gets the parentid (category) of this channel in the guild's channel list. + /// + /// Gets the parent ID (category) of this channel in the guild's channel list. + /// ulong? CategoryId { get; } - /// Gets the parent channel (category) of this channel. + /// + /// Gets the parent channel (category) of this channel. + /// Task GetCategoryAsync(); - /// Gets the guild this channel is a member of. + /// + /// Gets the guild this channel is a member of. + /// IGuild Guild { get; } - /// Gets the id of the guild this channel is a member of. + /// + /// Gets the id of the guild this channel is a member of. + /// ulong GuildId { get; } - /// Gets a collection of permission overwrites for this channel. + /// + /// Gets a collection of permission overwrites for this channel. + /// IReadOnlyCollection PermissionOverwrites { get; } - /// Creates a new invite to this channel. - /// The time (in seconds) until the invite expires. Set to null to never expire. - /// The max amount of times this invite may be used. Set to null to have unlimited uses. - /// If true, a user accepting this invite will be kicked from the guild after closing their client. + /// + /// Creates a new invite to this channel. + /// + /// + /// The time (in seconds) until the invite expires. Set to to never expire. + /// + /// + /// The max amount of times this invite may be used. Set to to have unlimited uses. + /// + /// + /// If , a user accepting this invite will be kicked from the guild after closing their client. + /// + /// + /// If , don't try to reuse a similar invite (useful for creating many unique one time use invites). + /// Task CreateInviteAsync(int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null); - /// Returns a collection of all invites to this channel. + /// + /// Returns a collection of all invites to this channel. + /// Task> GetInvitesAsync(RequestOptions options = null); - /// Modifies this guild channel. + /// + /// Modifies this guild channel. + /// Task ModifyAsync(Action func, RequestOptions options = null); - /// Gets the permission overwrite for a specific role, or null if one does not exist. + /// + /// Gets the permission overwrite for a specific role, or if one does not exist. + /// OverwritePermissions? GetPermissionOverwrite(IRole role); - /// Gets the permission overwrite for a specific user, or null if one does not exist. + /// + /// Gets the permission overwrite for a specific user, or if one does not exist. + /// OverwritePermissions? GetPermissionOverwrite(IUser user); - /// Removes the permission overwrite for the given role, if one exists. + /// + /// Removes the permission overwrite for the given role, if one exists. + /// Task RemovePermissionOverwriteAsync(IRole role, RequestOptions options = null); - /// Removes the permission overwrite for the given user, if one exists. + /// + /// Removes the permission overwrite for the given user, if one exists. + /// Task RemovePermissionOverwriteAsync(IUser user, RequestOptions options = null); - /// Adds or updates the permission overwrite for the given role. + /// + /// Adds or updates the permission overwrite for the given role. + /// Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options = null); - /// Adds or updates the permission overwrite for the given user. + /// + /// Adds or updates the permission overwrite for the given user. + /// Task AddPermissionOverwriteAsync(IUser user, OverwritePermissions permissions, RequestOptions options = null); - /// Gets a collection of all users in this channel. + /// + /// Gets a collection of all users in this channel. + /// new IAsyncEnumerable> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); - /// Gets a user in this channel with the provided id. + /// + /// Gets a user in this channel with the provided ID. + /// new Task GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); } -} \ No newline at end of file +} diff --git a/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs b/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs index 5a6e5df59..1d855ba08 100644 --- a/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs @@ -5,34 +5,58 @@ using System.Threading.Tasks; namespace Discord { + /// + /// Represents a generic channel that can send and receive messages. + /// public interface IMessageChannel : IChannel { - /// Sends a message to this message channel. + /// + /// Sends a message to this message channel. + /// Task SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null); #if FILESYSTEM - /// Sends a file to this text channel, with an optional caption. + /// + /// Sends a file to this message channel, with an optional caption. + /// Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null); #endif - /// Sends a file to this text channel, with an optional caption. + /// + /// Sends a file to this message channel, with an optional caption. + /// Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null); - /// Gets a message from this message channel with the given id, or null if not found. + /// + /// Gets a message from this message channel with the given id, or if not found. + /// Task GetMessageAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); - /// Gets the last N messages from this message channel. + /// + /// Gets the last N messages from this message channel. + /// IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); - /// Gets a collection of messages in this channel. + /// + /// Gets a collection of messages in this channel. + /// IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); - /// Gets a collection of messages in this channel. + /// + /// Gets a collection of messages in this channel. + /// IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); - /// Gets a collection of pinned messages in this channel. + /// + /// Gets a collection of pinned messages in this channel. + /// Task> GetPinnedMessagesAsync(RequestOptions options = null); - /// Broadcasts the "user is typing" message to all users in this channel, lasting 10 seconds. + /// + /// Broadcasts the "user is typing" message to all users in this channel, lasting 10 seconds. + /// Task TriggerTypingAsync(RequestOptions options = null); - /// Continuously broadcasts the "user is typing" message to all users in this channel until the returned object is disposed. + /// + /// Continuously broadcasts the "user is typing" message to all users in this channel until the returned + /// object is disposed. + /// IDisposable EnterTypingState(RequestOptions options = null); } } diff --git a/src/Discord.Net.Core/Entities/Channels/IPrivateChannel.cs b/src/Discord.Net.Core/Entities/Channels/IPrivateChannel.cs index 9a3289794..fecb4fc19 100644 --- a/src/Discord.Net.Core/Entities/Channels/IPrivateChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IPrivateChannel.cs @@ -1,9 +1,15 @@ -using System.Collections.Generic; +using System.Collections.Generic; namespace Discord { + /// + /// Represents a generic channel that is private to select recipients. + /// public interface IPrivateChannel : IChannel { + /// + /// Users that can access this channel. + /// IReadOnlyCollection Recipients { get; } } } diff --git a/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs b/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs index 7c6ec3908..1998083af 100644 --- a/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs @@ -1,31 +1,50 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Threading.Tasks; namespace Discord { + /// + /// Represents a generic channel in a guild that can send and receive messages. + /// public interface ITextChannel : IMessageChannel, IMentionable, IGuildChannel { - /// Checks if the channel is NSFW. + /// + /// Gets whether the channel is NSFW. + /// bool IsNsfw { get; } - /// Gets the current topic for this text channel. + /// + /// Gets the current topic for this text channel. + /// string Topic { get; } - /// Bulk deletes multiple messages. + /// + /// Bulk deletes multiple messages. + /// Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null); - /// Bulk deletes multiple messages. + /// + /// Bulk deletes multiple messages. + /// Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null); - /// Modifies this text channel. + /// + /// Modifies this text channel. + /// Task ModifyAsync(Action func, RequestOptions options = null); - /// Creates a webhook in this text channel. + /// + /// Creates a webhook in this text channel. + /// Task CreateWebhookAsync(string name, Stream avatar = null, RequestOptions options = null); - /// Gets the webhook in this text channel with the provided id, or null if not found. + /// + /// Gets the webhook in this text channel with the provided ID, or if not found. + /// Task GetWebhookAsync(ulong id, RequestOptions options = null); - /// Gets the webhooks for this text channel. + /// + /// Gets the webhooks for this text channel. + /// Task> GetWebhooksAsync(RequestOptions options = null); } -} \ No newline at end of file +} diff --git a/src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs b/src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs index e2a2ad8eb..e6e589235 100644 --- a/src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs @@ -1,16 +1,26 @@ -using System; +using System; using System.Threading.Tasks; namespace Discord { + /// + /// Represents a voice channel in a guild. + /// public interface IVoiceChannel : IGuildChannel, IAudioChannel { - /// Gets the bitrate, in bits per second, clients in this voice channel are requested to use. + /// + /// Gets the bitrate, in bits per second, clients in this voice channel are requested to use. + /// int Bitrate { get; } - /// Gets the max amount of users allowed to be connected to this channel at one time. + /// + /// Gets the max amount of users allowed to be connected to this channel at one time, or + /// if none is set. + /// int? UserLimit { get; } - /// Modifies this voice channel. + /// + /// Modifies this voice channel. + /// Task ModifyAsync(Action func, RequestOptions options = null); } -} \ No newline at end of file +} diff --git a/src/Discord.Net.Core/Entities/Channels/ReorderChannelProperties.cs b/src/Discord.Net.Core/Entities/Channels/ReorderChannelProperties.cs index 31f814334..02cb2547c 100644 --- a/src/Discord.Net.Core/Entities/Channels/ReorderChannelProperties.cs +++ b/src/Discord.Net.Core/Entities/Channels/ReorderChannelProperties.cs @@ -1,12 +1,22 @@ -namespace Discord +namespace Discord { + /// + /// Properties that are used to reorder an . + /// public class ReorderChannelProperties { - /// The id of the channel to apply this position to. + /// + /// Gets the ID of the channel to apply this position to. + /// public ulong Id { get; } - /// The new zero-based position of this channel. + /// + /// Gets the new zero-based position of this channel. + /// public int Position { get; } + /// Creates a used to reorder a channel. + /// Sets the ID of the channel to apply this position to. + /// Sets the new zero-based position of this channel. public ReorderChannelProperties(ulong id, int position) { Id = id; diff --git a/src/Discord.Net.Core/Entities/Channels/TextChannelProperties.cs b/src/Discord.Net.Core/Entities/Channels/TextChannelProperties.cs index b7b568133..b68c416b7 100644 --- a/src/Discord.Net.Core/Entities/Channels/TextChannelProperties.cs +++ b/src/Discord.Net.Core/Entities/Channels/TextChannelProperties.cs @@ -1,14 +1,16 @@ -namespace Discord +namespace Discord { - /// + /// + /// Properties that are used to modify an with the specified changes. + /// public class TextChannelProperties : GuildChannelProperties { /// - /// What the topic of the channel should be set to. + /// Gets or sets the topic of the channel. /// public Optional Topic { get; set; } /// - /// Should this channel be flagged as NSFW? + /// Gets or sets whether this channel should be flagged as NSFW. /// public Optional IsNsfw { get; set; } } diff --git a/src/Discord.Net.Core/Entities/Channels/VoiceChannelProperties.cs b/src/Discord.Net.Core/Entities/Channels/VoiceChannelProperties.cs index 81dd8063e..c285560df 100644 --- a/src/Discord.Net.Core/Entities/Channels/VoiceChannelProperties.cs +++ b/src/Discord.Net.Core/Entities/Channels/VoiceChannelProperties.cs @@ -1,14 +1,16 @@ -namespace Discord +namespace Discord { - /// + /// + /// Properties that are used to modify an with the specified changes. + /// public class VoiceChannelProperties : GuildChannelProperties { /// - /// 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. /// public Optional Bitrate { get; set; } /// - /// 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 if none. /// public Optional UserLimit { get; set; } } diff --git a/src/Discord.Net.Core/Entities/Emotes/Emoji.cs b/src/Discord.Net.Core/Entities/Emotes/Emoji.cs index c2dfc31ad..da3c512c5 100644 --- a/src/Discord.Net.Core/Entities/Emotes/Emoji.cs +++ b/src/Discord.Net.Core/Entities/Emotes/Emoji.cs @@ -1,28 +1,34 @@ -namespace Discord +namespace Discord { /// - /// A unicode emoji + /// A Unicode emoji. /// public class Emoji : IEmote { - // TODO: need to constrain this to unicode-only emojis somehow - + // TODO: need to constrain this to Unicode-only emojis somehow + /// - /// The unicode representation of this emote. + /// Gets the Unicode representation of this emote. /// public string Name { get; } - + /// + /// Gets the Unicode representation of this emote. + /// public override string ToString() => Name; /// - /// Creates a unicode emoji. + /// Creates a Unicode emoji. /// - /// The pure UTF-8 encoding of an emoji + /// The pure UTF-8 encoding of an emoji. public Emoji(string unicode) { Name = unicode; } + /// + /// Determines whether the specified emoji is equal to the current emoji. + /// + /// The object to compare with the current object. public override bool Equals(object other) { if (other == null) return false; @@ -34,6 +40,7 @@ return string.Equals(Name, otherEmoji.Name); } + /// public override int GetHashCode() => Name.GetHashCode(); } } diff --git a/src/Discord.Net.Core/Entities/Emotes/Emote.cs b/src/Discord.Net.Core/Entities/Emotes/Emote.cs index e3a228c83..682ca33b7 100644 --- a/src/Discord.Net.Core/Entities/Emotes/Emote.cs +++ b/src/Discord.Net.Core/Entities/Emotes/Emote.cs @@ -1,26 +1,32 @@ -using System; +using System; using System.Globalization; namespace Discord { /// - /// A custom image-based emote + /// A custom image-based emote. /// public class Emote : IEmote, ISnowflakeEntity { /// - /// The display name (tooltip) of this emote + /// Gets the display name (tooltip) of this emote. /// public string Name { get; } /// - /// The ID of this emote + /// Gets the ID of this emote. /// public ulong Id { get; } /// - /// Is this emote animated? + /// Gets whether this emote is animated. /// public bool Animated { get; } + /// + /// Gets the date when this emote is created. + /// public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); + /// + /// Gets the image URL of this emote. + /// public string Url => CDN.GetEmojiUrl(Id, Animated); internal Emote(ulong id, string name, bool animated) @@ -49,16 +55,14 @@ namespace Discord } } - /// - /// Parse an Emote from its raw format - /// - /// The raw encoding of an emote; for example, <:dab:277855270321782784> + /// Parses an from its raw format. + /// The raw encoding of an emote; for example, <:dab:277855270321782784>. /// An emote public static Emote Parse(string text) { if (TryParse(text, out Emote result)) return result; - throw new ArgumentException("Invalid emote format", nameof(text)); + throw new ArgumentException("Invalid emote format.", nameof(text)); } public static bool TryParse(string text, out Emote result) diff --git a/src/Discord.Net.Core/Entities/Emotes/EmoteProperties.cs b/src/Discord.Net.Core/Entities/Emotes/EmoteProperties.cs index be24d306c..255bf0721 100644 --- a/src/Discord.Net.Core/Entities/Emotes/EmoteProperties.cs +++ b/src/Discord.Net.Core/Entities/Emotes/EmoteProperties.cs @@ -1,10 +1,19 @@ -using System.Collections.Generic; +using System.Collections.Generic; namespace Discord { + /// + /// Properties that are used to modify an with the specified changes. + /// public class EmoteProperties { + /// + /// Gets or sets the name of the . + /// public Optional Name { get; set; } + /// + /// Gets or sets the roles that can access this . + /// public Optional> Roles { get; set; } } } diff --git a/src/Discord.Net.Core/Entities/Emotes/GuildEmote.cs b/src/Discord.Net.Core/Entities/Emotes/GuildEmote.cs index 95b062bd2..149a0f284 100644 --- a/src/Discord.Net.Core/Entities/Emotes/GuildEmote.cs +++ b/src/Discord.Net.Core/Entities/Emotes/GuildEmote.cs @@ -1,16 +1,25 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Diagnostics; namespace Discord { /// - /// An image-based emote that is attached to a guild + /// An image-based emote that is attached to a guild. /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class GuildEmote : Emote { + /// + /// Gets whether this emoji is managed. + /// public bool IsManaged { get; } + /// + /// Gets whether this emoji must be wrapped in colons. + /// public bool RequireColons { get; } + /// + /// Gets the roles this emoji is whitelisted to. + /// public IReadOnlyList RoleIds { get; } internal GuildEmote(ulong id, string name, bool animated, bool isManaged, bool requireColons, IReadOnlyList roleIds) : base(id, name, animated) @@ -21,6 +30,9 @@ namespace Discord } private string DebuggerDisplay => $"{Name} ({Id})"; + /// + /// Gets the raw representation of the emoji. + /// public override string ToString() => $"<{(Animated ? "a" : "")}:{Name}:{Id}>"; } } diff --git a/src/Discord.Net.Core/Entities/Emotes/IEmote.cs b/src/Discord.Net.Core/Entities/Emotes/IEmote.cs index fac61402a..e39601852 100644 --- a/src/Discord.Net.Core/Entities/Emotes/IEmote.cs +++ b/src/Discord.Net.Core/Entities/Emotes/IEmote.cs @@ -1,12 +1,12 @@ -namespace Discord +namespace Discord { /// - /// A general container for any type of emote in a message. + /// Represents a general container for any type of emote in a message. /// public interface IEmote { /// - /// The display name or unicode representation of this emote + /// Gets the display name or Unicode representation of this emote. /// string Name { get; } } diff --git a/src/Discord.Net.Core/Entities/Guilds/DefaultMessageNotifications.cs b/src/Discord.Net.Core/Entities/Guilds/DefaultMessageNotifications.cs index a5cabc117..ffcd28cee 100644 --- a/src/Discord.Net.Core/Entities/Guilds/DefaultMessageNotifications.cs +++ b/src/Discord.Net.Core/Entities/Guilds/DefaultMessageNotifications.cs @@ -1,10 +1,17 @@ -namespace Discord +namespace Discord { + /// + /// Specifies the default message notification behavior the guild uses. + /// public enum DefaultMessageNotifications { - /// By default, all messages will trigger notifications. + /// + /// By default, all messages will trigger notifications. + /// AllMessages = 0, - /// By default, only mentions will trigger notifications. + /// + /// By default, only mentions will trigger notifications. + /// MentionsOnly = 1 } } diff --git a/src/Discord.Net.Core/Entities/Guilds/GuildEmbedProperties.cs b/src/Discord.Net.Core/Entities/Guilds/GuildEmbedProperties.cs index a2b2ec4fc..68925b103 100644 --- a/src/Discord.Net.Core/Entities/Guilds/GuildEmbedProperties.cs +++ b/src/Discord.Net.Core/Entities/Guilds/GuildEmbedProperties.cs @@ -1,20 +1,20 @@ -namespace Discord +namespace Discord { /// - /// Modify the widget of an IGuild with the specified parameters + /// Properties that are used to modify the widget of an with the specified changes. /// public class GuildEmbedProperties { /// - /// Should the widget be enabled? + /// Sets whether the widget should be enabled. /// public Optional Enabled { get; set; } /// - /// What channel should the invite place users in, if not null. + /// Sets the channel that the invite should place its users in, if not . /// public Optional Channel { get; set; } /// - /// What channel should the invite place users in, if not null. + /// Sets the channel the invite should place its users in, if not . /// public Optional ChannelId { get; set; } } diff --git a/src/Discord.Net.Core/Entities/Guilds/GuildIntegrationProperties.cs b/src/Discord.Net.Core/Entities/Guilds/GuildIntegrationProperties.cs index f329e78e6..c8b5b3072 100644 --- a/src/Discord.Net.Core/Entities/Guilds/GuildIntegrationProperties.cs +++ b/src/Discord.Net.Core/Entities/Guilds/GuildIntegrationProperties.cs @@ -1,9 +1,21 @@ -namespace Discord +namespace Discord { + /// + /// Properties used to modify an with the specified changes. + /// public class GuildIntegrationProperties { + /// + /// Gets or sets the behavior when an integration subscription lapses. + /// public Optional ExpireBehavior { get; set; } + /// + /// Gets or sets the period (in seconds) where the integration will ignore lapsed subscriptions. + /// public Optional ExpireGracePeriod { get; set; } + /// + /// Gets or sets whether emoticons should be synced for this integration. + /// public Optional EnableEmoticons { get; set; } } } diff --git a/src/Discord.Net.Core/Entities/Guilds/GuildProperties.cs b/src/Discord.Net.Core/Entities/Guilds/GuildProperties.cs index 1b406ef7f..fc33f3fe4 100644 --- a/src/Discord.Net.Core/Entities/Guilds/GuildProperties.cs +++ b/src/Discord.Net.Core/Entities/Guilds/GuildProperties.cs @@ -1,78 +1,79 @@ -namespace Discord +namespace Discord { /// - /// Modify an IGuild with the specified changes + /// Properties that are used to modify an with the specified changes. /// /// - /// - /// await Context.Guild.ModifyAsync(async x => - /// { - /// x.Name = "aaaaaah"; - /// x.RegionId = (await Context.Client.GetOptimalVoiceRegionAsync()).Id; - /// }); + /// + /// await Context.Guild.ModifyAsync(async x => + /// { + /// x.Name = "aaaaaah"; + /// x.RegionId = (await Context.Client.GetOptimalVoiceRegionAsync()).Id; + /// }); /// /// - /// + /// public class GuildProperties { public Optional Username { get; set; } /// - /// The name of the Guild + /// Gets or sets the name of the Guild. /// public Optional Name { get; set; } /// - /// The region for the Guild's voice connections + /// Gets or sets the region for the Guild's voice connections. /// public Optional Region { get; set; } /// - /// The ID of the region for the Guild's voice connections + /// Gets or sets the ID of the region for the Guild's voice connections. /// public Optional RegionId { get; set; } /// - /// What verification level new users need to achieve before speaking + /// Gets or sets the verification level new users need to achieve before speaking. /// public Optional VerificationLevel { get; set; } /// - /// The default message notification state for the guild + /// Gets or sets the default message notification state for the guild. /// public Optional DefaultMessageNotifications { get; set; } /// - /// How many seconds before a user is sent to AFK. This value MUST be one of: (60, 300, 900, 1800, 3600). + /// Gets or sets how many seconds before a user is sent to AFK. This value MUST be one of: (60, 300, 900, + /// 1800, 3600). /// public Optional AfkTimeout { get; set; } /// - /// The icon of the guild + /// Gets or sets the icon of the guild. /// public Optional Icon { get; set; } /// - /// The guild's splash image + /// Gets or sets the guild's splash image. /// /// - /// The guild must be partnered for this value to have any effect. + /// The guild must be partnered for this value to have any effect. /// public Optional Splash { get; set; } /// - /// The IVoiceChannel where AFK users should be sent. + /// Gets or sets the where AFK users should be sent. /// public Optional AfkChannel { get; set; } /// - /// The ID of the IVoiceChannel where AFK users should be sent. + /// Gets or sets the ID of the where AFK users should be sent. /// public Optional AfkChannelId { get; set; } /// - /// The ITextChannel where System messages should be sent. + /// Gets or sets the where System messages should be sent. /// public Optional SystemChannel { get; set; } /// - /// The ID of the ITextChannel where System messages should be sent. + /// Gets or sets the ID of the where System messages should be sent. /// public Optional SystemChannelId { get; set; } /// - /// The owner of this guild. + /// Gets or sets the owner of this guild. /// public Optional Owner { get; set; } /// - /// The ID of the owner of this guild. + /// Gets or sets the ID of the owner of this guild. /// public Optional OwnerId { get; set; } } diff --git a/src/Discord.Net.Core/Entities/Guilds/IBan.cs b/src/Discord.Net.Core/Entities/Guilds/IBan.cs index 05ab0c00f..3ce76d29b 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IBan.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IBan.cs @@ -1,8 +1,17 @@ -namespace Discord +namespace Discord { + /// + /// Represents a generic ban object. + /// public interface IBan { + /// + /// Gets the banned user. + /// IUser User { get; } + /// + /// Gets the reason why the user is banned. + /// string Reason { get; } } } diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs index 2f0599d76..56c094621 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs @@ -1,138 +1,319 @@ -using Discord.Audio; +using Discord.Audio; using System; using System.Collections.Generic; using System.Threading.Tasks; namespace Discord { + /// + /// Represents a generic guild object. + /// public interface IGuild : IDeletable, ISnowflakeEntity { - /// Gets the name of this guild. + /// + /// Gets the name of this guild. + /// string Name { get; } - /// Gets the amount of time (in seconds) a user must be inactive in a voice channel for until they are automatically moved to the AFK voice channel, if one is set. + /// + /// Gets the amount of time (in seconds) a user must be inactive in a voice channel for until they are + /// automatically moved to the AFK voice channel, if one is set. + /// int AFKTimeout { get; } - /// Returns true if this guild is embeddable (e.g. widget) + /// + /// Returns if this guild is embeddable (e.g. widget). + /// bool IsEmbeddable { get; } - /// Gets the default message notifications for users who haven't explicitly set their notification settings. + /// + /// Gets the default message notifications for users who haven't explicitly set their notification settings. + /// DefaultMessageNotifications DefaultMessageNotifications { get; } - /// Gets the level of mfa requirements a user must fulfill before being allowed to perform administrative actions in this guild. + /// + /// Gets the level of Multi-Factor Authentication requirements a user must fulfill before being allowed to + /// perform administrative actions in this guild. + /// MfaLevel MfaLevel { get; } - /// Gets the level of requirements a user must fulfill before being allowed to post messages in this guild. + /// + /// Gets the level of requirements a user must fulfill before being allowed to post messages in this guild. + /// VerificationLevel VerificationLevel { get; } - /// Returns the id of this guild's icon, or null if one is not set. + /// + /// Returns the ID of this guild's icon, or if one is not set. + /// string IconId { get; } - /// Returns the url to this guild's icon, or null if one is not set. + /// + /// Returns the URL of this guild's icon, or if one is not set. + /// string IconUrl { get; } - /// Returns the id of this guild's splash image, or null if one is not set. + /// + /// Returns the ID of this guild's splash image, or if one is not set. + /// string SplashId { get; } - /// Returns the url to this guild's splash image, or null if one is not set. + /// + /// Returns the URL of this guild's splash image, or if one is not set. + /// string SplashUrl { get; } - /// Returns true if this guild is currently connected and ready to be used. Only applies to the WebSocket client. + /// + /// Returns if this guild is currently connected and ready to be used. Only applies + /// to the WebSocket client. + /// bool Available { get; } - /// Gets the id of the AFK voice channel for this guild if set, or null if not. + /// + /// Gets the ID of the AFK voice channel for this guild if set, or if not. + /// ulong? AFKChannelId { get; } - /// Gets the id of the the default channel for this guild. + /// + /// Gets the ID of the the default channel for this guild. + /// ulong DefaultChannelId { get; } - /// Gets the id of the embed channel for this guild if set, or null if not. + /// + /// Gets the ID of the embed channel for this guild if set, or if not. + /// ulong? EmbedChannelId { get; } - /// Gets the id of the channel where randomized welcome messages are sent, or null if not. + /// + /// Gets the ID of the channel where randomized welcome messages are sent if set, or if not. + /// ulong? SystemChannelId { get; } - /// Gets the id of the user that created this guild. + /// + /// Gets the ID of the user that created this guild. + /// ulong OwnerId { get; } - /// Gets the id of the region hosting this guild's voice channels. + /// + /// Gets the ID of the region hosting this guild's voice channels. + /// string VoiceRegionId { get; } - /// Gets the IAudioClient currently associated with this guild. + /// + /// Gets the currently associated with this guild. + /// IAudioClient AudioClient { get; } - /// Gets the built-in role containing all users in this guild. + /// + /// Gets the built-in role containing all users in this guild. + /// IRole EveryoneRole { get; } - /// Gets a collection of all custom emojis for this guild. + /// + /// Gets a collection of all custom emotes for this guild. + /// IReadOnlyCollection Emotes { get; } - /// Gets a collection of all extra features added to this guild. + /// + /// Gets a collection of all extra features added to this guild. + /// IReadOnlyCollection Features { get; } - /// Gets a collection of all roles in this guild. + /// + /// Gets a collection of all roles in this guild. + /// IReadOnlyCollection Roles { get; } - /// Modifies this guild. + /// + /// Modifies this guild. + /// Task ModifyAsync(Action func, RequestOptions options = null); - /// Modifies this guild's embed. + /// + /// Modifies this guild's embed channel. + /// Task ModifyEmbedAsync(Action func, RequestOptions options = null); - /// Bulk modifies the channels of this guild. + /// + /// Bulk modifies the order of channels in this guild. + /// Task ReorderChannelsAsync(IEnumerable args, RequestOptions options = null); - /// Bulk modifies the roles of this guild. + /// + /// Bulk modifies the order of roles in this guild. + /// Task ReorderRolesAsync(IEnumerable args, RequestOptions options = null); - /// Leaves this guild. If you are the owner, use Delete instead. + /// + /// Leaves this guild. If you are the owner, use + /// instead. + /// Task LeaveAsync(RequestOptions options = null); - /// Gets a collection of all users banned on this guild. + /// + /// Gets a collection of all users banned on this guild. + /// Task> GetBansAsync(RequestOptions options = null); - /// Bans the provided user from this guild and optionally prunes their recent messages. - /// The number of days to remove messages from this user for - must be between [0, 7] + /// + /// Bans the provided from this guild and optionally prunes their recent messages. + /// + /// + /// The user to ban. + /// + /// + /// The number of days to remove messages from this for - must be between [0, 7] + /// + /// + /// The reason of the ban to be written in the audit log. + /// Task AddBanAsync(IUser user, int pruneDays = 0, string reason = null, RequestOptions options = null); - /// Bans the provided user id from this guild and optionally prunes their recent messages. - /// The number of days to remove messages from this user for - must be between [0, 7] + /// + /// Bans the provided user ID from this guild and optionally prunes their recent messages. + /// + /// + /// The ID of the user to ban. + /// + /// + /// The number of days to remove messages from this user for - must be between [0, 7] + /// + /// + /// The reason of the ban to be written in the audit log. + /// Task AddBanAsync(ulong userId, int pruneDays = 0, string reason = null, RequestOptions options = null); - /// Unbans the provided user if it is currently banned. + /// + /// Unbans the provided if they are currently banned. + /// Task RemoveBanAsync(IUser user, RequestOptions options = null); - /// Unbans the provided user id if it is currently banned. + /// + /// Unbans the provided user ID if it is currently banned. + /// Task RemoveBanAsync(ulong userId, RequestOptions options = null); - /// Gets a collection of all channels in this guild. + /// + /// Gets a collection of all channels in this guild. + /// Task> GetChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); - /// Gets the channel in this guild with the provided id, or null if not found. + /// + /// Gets the channel in this guild with the provided ID, or if not found. + /// + /// The channel ID. Task GetChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// + /// Gets a collection of all text channels in this guild. + /// Task> GetTextChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// + /// Gets a text channel in this guild with the provided ID, or if not found. + /// + /// The text channel ID. Task GetTextChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// + /// Gets a collection of all voice channels in this guild. + /// Task> GetVoiceChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// + /// Gets a collection of all category channels in this guild. + /// Task> GetCategoriesAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// + /// Gets the voice channel in this guild with the provided ID, or if not found. + /// + /// The text channel ID. Task GetVoiceChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// + /// Gets the voice AFK channel in this guild with the provided ID, or if not found. + /// Task GetAFKChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// + /// Gets the default system text channel in this guild with the provided ID, or if + /// none is set. + /// Task GetSystemChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// + /// Gets the top viewable text channel in this guild with the provided ID, or if not + /// found. + /// Task GetDefaultChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// + /// Gets the embed channel in this guild. + /// Task GetEmbedChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); - /// Creates a new text channel. + /// + /// Creates a new text channel. + /// + /// The new name for the text channel. Task CreateTextChannelAsync(string name, RequestOptions options = null); - /// Creates a new voice channel. + /// + /// Creates a new voice channel. + /// + /// The new name for the voice channel. Task CreateVoiceChannelAsync(string name, RequestOptions options = null); - /// Creates a new channel category. + /// + /// Creates a new channel category. + /// + /// The new name for the category. Task CreateCategoryAsync(string name, RequestOptions options = null); Task> GetIntegrationsAsync(RequestOptions options = null); Task CreateIntegrationAsync(ulong id, string type, RequestOptions options = null); - /// Gets a collection of all invites to this guild. + /// + /// Gets a collection of all invites to this guild. + /// Task> GetInvitesAsync(RequestOptions options = null); - /// Gets the role in this guild with the provided id, or null if not found. + /// + /// Gets the role in this guild with the provided ID, or if not found. + /// + /// The role ID. IRole GetRole(ulong id); - /// Creates a new role. + /// + /// Creates a new role with the provided name. + /// + /// The new name for the role. + /// The guild permission that the role should possess. + /// The color of the role. + /// Whether the role is separated from others on the sidebar. Task CreateRoleAsync(string name, GuildPermissions? permissions = null, Color? color = null, bool isHoisted = false, RequestOptions options = null); - /// Gets a collection of all users in this guild. + /// + /// Gets a collection of all users in this guild. + /// Task> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); //TODO: shouldnt this be paged? - /// Gets the user in this guild with the provided id, or null if not found. + /// + /// Gets the user in this guild with the provided ID, or if not found. + /// + /// The user ID. Task GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); - /// Gets the current user for this guild. + /// + /// Gets the current user for this guild. + /// Task GetCurrentUserAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); - /// Gets the owner of this guild. + /// + /// Gets the owner of this guild. + /// Task GetOwnerAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); - /// Downloads all users for this guild if the current list is incomplete. + /// + /// Downloads all users for this guild if the current list is incomplete. + /// Task DownloadUsersAsync(); - /// Removes all users from this guild if they have not logged on in a provided number of days or, if simulate is true, returns the number of users that would be removed. + /// + /// Removes all users from this guild if they have not logged on in a provided number of + /// or, if is true, returns the number of users that + /// would be removed. + /// + /// The number of days required for the users to be kicked. + /// Whether this prune action is a simulation. + /// + /// The number of users removed from this guild. + /// Task PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null); - /// Gets the webhook in this guild with the provided id, or null if not found. + /// + /// Gets the webhook in this guild with the provided ID, or if not found. + /// + /// The webhook ID. Task GetWebhookAsync(ulong id, RequestOptions options = null); - /// Gets a collection of all webhooks for this guild. + /// + /// Gets a collection of all webhooks from this guild. + /// Task> GetWebhooksAsync(RequestOptions options = null); - /// Gets a specific emote from this guild. + /// + /// Gets a specific emote from this guild. + /// + /// The guild emote ID. Task GetEmoteAsync(ulong id, RequestOptions options = null); - /// Creates a new emote in this guild. + /// + /// Creates a new emote in this guild. + /// + /// The name of the guild emote. + /// The image of the new emote. + /// The roles to limit the emote usage to. Task CreateEmoteAsync(string name, Image image, Optional> roles = default(Optional>), RequestOptions options = null); - /// Modifies an existing emote in this guild. + /// + /// Modifies an existing in this guild. + /// Task ModifyEmoteAsync(GuildEmote emote, Action func, RequestOptions options = null); - /// Deletes an existing emote from this guild. + /// + /// Deletes an existing from this guild. + /// + /// The guild emote to delete. Task DeleteEmoteAsync(GuildEmote emote, RequestOptions options = null); } -} \ No newline at end of file +} diff --git a/src/Discord.Net.Core/Entities/Guilds/IUserGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IUserGuild.cs index b27db9377..5da2ce5da 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IUserGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IUserGuild.cs @@ -1,14 +1,22 @@ -namespace Discord +namespace Discord { public interface IUserGuild : IDeletable, ISnowflakeEntity { - /// Gets the name of this guild. + /// + /// Gets the name of this guild. + /// string Name { get; } - /// Returns the url to this guild's icon, or null if one is not set. + /// + /// Gets the icon URL associated with this guild, or if one is not set. + /// string IconUrl { get; } - /// Returns true if the current user owns this guild. + /// + /// Returns if the current user owns this guild. + /// bool IsOwner { get; } - /// Returns the current user's permissions for this guild. + /// + /// Returns the current user's permissions for this guild. + /// GuildPermissions Permissions { get; } } } diff --git a/src/Discord.Net.Core/Entities/Guilds/IVoiceRegion.cs b/src/Discord.Net.Core/Entities/Guilds/IVoiceRegion.cs index 1a76287d8..fd83ae3ec 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IVoiceRegion.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IVoiceRegion.cs @@ -1,18 +1,25 @@ -namespace Discord +namespace Discord { + /// + /// Represents a region of which the user connects to when using voice. + /// public interface IVoiceRegion { - /// Gets the unique identifier for this voice region. + /// + /// Gets the unique identifier for this voice region. + /// string Id { get; } - /// Gets the name of this voice region. + /// + /// Gets the name of this voice region. + /// string Name { get; } - /// Returns true if this voice region is exclusive to VIP accounts. + /// + /// Returns if this voice region is exclusive to VIP accounts. + /// bool IsVip { get; } - /// Returns true if this voice region is the closest to your machine. + /// + /// Returns if this voice region is the closest to your machine. + /// bool IsOptimal { get; } - /// Gets an example hostname for this voice region. - string SampleHostname { get; } - /// Gets an example port for this voice region. - int SamplePort { get; } } -} \ No newline at end of file +} diff --git a/src/Discord.Net.Core/Entities/Guilds/MfaLevel.cs b/src/Discord.Net.Core/Entities/Guilds/MfaLevel.cs index 1dfef17d5..57edac2b0 100644 --- a/src/Discord.Net.Core/Entities/Guilds/MfaLevel.cs +++ b/src/Discord.Net.Core/Entities/Guilds/MfaLevel.cs @@ -1,10 +1,17 @@ -namespace Discord +namespace Discord { + /// + /// Specifies the guild's Multi-Factor Authentication (MFA) level requirement. + /// public enum MfaLevel { - /// Users have no additional MFA restriction on this guild. + /// + /// Users have no additional MFA restriction on this guild. + /// Disabled = 0, - /// Users must have MFA enabled on their account to perform administrative actions. + /// + /// Users must have MFA enabled on their account to perform administrative actions. + /// Enabled = 1 } } diff --git a/src/Discord.Net.Core/Entities/Guilds/PermissionTarget.cs b/src/Discord.Net.Core/Entities/Guilds/PermissionTarget.cs index 96595fb69..3da2fb147 100644 --- a/src/Discord.Net.Core/Entities/Guilds/PermissionTarget.cs +++ b/src/Discord.Net.Core/Entities/Guilds/PermissionTarget.cs @@ -1,8 +1,17 @@ -namespace Discord +namespace Discord { + /// + /// Specifies the target of the permission. + /// public enum PermissionTarget { + /// + /// The target of the permission is a role. + /// Role, + /// + /// The target of the permission is a user. + /// User } } diff --git a/src/Discord.Net.Core/Entities/Guilds/VerificationLevel.cs b/src/Discord.Net.Core/Entities/Guilds/VerificationLevel.cs index ac51fe927..3a5ae0468 100644 --- a/src/Discord.Net.Core/Entities/Guilds/VerificationLevel.cs +++ b/src/Discord.Net.Core/Entities/Guilds/VerificationLevel.cs @@ -1,16 +1,29 @@ -namespace Discord +namespace Discord { + /// + /// Specifies the verification level the guild uses. + /// public enum VerificationLevel { - /// Users have no additional restrictions on sending messages to this guild. + /// + /// Users have no additional restrictions on sending messages to this guild. + /// None = 0, - /// Users must have a verified email on their account. + /// + /// Users must have a verified email on their account. + /// Low = 1, - /// Users must fulfill the requirements of Low, and be registered on Discord for at least 5 minutes. + /// + /// Users must fulfill the requirements of Low and be registered on Discord for at least 5 minutes. + /// Medium = 2, - /// Users must fulfill the requirements of Medium, and be a member of this guild for at least 10 minutes. + /// + /// Users must fulfill the requirements of Medium and be a member of this guild for at least 10 minutes. + /// High = 3, - /// Users must fulfill the requirements of High, and must have a verified phone on their Discord account. + /// + /// Users must fulfill the requirements of High and must have a verified phone on their Discord account. + /// Extreme = 4 } } diff --git a/src/Discord.Net.Core/Entities/IApplication.cs b/src/Discord.Net.Core/Entities/IApplication.cs index 4fb1e4b91..eb9fddd89 100644 --- a/src/Discord.Net.Core/Entities/IApplication.cs +++ b/src/Discord.Net.Core/Entities/IApplication.cs @@ -1,13 +1,31 @@ -namespace Discord +namespace Discord { + /// + /// Represents a Discord application created via the developer portal. + /// public interface IApplication : ISnowflakeEntity { + /// + /// Gets the name of the application. + /// string Name { get; } + /// + /// Gets the description of the application. + /// string Description { get; } + /// + /// Gets the RPC origins of the application. + /// string[] RPCOrigins { get; } ulong Flags { get; } + /// + /// Gets the icon URL of the application. + /// string IconUrl { get; } + /// + /// Gets the partial user object containing info on the owner of the application. + /// IUser Owner { get; } } } diff --git a/src/Discord.Net.Core/Entities/IDeletable.cs b/src/Discord.Net.Core/Entities/IDeletable.cs index ba22a537a..ce019edcf 100644 --- a/src/Discord.Net.Core/Entities/IDeletable.cs +++ b/src/Discord.Net.Core/Entities/IDeletable.cs @@ -1,10 +1,15 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; namespace Discord { + /// + /// Represents whether the object is deletable or not. + /// public interface IDeletable { - /// Deletes this object and all its children. + /// + /// Deletes this object and all its children. + /// Task DeleteAsync(RequestOptions options = null); } } diff --git a/src/Discord.Net.Core/Entities/IEntity.cs b/src/Discord.Net.Core/Entities/IEntity.cs index 711fd0555..0cd692a41 100644 --- a/src/Discord.Net.Core/Entities/IEntity.cs +++ b/src/Discord.Net.Core/Entities/IEntity.cs @@ -8,7 +8,9 @@ namespace Discord ///// Gets the IDiscordClient that created this object. //IDiscordClient Discord { get; } - /// Gets the unique identifier for this object. + /// + /// Gets the unique identifier for this object. + /// TId Id { get; } } diff --git a/src/Discord.Net.Core/Entities/IMentionable.cs b/src/Discord.Net.Core/Entities/IMentionable.cs index abccc4480..1fd9400b3 100644 --- a/src/Discord.Net.Core/Entities/IMentionable.cs +++ b/src/Discord.Net.Core/Entities/IMentionable.cs @@ -1,8 +1,13 @@ -namespace Discord +namespace Discord { + /// + /// Represents whether the object is mentionable or not. + /// public interface IMentionable { - /// Returns a special string used to mention this object. + /// + /// Returns a special string used to mention this object. + /// string Mention { get; } } } diff --git a/src/Discord.Net.Core/Entities/ISnowflakeEntity.cs b/src/Discord.Net.Core/Entities/ISnowflakeEntity.cs index 5b099b5ac..f5dd2ab07 100644 --- a/src/Discord.Net.Core/Entities/ISnowflakeEntity.cs +++ b/src/Discord.Net.Core/Entities/ISnowflakeEntity.cs @@ -2,8 +2,10 @@ using System; namespace Discord { + /// Represents a Discord snowflake entity. public interface ISnowflakeEntity : IEntity { + /// Gets when the snowflake is created. DateTimeOffset CreatedAt { get; } } } diff --git a/src/Discord.Net.Core/Entities/IUpdateable.cs b/src/Discord.Net.Core/Entities/IUpdateable.cs index b0f51aee7..b4bbe169a 100644 --- a/src/Discord.Net.Core/Entities/IUpdateable.cs +++ b/src/Discord.Net.Core/Entities/IUpdateable.cs @@ -1,10 +1,15 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; namespace Discord { + /// + /// Represents whether the object is updatable or not. + /// public interface IUpdateable { - /// Updates this object's properties with its current state. + /// + /// Updates this object's properties with its current state. + /// Task UpdateAsync(RequestOptions options = null); } } diff --git a/src/Discord.Net.Core/Entities/Image.cs b/src/Discord.Net.Core/Entities/Image.cs index c2c997365..4bdd4be66 100644 --- a/src/Discord.Net.Core/Entities/Image.cs +++ b/src/Discord.Net.Core/Entities/Image.cs @@ -1,26 +1,33 @@ -using System.IO; +using System.IO; namespace Discord { /// - /// An image that will be uploaded to Discord. + /// An image that will be uploaded to Discord. /// public struct Image { + /// + /// Gets the stream to be uploaded to Discord. + /// public Stream Stream { get; } /// - /// Create the image with a Stream. + /// Create the image with a . /// - /// This must be some type of stream with the contents of a file in it. + /// + /// The to create the image with. Note that this must be some type of stream + /// with the contents of a file in it. + /// public Image(Stream stream) { Stream = stream; } #if FILESYSTEM /// - /// Create the image from a file path. + /// Create the image from a file path. /// /// - /// This file path is NOT validated, and is passed directly into a + /// This file is NOT validated, and is passed directly into a + /// /// /// The path to the file. public Image(string path) diff --git a/src/Discord.Net.Core/Entities/ImageFormat.cs b/src/Discord.Net.Core/Entities/ImageFormat.cs index 302da79c8..9c04328f4 100644 --- a/src/Discord.Net.Core/Entities/ImageFormat.cs +++ b/src/Discord.Net.Core/Entities/ImageFormat.cs @@ -1,11 +1,29 @@ -namespace Discord +namespace Discord { + /// + /// Specifies the type of format the image should return in. + /// public enum ImageFormat { + /// + /// Use automatically detected format. + /// Auto, + /// + /// Use Google's WebP image format. + /// WebP, + /// + /// Use PNG. + /// Png, + /// + /// Use JPEG. + /// Jpeg, + /// + /// Use GIF. + /// Gif, } } diff --git a/src/Discord.Net.Core/Entities/Invites/IInvite.cs b/src/Discord.Net.Core/Entities/Invites/IInvite.cs index 73555e453..1be65face 100644 --- a/src/Discord.Net.Core/Entities/Invites/IInvite.cs +++ b/src/Discord.Net.Core/Entities/Invites/IInvite.cs @@ -1,29 +1,50 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; namespace Discord { + /// + /// Represents a generic invite object. + /// public interface IInvite : IEntity, IDeletable { - /// Gets the unique identifier for this invite. + /// + /// Gets the unique identifier for this invite. + /// string Code { get; } - /// Gets the url used to accept this invite, using Code. + /// + /// Gets the URL used to accept this invite, using Code. + /// string Url { get; } - /// Gets the channel this invite is linked to. + /// + /// Gets the channel this invite is linked to. + /// IChannel Channel { get; } - /// Gets the id of the channel this invite is linked to. + /// + /// Gets the ID of the channel this invite is linked to. + /// ulong ChannelId { get; } - /// Gets the name of the channel this invite is linked to. + /// + /// Gets the name of the channel this invite is linked to. + /// string ChannelName { get; } - /// Gets the guild this invite is linked to. + /// + /// Gets the guild this invite is linked to. + /// IGuild Guild { get; } - /// Gets the id of the guild this invite is linked to. + /// + /// Gets the ID of the guild this invite is linked to. + /// ulong GuildId { get; } - /// Gets the name of the guild this invite is linked to. + /// + /// Gets the name of the guild this invite is linked to. + /// string GuildName { get; } - /// Accepts this invite and joins the target guild. This will fail on bot accounts. + /// + /// Accepts this invite and joins the target guild. This will fail on bot accounts. + /// Task AcceptAsync(RequestOptions options = null); } } diff --git a/src/Discord.Net.Core/Entities/Invites/IInviteMetadata.cs b/src/Discord.Net.Core/Entities/Invites/IInviteMetadata.cs index 1136e1678..dcd3de997 100644 --- a/src/Discord.Net.Core/Entities/Invites/IInviteMetadata.cs +++ b/src/Discord.Net.Core/Entities/Invites/IInviteMetadata.cs @@ -1,22 +1,38 @@ -using System; +using System; namespace Discord { + /// Represents additional information regarding the generic invite object. public interface IInviteMetadata : IInvite { - /// Gets the user that created this invite. + /// + /// Gets the user that created this invite. + /// IUser Inviter { get; } - /// Returns true if this invite was revoked. + /// + /// Returns if this invite was revoked. + /// bool IsRevoked { get; } - /// Returns true if users accepting this invite will be removed from the guild when they log off. + /// + /// Returns if users accepting this invite will be removed from the guild when they + /// log off. + /// bool IsTemporary { get; } - /// Gets the time (in seconds) until the invite expires, or null if it never expires. + /// + /// Gets the time (in seconds) until the invite expires, or if it never expires. + /// int? MaxAge { get; } - /// Gets the max amount of times this invite may be used, or null if there is no limit. + /// + /// Gets the max amount of times this invite may be used, or if there is no limit. + /// int? MaxUses { get; } - /// Gets the amount of times this invite has been used. + /// + /// Gets the amount of times this invite has been used. + /// int Uses { get; } - /// Gets when this invite was created. + /// + /// Gets when this invite was created. + /// DateTimeOffset CreatedAt { get; } } -} \ No newline at end of file +} diff --git a/src/Discord.Net.Core/Entities/Messages/Embed.cs b/src/Discord.Net.Core/Entities/Messages/Embed.cs index 5fae7acde..1f2846f2e 100644 --- a/src/Discord.Net.Core/Entities/Messages/Embed.cs +++ b/src/Discord.Net.Core/Entities/Messages/Embed.cs @@ -1,26 +1,42 @@ -using System; +using System; using System.Collections.Immutable; using System.Diagnostics; using System.Linq; namespace Discord { + /// + /// Represents an embed object seen in an . + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class Embed : IEmbed { + /// public EmbedType Type { get; } + /// public string Description { get; internal set; } + /// public string Url { get; internal set; } + /// public string Title { get; internal set; } + /// public DateTimeOffset? Timestamp { get; internal set; } + /// public Color? Color { get; internal set; } + /// public EmbedImage? Image { get; internal set; } + /// public EmbedVideo? Video { get; internal set; } + /// public EmbedAuthor? Author { get; internal set; } + /// public EmbedFooter? Footer { get; internal set; } + /// public EmbedProvider? Provider { get; internal set; } + /// public EmbedThumbnail? Thumbnail { get; internal set; } + /// public ImmutableArray Fields { get; internal set; } internal Embed(EmbedType type) @@ -57,8 +73,14 @@ namespace Discord Fields = fields; } + /// + /// Gets the total length of all embed properties. + /// public int Length => Title?.Length + Author?.Name?.Length + Description?.Length + Footer?.Text?.Length + Fields.Sum(f => f.Name.Length + f.Value.ToString().Length) ?? 0; + /// + /// Gets the title of the embed. + /// public override string ToString() => Title; private string DebuggerDisplay => $"{Title} ({Type})"; } diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedAuthor.cs b/src/Discord.Net.Core/Entities/Messages/EmbedAuthor.cs index c59473704..ab1360ce3 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedAuthor.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedAuthor.cs @@ -1,14 +1,28 @@ -using System; using System.Diagnostics; namespace Discord { + /// + /// Represents a author field of an . + /// [DebuggerDisplay("{DebuggerDisplay,nq}")] public struct EmbedAuthor { + /// + /// Gets the name of the author field. + /// public string Name { get; internal set; } + /// + /// Gets the URL of the author field. + /// public string Url { get; internal set; } + /// + /// Gets the icon URL of the author field. + /// public string IconUrl { get; internal set; } + /// + /// Gets the proxified icon URL of the author field. + /// public string ProxyIconUrl { get; internal set; } internal EmbedAuthor(string name, string url, string iconUrl, string proxyIconUrl) @@ -20,6 +34,12 @@ namespace Discord } private string DebuggerDisplay => $"{Name} ({Url})"; + /// + /// Gets the name of the author field. + /// + /// + /// + /// public override string ToString() => Name; } } diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs b/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs new file mode 100644 index 000000000..fa7d87410 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs @@ -0,0 +1,514 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; + +namespace Discord +{ + /// + /// Represents a builder class for creating a . + /// + public class EmbedBuilder + { + private string _title; + private string _description; + private string _url; + private EmbedImage? _image; + private EmbedThumbnail? _thumbnail; + private List _fields; + + /// + /// Returns the maximum number of fields allowed by Discord. + /// + public const int MaxFieldCount = 25; + /// + /// Returns the maximum length of title allowed by Discord. + /// + public const int MaxTitleLength = 256; + /// + /// Returns the maximum length of description allowed by Discord. + /// + public const int MaxDescriptionLength = 2048; + /// + /// Returns the maximum length of total characters allowed by Discord. + /// + public const int MaxEmbedLength = 6000; + + /// Initializes a new class. + public EmbedBuilder() + { + Fields = new List(); + } + + /// Gets or sets the title of an . + public string Title + { + get => _title; + set + { + if (value?.Length > MaxTitleLength) throw new ArgumentException($"Title length must be less than or equal to {MaxTitleLength}.", nameof(Title)); + _title = value; + } + } + /// Gets or sets the description of an . + public string Description + { + get => _description; + set + { + if (value?.Length > MaxDescriptionLength) throw new ArgumentException($"Description length must be less than or equal to {MaxDescriptionLength}.", nameof(Description)); + _description = value; + } + } + + /// Gets or sets the URL of an . + public string Url + { + get => _url; + set + { + if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI.", nameof(Url)); + _url = value; + } + } + /// Gets or sets the thumbnail URL of an . + public string ThumbnailUrl + { + get => _thumbnail?.Url; + set + { + if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI.", nameof(ThumbnailUrl)); + _thumbnail = new EmbedThumbnail(value, null, null, null); + } + } + /// Gets or sets the image URL of an . + public string ImageUrl + { + get => _image?.Url; + set + { + if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI.", nameof(ImageUrl)); + _image = new EmbedImage(value, null, null, null); + } + } + /// Gets or sets the list of of an . + public List Fields + { + get => _fields; + set + { + if (value == null) throw new ArgumentNullException(nameof(Fields), "Cannot set an embed builder's fields collection to null."); + if (value.Count > MaxFieldCount) throw new ArgumentException($"Field count must be less than or equal to {MaxFieldCount}.", nameof(Fields)); + _fields = value; + } + } + + /// Gets or sets the timestamp of an . + public DateTimeOffset? Timestamp { get; set; } + /// Gets or sets the sidebar color of an . + public Color? Color { get; set; } + /// Gets or sets the of an . + public EmbedAuthorBuilder Author { get; set; } + /// Gets or sets the of an . + public EmbedFooterBuilder Footer { get; set; } + + /// + /// Gets the total length of all embed properties. + /// + public int Length + { + get + { + int titleLength = Title?.Length ?? 0; + int authorLength = Author?.Name?.Length ?? 0; + int descriptionLength = Description?.Length ?? 0; + int footerLength = Footer?.Text?.Length ?? 0; + int fieldSum = Fields.Sum(f => f.Name.Length + f.Value.ToString().Length); + + return titleLength + authorLength + descriptionLength + footerLength + fieldSum; + } + } + + /// + /// Sets the title of an . + /// + /// The title to be set. + public EmbedBuilder WithTitle(string title) + { + Title = title; + return this; + } + /// + /// Sets the description of an . + /// + /// The description to be set. + public EmbedBuilder WithDescription(string description) + { + Description = description; + return this; + } + /// + /// Sets the URL of an . + /// + /// The URL to be set. + public EmbedBuilder WithUrl(string url) + { + Url = url; + return this; + } + /// + /// Sets the thumbnail URL of an . + /// + /// The thumbnail URL to be set. + public EmbedBuilder WithThumbnailUrl(string thumbnailUrl) + { + ThumbnailUrl = thumbnailUrl; + return this; + } + /// + /// Sets the image URL of an . + /// + /// The image URL to be set. + public EmbedBuilder WithImageUrl(string imageUrl) + { + ImageUrl = imageUrl; + return this; + } + /// + /// Sets the timestamp of an to the current time. + /// + public EmbedBuilder WithCurrentTimestamp() + { + Timestamp = DateTimeOffset.UtcNow; + return this; + } + /// + /// Sets the timestamp of an . + /// + /// The timestamp to be set. + public EmbedBuilder WithTimestamp(DateTimeOffset dateTimeOffset) + { + Timestamp = dateTimeOffset; + return this; + } + /// + /// Sets the sidebar color of an . + /// + /// The color to be set. + public EmbedBuilder WithColor(Color color) + { + Color = color; + return this; + } + + /// + /// Sets the of an . + /// + /// The author builder class containing the author field properties. + public EmbedBuilder WithAuthor(EmbedAuthorBuilder author) + { + Author = author; + return this; + } + /// + /// Sets the author field of an with the provided properties. + /// + /// The containing the author field properties. + public EmbedBuilder WithAuthor(Action action) + { + var author = new EmbedAuthorBuilder(); + action(author); + Author = author; + return this; + } + /// + /// Sets the author field of an with the provided name, icon URL, and URL. + /// + /// The title of the author field. + /// The icon URL of the author field. + /// The URL of the author field. + public EmbedBuilder WithAuthor(string name, string iconUrl = null, string url = null) + { + var author = new EmbedAuthorBuilder + { + Name = name, + IconUrl = iconUrl, + Url = url + }; + Author = author; + return this; + } + /// + /// Sets the of an . + /// + /// The footer builder class containing the footer field properties. + public EmbedBuilder WithFooter(EmbedFooterBuilder footer) + { + Footer = footer; + return this; + } + /// + /// Sets the footer field of an with the provided properties. + /// + /// The containing the footer field properties. + public EmbedBuilder WithFooter(Action action) + { + var footer = new EmbedFooterBuilder(); + action(footer); + Footer = footer; + return this; + } + /// + /// Sets the footer field of an with the provided name, icon URL. + /// + /// The title of the footer field. + /// The icon URL of the footer field. + public EmbedBuilder WithFooter(string text, string iconUrl = null) + { + var footer = new EmbedFooterBuilder + { + Text = text, + IconUrl = iconUrl + }; + Footer = footer; + return this; + } + + /// + /// Adds an field with the provided name and value. + /// + /// The title of the field. + /// The value of the field. + /// Indicates whether the field is in-line or not. + public EmbedBuilder AddField(string name, object value, bool inline = false) + { + var field = new EmbedFieldBuilder() + .WithIsInline(inline) + .WithName(name) + .WithValue(value); + AddField(field); + return this; + } + /// + /// Adds a field with the provided to an + /// . + /// + /// The field builder class containing the field properties. + public EmbedBuilder AddField(EmbedFieldBuilder field) + { + if (Fields.Count >= MaxFieldCount) + { + throw new ArgumentException($"Field count must be less than or equal to {MaxFieldCount}.", nameof(field)); + } + + Fields.Add(field); + return this; + } + /// + /// Adds an field with the provided properties. + /// + /// The containing the field properties. + public EmbedBuilder AddField(Action action) + { + var field = new EmbedFieldBuilder(); + action(field); + AddField(field); + return this; + } + + /// + /// Builds the into a Rich Embed format. + /// + /// + /// The built embed object. + /// + public Embed Build() + { + if (Length > MaxEmbedLength) + throw new InvalidOperationException($"Total embed length must be less than or equal to {MaxEmbedLength}."); + + var fields = ImmutableArray.CreateBuilder(Fields.Count); + for (int i = 0; i < Fields.Count; i++) + fields.Add(Fields[i].Build()); + + return new Embed(EmbedType.Rich, Title, Description, Url, Timestamp, Color, _image, null, Author?.Build(), Footer?.Build(), null, _thumbnail, fields.ToImmutable()); + } + } + + public class EmbedFieldBuilder + { + private string _name; + private string _value; + private EmbedField _field; + /// + /// Gets the maximum field length for name allowed by Discord. + /// + public const int MaxFieldNameLength = 256; + /// + /// Gets the maximum field length for value allowed by Discord. + /// + public const int MaxFieldValueLength = 1024; + + /// + /// Gets or sets the field name. + /// + public string Name + { + get => _name; + set + { + if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException("Field name must not be null, empty or entirely whitespace.", nameof(Name)); + if (value.Length > MaxFieldNameLength) throw new ArgumentException($"Field name length must be less than or equal to {MaxFieldNameLength}.", nameof(Name)); + _name = value; + } + } + + /// + /// Gets or sets the field value. + /// + public object Value + { + get => _value; + set + { + var stringValue = value?.ToString(); + if (string.IsNullOrEmpty(stringValue)) throw new ArgumentException($"Field value must not be null or empty.", nameof(Value)); + if (stringValue.Length > MaxFieldValueLength) throw new ArgumentException($"Field value length must be less than or equal to {MaxFieldValueLength}.", nameof(Value)); + _value = stringValue; + } + } + /// + /// Gets or sets whether the field should be in-line with each other. + /// + public bool IsInline { get; set; } + + /// + /// Sets the field name. + /// + /// The name to set the field name to. + public EmbedFieldBuilder WithName(string name) + { + Name = name; + return this; + } + /// + /// Sets the field value. + /// + /// The value to set the field value to. + public EmbedFieldBuilder WithValue(object value) + { + Value = value; + return this; + } + /// + /// Sets whether the field should be in-line with each other. + /// + public EmbedFieldBuilder WithIsInline(bool isInline) + { + IsInline = isInline; + return this; + } + + /// + /// Builds the field builder into a class. + /// + public EmbedField Build() + => new EmbedField(Name, Value.ToString(), IsInline); + } + + public class EmbedAuthorBuilder + { + private string _name; + private string _url; + private string _iconUrl; + public const int MaxAuthorNameLength = 256; + + public string Name + { + get => _name; + set + { + if (value?.Length > MaxAuthorNameLength) throw new ArgumentException($"Author name length must be less than or equal to {MaxAuthorNameLength}.", nameof(Name)); + _name = value; + } + } + public string Url + { + get => _url; + set + { + if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI.", nameof(Url)); + _url = value; + } + } + public string IconUrl + { + get => _iconUrl; + set + { + if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI.", nameof(IconUrl)); + _iconUrl = value; + } + } + + public EmbedAuthorBuilder WithName(string name) + { + Name = name; + return this; + } + public EmbedAuthorBuilder WithUrl(string url) + { + Url = url; + return this; + } + public EmbedAuthorBuilder WithIconUrl(string iconUrl) + { + IconUrl = iconUrl; + return this; + } + + public EmbedAuthor Build() + => new EmbedAuthor(Name, Url, IconUrl, null); + } + + public class EmbedFooterBuilder + { + private string _text; + private string _iconUrl; + + public const int MaxFooterTextLength = 2048; + + public string Text + { + get => _text; + set + { + if (value?.Length > MaxFooterTextLength) throw new ArgumentException($"Footer text length must be less than or equal to {MaxFooterTextLength}.", nameof(Text)); + _text = value; + } + } + public string IconUrl + { + get => _iconUrl; + set + { + if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI.", nameof(IconUrl)); + _iconUrl = value; + } + } + + public EmbedFooterBuilder WithText(string text) + { + Text = text; + return this; + } + public EmbedFooterBuilder WithIconUrl(string iconUrl) + { + IconUrl = iconUrl; + return this; + } + + public EmbedFooter Build() + => new EmbedFooter(Text, IconUrl, null); + } +} diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedField.cs b/src/Discord.Net.Core/Entities/Messages/EmbedField.cs index f7c1f8348..40404167d 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedField.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedField.cs @@ -1,12 +1,24 @@ -using System.Diagnostics; +using System.Diagnostics; namespace Discord { + /// + /// Represents a field for an . + /// [DebuggerDisplay("{DebuggerDisplay,nq}")] public struct EmbedField { + /// + /// Gets the name of the field. + /// public string Name { get; internal set; } + /// + /// Gets the value of the field. + /// public string Value { get; internal set; } + /// + /// Gets whether the field should be in-line with each other. + /// public bool Inline { get; internal set; } internal EmbedField(string name, string value, bool inline) @@ -17,6 +29,9 @@ namespace Discord } private string DebuggerDisplay => $"{Name} ({Value}"; + /// + /// Gets the name of the field. + /// public override string ToString() => Name; } } diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedFooter.cs b/src/Discord.Net.Core/Entities/Messages/EmbedFooter.cs index 29d85cd90..cd3839ac6 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedFooter.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedFooter.cs @@ -1,13 +1,16 @@ -using System; using System.Diagnostics; namespace Discord { + /// A footer field for an . [DebuggerDisplay("{DebuggerDisplay,nq}")] public struct EmbedFooter { + /// Gets the text of the footer. public string Text { get; internal set; } + /// Gets the icon URL of the footer. public string IconUrl { get; internal set; } + /// Gets the proxified icon URL of the footer. public string ProxyUrl { get; internal set; } internal EmbedFooter(string text, string iconUrl, string proxyUrl) diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedImage.cs b/src/Discord.Net.Core/Entities/Messages/EmbedImage.cs index f21d42c0c..b71e77721 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedImage.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedImage.cs @@ -1,14 +1,18 @@ -using System; using System.Diagnostics; namespace Discord { + /// An image for an . [DebuggerDisplay("{DebuggerDisplay,nq}")] public struct EmbedImage { + /// Gets the URL of the image. public string Url { get; } + /// Gets the proxified URL of the image. public string ProxyUrl { get; } + /// Gets the height of the image if any is set. public int? Height { get; } + /// Gets the width of the image if any is set. public int? Width { get; } internal EmbedImage(string url, string proxyUrl, int? height, int? width) diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedProvider.cs b/src/Discord.Net.Core/Entities/Messages/EmbedProvider.cs index 24722b158..365f6b85f 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedProvider.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedProvider.cs @@ -1,12 +1,14 @@ -using System; using System.Diagnostics; namespace Discord { + /// A provider field for an . [DebuggerDisplay("{DebuggerDisplay,nq}")] public struct EmbedProvider { + /// Gets the name of the provider. public string Name { get; } + /// Gets the URL of the provider. public string Url { get; } internal EmbedProvider(string name, string url) diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedThumbnail.cs b/src/Discord.Net.Core/Entities/Messages/EmbedThumbnail.cs index 209a93e37..3d00cfb2e 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedThumbnail.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedThumbnail.cs @@ -1,14 +1,18 @@ -using System; using System.Diagnostics; namespace Discord { + /// A thumbnail featured in an . [DebuggerDisplay("{DebuggerDisplay,nq}")] public struct EmbedThumbnail { + /// Gets the URL of the thumbnail. public string Url { get; } + /// Gets the proxified URL of the thumbnail. public string ProxyUrl { get; } + /// Gets the height of the thumbnail if any is set. public int? Height { get; } + /// Gets the width of the thumbnail if any is set. public int? Width { get; } internal EmbedThumbnail(string url, string proxyUrl, int? height, int? width) diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedType.cs b/src/Discord.Net.Core/Entities/Messages/EmbedType.cs index 5bb2653e2..978f45bc2 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedType.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedType.cs @@ -1,15 +1,45 @@ namespace Discord { + /// + /// Specifies the type of embed. + /// public enum EmbedType { + /// + /// An unknown embed type. + /// Unknown = -1, + /// + /// A rich embed type. + /// Rich, + /// + /// A link embed type. + /// Link, + /// + /// A video embed type. + /// Video, + /// + /// An image embed type. + /// Image, + /// + /// A GIFV embed type. + /// Gifv, + /// + /// An article embed type. + /// Article, + /// + /// A tweet embed type. + /// Tweet, + /// + /// A HTML embed type. + /// Html, } } diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedVideo.cs b/src/Discord.Net.Core/Entities/Messages/EmbedVideo.cs index f00681d89..525c25562 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedVideo.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedVideo.cs @@ -1,13 +1,24 @@ -using System; using System.Diagnostics; namespace Discord { + /// + /// A video featured in an . + /// [DebuggerDisplay("{DebuggerDisplay,nq}")] public struct EmbedVideo { + /// + /// Gets the URL of the video. + /// public string Url { get; } + /// + /// Gets the height of the video, or if none. + /// public int? Height { get; } + /// + /// Gets the weight of the video, or if none. + /// public int? Width { get; } internal EmbedVideo(string url, int? height, int? width) @@ -18,6 +29,9 @@ namespace Discord } private string DebuggerDisplay => $"{Url} ({(Width != null && Height != null ? $"{Width}x{Height}" : "0x0")})"; - public override string ToString() => Url.ToString(); + /// + /// Gets the URL of the video. + /// + public override string ToString() => Url; } } diff --git a/src/Discord.Net.Core/Entities/Messages/IAttachment.cs b/src/Discord.Net.Core/Entities/Messages/IAttachment.cs index 225e9cf2e..f01876186 100644 --- a/src/Discord.Net.Core/Entities/Messages/IAttachment.cs +++ b/src/Discord.Net.Core/Entities/Messages/IAttachment.cs @@ -1,14 +1,38 @@ -namespace Discord +namespace Discord { + /// + /// Represents a Discord attachment. + /// public interface IAttachment { + /// + /// Gets the snowflake ID of the attachment. + /// ulong Id { get; } + /// + /// Gets the filename of the attachment. + /// string Filename { get; } + /// + /// Gets the URL of the attachment. + /// string Url { get; } + /// + /// Gets the proxied URL of the attachment. + /// string ProxyUrl { get; } + /// + /// Gets the file size of the attachment. + /// int Size { get; } + /// + /// Gets the height of the attachment if it is an image, or return when it is not. + /// int? Height { get; } + /// + /// Gets the width of the attachment if it is an image, or return when it is not. + /// int? Width { get; } } } diff --git a/src/Discord.Net.Core/Entities/Messages/IEmbed.cs b/src/Discord.Net.Core/Entities/Messages/IEmbed.cs index f390c4c28..473a61ed5 100644 --- a/src/Discord.Net.Core/Entities/Messages/IEmbed.cs +++ b/src/Discord.Net.Core/Entities/Messages/IEmbed.cs @@ -1,22 +1,64 @@ -using System; +using System; using System.Collections.Immutable; namespace Discord { + /// + /// Represents a Discord embed object. + /// public interface IEmbed { + /// + /// Gets the title URL of the embed. + /// string Url { get; } + /// + /// Gets the title of the embed. + /// string Title { get; } + /// + /// Gets the description of the embed. + /// string Description { get; } + /// + /// Gets the type of the embed. + /// EmbedType Type { get; } + /// + /// Gets the timestamp of the embed, or if none is set. + /// DateTimeOffset? Timestamp { get; } + /// + /// Gets the sidebar color of the embed, or if none is set. + /// Color? Color { get; } + /// + /// Gets the image of the embed, or if none is set. + /// EmbedImage? Image { get; } + /// + /// Gets the video of the embed, or if none is set. + /// EmbedVideo? Video { get; } + /// + /// Gets the author field of the embed, or if none is set. + /// EmbedAuthor? Author { get; } + /// + /// Gets the footer field of the embed, or if none is set. + /// EmbedFooter? Footer { get; } + /// + /// Gets the provider of the embed, or if none is set. + /// EmbedProvider? Provider { get; } + /// + /// Gets the thumbnail featured in the embed, or if none is set. + /// EmbedThumbnail? Thumbnail { get; } + /// + /// Gets the fields of the embed. + /// ImmutableArray Fields { get; } } } diff --git a/src/Discord.Net.Core/Entities/Messages/IMessage.cs b/src/Discord.Net.Core/Entities/Messages/IMessage.cs index 4266f893a..e390fa682 100644 --- a/src/Discord.Net.Core/Entities/Messages/IMessage.cs +++ b/src/Discord.Net.Core/Entities/Messages/IMessage.cs @@ -1,41 +1,74 @@ -using System; +using System; using System.Collections.Generic; namespace Discord { + /// + /// Represents a Discord message object. + /// public interface IMessage : ISnowflakeEntity, IDeletable { - /// Gets the type of this system message. + /// + /// Gets the type of this system message. + /// MessageType Type { get; } - /// Gets the source of this message. + /// + /// Gets the source type of this message. + /// MessageSource Source { get; } - /// Returns true if this message was sent as a text-to-speech message. + /// + /// Returns if this message was sent as a text-to-speech message. + /// bool IsTTS { get; } - /// Returns true if this message was added to its channel's pinned messages. + /// + /// Returns if this message was added to its channel's pinned messages. + /// bool IsPinned { get; } - /// Returns the content for this message. + /// + /// Returns the content for this message. + /// string Content { get; } - /// Gets the time this message was sent. + /// + /// Gets the time this message was sent. + /// DateTimeOffset Timestamp { get; } - /// Gets the time of this message's last edit, if any. + /// + /// Gets the time of this message's last edit, or if none is set. + /// DateTimeOffset? EditedTimestamp { get; } - /// Gets the channel this message was sent to. + /// + /// Gets the channel this message was sent to. + /// IMessageChannel Channel { get; } - /// Gets the author of this message. + /// + /// Gets the author of this message. + /// IUser Author { get; } - /// Returns all attachments included in this message. + /// + /// Returns all attachments included in this message. + /// IReadOnlyCollection Attachments { get; } - /// Returns all embeds included in this message. + /// + /// Returns all embeds included in this message. + /// IReadOnlyCollection Embeds { get; } - /// Returns all tags included in this message's content. + /// + /// Returns all tags included in this message's content. + /// IReadOnlyCollection Tags { get; } - /// Returns the ids of channels mentioned in this message. + /// + /// Returns the IDs of channels mentioned in this message. + /// IReadOnlyCollection MentionedChannelIds { get; } - /// Returns the ids of roles mentioned in this message. + /// + /// Returns the IDs of roles mentioned in this message. + /// IReadOnlyCollection MentionedRoleIds { get; } - /// Returns the ids of users mentioned in this message. + /// + /// Returns the IDs of users mentioned in this message. + /// IReadOnlyCollection MentionedUserIds { get; } } } diff --git a/src/Discord.Net.Core/Entities/Messages/IReaction.cs b/src/Discord.Net.Core/Entities/Messages/IReaction.cs index 37ead42ae..b7d7128c0 100644 --- a/src/Discord.Net.Core/Entities/Messages/IReaction.cs +++ b/src/Discord.Net.Core/Entities/Messages/IReaction.cs @@ -1,7 +1,13 @@ -namespace Discord +namespace Discord { + /// + /// Represents a generic reaction object. + /// public interface IReaction { + /// + /// The used in the reaction. + /// IEmote Emote { get; } } } diff --git a/src/Discord.Net.Core/Entities/Messages/ISystemMessage.cs b/src/Discord.Net.Core/Entities/Messages/ISystemMessage.cs index 2dfaf8f2d..0f5a171d1 100644 --- a/src/Discord.Net.Core/Entities/Messages/ISystemMessage.cs +++ b/src/Discord.Net.Core/Entities/Messages/ISystemMessage.cs @@ -1,5 +1,8 @@ -namespace Discord +namespace Discord { + /// + /// Represents a message sent by the system. + /// public interface ISystemMessage : IMessage { } diff --git a/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs b/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs index 52df187f8..1afb3a3b2 100644 --- a/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs +++ b/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs @@ -1,31 +1,52 @@ -using System; +using System; using System.Collections.Generic; using System.Threading.Tasks; namespace Discord { + /// + /// Represents a Discord message object. + /// public interface IUserMessage : IMessage { - /// Modifies this message. + /// + /// Modifies this message. + /// Task ModifyAsync(Action func, RequestOptions options = null); - /// Adds this message to its channel's pinned messages. + /// + /// Adds this message to its channel's pinned messages. + /// Task PinAsync(RequestOptions options = null); - /// Removes this message from its channel's pinned messages. + /// + /// Removes this message from its channel's pinned messages. + /// Task UnpinAsync(RequestOptions options = null); - /// Returns all reactions included in this message. + /// + /// Returns all reactions included in this message. + /// IReadOnlyDictionary Reactions { get; } - /// Adds a reaction to this message. + /// + /// Adds a reaction to this message. + /// Task AddReactionAsync(IEmote emote, RequestOptions options = null); - /// Removes a reaction from message. + /// + /// Removes a reaction from message. + /// Task RemoveReactionAsync(IEmote emote, IUser user, RequestOptions options = null); - /// Removes all reactions from this message. + /// + /// Removes all reactions from this message. + /// Task RemoveAllReactionsAsync(RequestOptions options = null); - /// Gets all users that reacted to a message with a given emote + /// + /// Gets all users that reacted to a message with a given emote. + /// Task> GetReactionUsersAsync(IEmote emoji, int limit = 100, ulong? afterUserId = null, RequestOptions options = null); - /// Transforms this message's text into a human readable form by resolving its tags. + /// + /// Transforms this message's text into a human-readable form by resolving its tags. + /// string Resolve( TagHandling userHandling = TagHandling.Name, TagHandling channelHandling = TagHandling.Name, diff --git a/src/Discord.Net.Core/Entities/Messages/MessageProperties.cs b/src/Discord.Net.Core/Entities/Messages/MessageProperties.cs index b3f3a9c89..9daec32d3 100644 --- a/src/Discord.Net.Core/Entities/Messages/MessageProperties.cs +++ b/src/Discord.Net.Core/Entities/Messages/MessageProperties.cs @@ -1,36 +1,36 @@ -namespace Discord +namespace Discord { /// - /// Modify a message with the specified parameters. + /// Properties that are used to modify an with the specified changes. /// /// - /// The content of a message can be cleared with String.Empty; if and only if an Embed is present. + /// The content of a message can be cleared with String.Empty; if and only if an Embed is present. /// /// - /// - /// var message = await ReplyAsync("abc"); - /// await message.ModifyAsync(x => - /// { - /// x.Content = ""; - /// x.Embed = new EmbedBuilder() - /// .WithColor(new Color(40, 40, 120)) - /// .WithAuthor(a => a.Name = "foxbot") - /// .WithTitle("Embed!") - /// .WithDescription("This is an embed."); - /// }); - /// + /// + /// var message = await ReplyAsync("abc"); + /// await message.ModifyAsync(x => + /// { + /// x.Content = ""; + /// x.Embed = new EmbedBuilder() + /// .WithColor(new Color(40, 40, 120)) + /// .WithAuthor(a => a.Name = "foxbot") + /// .WithTitle("Embed!") + /// .WithDescription("This is an embed."); + /// }); + /// /// public class MessageProperties { /// - /// The content of the message + /// Gets or sets the content of the message. /// /// - /// This must be less than 2000 characters. + /// This must be less than 2000 characters. /// public Optional Content { get; set; } /// - /// The embed the message should display + /// Gets or sets the embed the message should display. /// public Optional Embed { get; set; } } diff --git a/src/Discord.Net.Core/Entities/Messages/MessageSource.cs b/src/Discord.Net.Core/Entities/Messages/MessageSource.cs index 1cb2f8b94..bd4f23727 100644 --- a/src/Discord.Net.Core/Entities/Messages/MessageSource.cs +++ b/src/Discord.Net.Core/Entities/Messages/MessageSource.cs @@ -1,10 +1,25 @@ namespace Discord { + /// + /// Specifies the source of the Discord message. + /// public enum MessageSource { + /// + /// The message is sent by the system. + /// System, + /// + /// The message is sent by a user. + /// User, + /// + /// The message is sent by a bot. + /// Bot, + /// + /// The message is sent by a webhook. + /// Webhook } } diff --git a/src/Discord.Net.Core/Entities/Messages/MessageType.cs b/src/Discord.Net.Core/Entities/Messages/MessageType.cs index 687e69e14..5056da8ea 100644 --- a/src/Discord.Net.Core/Entities/Messages/MessageType.cs +++ b/src/Discord.Net.Core/Entities/Messages/MessageType.cs @@ -1,13 +1,37 @@ -namespace Discord +namespace Discord { + /// + /// Specifies the type of message. + /// public enum MessageType { + /// + /// The default message type. + /// Default = 0, + /// + /// The message when a recipient is added. + /// RecipientAdd = 1, + /// + /// The message when a recipient is removed. + /// RecipientRemove = 2, + /// + /// The message when a user is called. + /// Call = 3, + /// + /// The message when a channel name is changed. + /// ChannelNameChange = 4, + /// + /// The message when a channel icon is changed. + /// ChannelIconChange = 5, + /// + /// The message when another message is pinned. + /// ChannelPinnedMessage = 6 } } diff --git a/src/Discord.Net.Core/Entities/Messages/ReactionMetadata.cs b/src/Discord.Net.Core/Entities/Messages/ReactionMetadata.cs index 005276202..8ef11bc47 100644 --- a/src/Discord.Net.Core/Entities/Messages/ReactionMetadata.cs +++ b/src/Discord.Net.Core/Entities/Messages/ReactionMetadata.cs @@ -1,11 +1,18 @@ -namespace Discord +namespace Discord { + /// + /// A metadata containing reaction information. + /// public struct ReactionMetadata { - /// Gets the number of reactions + /// + /// Gets the number of reactions. + /// public int ReactionCount { get; internal set; } - /// Returns true if the current user has used this reaction + /// + /// Returns if the current user has used this reaction. + /// public bool IsMe { get; internal set; } } } diff --git a/src/Discord.Net.Core/Entities/Messages/TagHandling.cs b/src/Discord.Net.Core/Entities/Messages/TagHandling.cs index 492f05879..667f7241b 100644 --- a/src/Discord.Net.Core/Entities/Messages/TagHandling.cs +++ b/src/Discord.Net.Core/Entities/Messages/TagHandling.cs @@ -1,13 +1,21 @@ -namespace Discord +namespace Discord { + /// Specifies the handling type the tag should use. public enum TagHandling { + /// Tag handling is ignored. Ignore = 0, //<@53905483156684800> -> <@53905483156684800> - Remove, //<@53905483156684800> -> + /// Removes the tag entirely. + Remove, //<@53905483156684800> -> + /// Resolves to username (e.g. @User). Name, //<@53905483156684800> -> @Voltana + /// Resolves to username without mention prefix (e.g. User). NameNoPrefix, //<@53905483156684800> -> Voltana + /// Resolves to username with discriminator value. (e.g. @User#0001). FullName, //<@53905483156684800> -> @Voltana#8252 + /// Resolves to username with discriminator value without mention prefix. (e.g. User#0001). FullNameNoPrefix, //<@53905483156684800> -> Voltana#8252 + /// Sanitizes the tag. Sanitize //<@53905483156684800> -> <@53905483156684800> (w/ nbsp) } } diff --git a/src/Discord.Net.Core/Entities/Messages/TagType.cs b/src/Discord.Net.Core/Entities/Messages/TagType.cs index 2d93bb3e3..177157251 100644 --- a/src/Discord.Net.Core/Entities/Messages/TagType.cs +++ b/src/Discord.Net.Core/Entities/Messages/TagType.cs @@ -1,12 +1,19 @@ -namespace Discord +namespace Discord { + /// Specifies the type of Discord tag. public enum TagType { + /// The object is an user mention. UserMention, + /// The object is a channel mention. ChannelMention, + /// The object is a role mention. RoleMention, + /// The object is an everyone mention. EveryoneMention, + /// The object is a here mention. HereMention, + /// The object is an emoji. Emoji } } diff --git a/src/Discord.Net.Core/Entities/Permissions/ChannelPermission.cs b/src/Discord.Net.Core/Entities/Permissions/ChannelPermission.cs index 740b6c30b..d683bd36b 100644 --- a/src/Discord.Net.Core/Entities/Permissions/ChannelPermission.cs +++ b/src/Discord.Net.Core/Entities/Permissions/ChannelPermission.cs @@ -1,38 +1,103 @@ -using System; +using System; namespace Discord { - [FlagsAttribute] + /// Defines the available permissions for a channel. + [Flags] public enum ChannelPermission : ulong { // General - CreateInstantInvite = 0x00_00_00_01, - ManageChannels = 0x00_00_00_10, + /// + /// Allows creation of instant invites. + /// + CreateInstantInvite = 0x00_00_00_01, + /// + /// Allows management and editing of channels. + /// + ManageChannels = 0x00_00_00_10, // Text - AddReactions = 0x00_00_00_40, + /// + /// Allows for the addition of reactions to messages. + /// + AddReactions = 0x00_00_00_40, + /// + /// Allows for reading of message. + /// [Obsolete("Use ViewChannel instead.")] - ReadMessages = ViewChannel, - ViewChannel = 0x00_00_04_00, - SendMessages = 0x00_00_08_00, - SendTTSMessages = 0x00_00_10_00, - ManageMessages = 0x00_00_20_00, - EmbedLinks = 0x00_00_40_00, + ReadMessages = ViewChannel, + /// + /// Allows guild members to view a channel, which includes reading messages in text channels. + /// + ViewChannel = 0x00_00_04_00, + /// + /// Allows for sending messages in a channel. + /// + SendMessages = 0x00_00_08_00, + /// + /// Allows for sending of text-to-speech messages. + /// + SendTTSMessages = 0x00_00_10_00, + /// + /// Allows for deletion of other users messages. + /// + ManageMessages = 0x00_00_20_00, + /// + /// Allows links sent by users with this permission will be auto-embedded. + /// + EmbedLinks = 0x00_00_40_00, + /// + /// Allows for uploading images and files. + /// AttachFiles = 0x00_00_80_00, - ReadMessageHistory = 0x00_01_00_00, + /// + /// Allows for reading of message history. + /// + ReadMessageHistory = 0x00_01_00_00, + /// + /// Allows for using the @everyone tag to notify all users in a channel, and the @here tag to notify all + /// online users in a channel. + /// MentionEveryone = 0x00_02_00_00, + /// + /// Allows the usage of custom emojis from other servers. + /// UseExternalEmojis = 0x00_04_00_00, // Voice - Connect = 0x00_10_00_00, - Speak = 0x00_20_00_00, - MuteMembers = 0x00_40_00_00, - DeafenMembers = 0x00_80_00_00, + /// + /// Allows for joining of a voice channel. + /// + Connect = 0x00_10_00_00, + /// + /// Allows for speaking in a voice channel. + /// + Speak = 0x00_20_00_00, + /// + /// Allows for muting members in a voice channel. + /// + MuteMembers = 0x00_40_00_00, + /// + /// Allows for deafening of members in a voice channel. + /// + DeafenMembers = 0x00_80_00_00, + /// + /// Allows for moving of members between voice channels. + /// MoveMembers = 0x01_00_00_00, - UseVAD = 0x02_00_00_00, + /// + /// Allows for using voice-activity-detection in a voice channel. + /// + UseVAD = 0x02_00_00_00, // More General + /// + /// Allows management and editing of roles. + /// ManageRoles = 0x10_00_00_00, - ManageWebhooks = 0x20_00_00_00, + /// + /// Allows management and editing of webhooks. + /// + ManageWebhooks = 0x20_00_00_00, } } diff --git a/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs b/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs index 1a8aad53c..58d25daac 100644 --- a/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs +++ b/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; @@ -7,81 +7,84 @@ namespace Discord [DebuggerDisplay("{DebuggerDisplay,nq}")] public struct ChannelPermissions { - /// Gets a blank ChannelPermissions that grants no permissions. + /// Gets a blank that grants no permissions. public static readonly ChannelPermissions None = new ChannelPermissions(); - /// Gets a ChannelPermissions that grants all permissions for text channels. + /// Gets a that grants all permissions for text channels. public static readonly ChannelPermissions Text = new ChannelPermissions(0b01100_0000000_1111111110001_010001); - /// Gets a ChannelPermissions that grants all permissions for voice channels. + /// Gets a that grants all permissions for voice channels. public static readonly ChannelPermissions Voice = new ChannelPermissions(0b00100_1111110_0000000000000_010001); - /// Gets a ChannelPermissions that grants all permissions for direct message channels. + /// Gets a that grants all permissions for category channels. + public static readonly ChannelPermissions Category = new ChannelPermissions(0b01100_1111110_1111111110001_010001); + /// Gets a that grants all permissions for direct message channels. public static readonly ChannelPermissions DM = new ChannelPermissions(0b00000_1000110_1011100110000_000000); - /// Gets a ChannelPermissions that grants all permissions for group channels. + /// Gets a that grants all permissions for group channels. public static readonly ChannelPermissions Group = new ChannelPermissions(0b00000_1000110_0001101100000_000000); - /// Gets a ChannelPermissions that grants all permissions for a given channelType. + /// Gets a that grants all permissions for a given channelType. public static ChannelPermissions All(IChannel channel) { switch (channel) { case ITextChannel _: return Text; case IVoiceChannel _: return Voice; + case ICategoryChannel _: return Category; case IDMChannel _: return DM; case IGroupChannel _: return Group; - default: throw new ArgumentException("Unknown channel type", nameof(channel)); + default: throw new ArgumentException("Unknown channel type.", nameof(channel)); } } - /// Gets a packed value representing all the permissions in this ChannelPermissions. + /// Gets a packed value representing all the permissions in this . public ulong RawValue { get; } - /// If True, a user may create invites. + /// If , a user may create invites. public bool CreateInstantInvite => Permissions.GetValue(RawValue, ChannelPermission.CreateInstantInvite); - /// If True, a user may create, delete and modify this channel. + /// If , a user may create, delete and modify this channel. public bool ManageChannel => Permissions.GetValue(RawValue, ChannelPermission.ManageChannels); - /// If true, a user may add reactions. + /// If , a user may add reactions. public bool AddReactions => Permissions.GetValue(RawValue, ChannelPermission.AddReactions); - /// If True, a user may join channels. + /// If , a user may join channels. [Obsolete("Use ViewChannel instead.")] public bool ReadMessages => ViewChannel; - /// If True, a user may view channels. + /// If , a user may view channels. public bool ViewChannel => Permissions.GetValue(RawValue, ChannelPermission.ViewChannel); - /// If True, a user may send messages. + /// If , a user may send messages. public bool SendMessages => Permissions.GetValue(RawValue, ChannelPermission.SendMessages); - /// If True, a user may send text-to-speech messages. + /// If , a user may send text-to-speech messages. public bool SendTTSMessages => Permissions.GetValue(RawValue, ChannelPermission.SendTTSMessages); - /// If True, a user may delete messages. + /// If , a user may delete messages. public bool ManageMessages => Permissions.GetValue(RawValue, ChannelPermission.ManageMessages); - /// If True, Discord will auto-embed links sent by this user. + /// If , Discord will auto-embed links sent by this user. public bool EmbedLinks => Permissions.GetValue(RawValue, ChannelPermission.EmbedLinks); - /// If True, a user may send files. + /// If , a user may send files. public bool AttachFiles => Permissions.GetValue(RawValue, ChannelPermission.AttachFiles); - /// If True, a user may read previous messages. + /// If , a user may read previous messages. public bool ReadMessageHistory => Permissions.GetValue(RawValue, ChannelPermission.ReadMessageHistory); - /// If True, a user may mention @everyone. + /// If , a user may mention @everyone. public bool MentionEveryone => Permissions.GetValue(RawValue, ChannelPermission.MentionEveryone); - /// If True, a user may use custom emoji from other guilds. + /// If , a user may use custom emoji from other guilds. public bool UseExternalEmojis => Permissions.GetValue(RawValue, ChannelPermission.UseExternalEmojis); - /// If True, a user may connect to a voice channel. + /// If , a user may connect to a voice channel. public bool Connect => Permissions.GetValue(RawValue, ChannelPermission.Connect); - /// If True, a user may speak in a voice channel. + /// If , a user may speak in a voice channel. public bool Speak => Permissions.GetValue(RawValue, ChannelPermission.Speak); - /// If True, a user may mute users. + /// If , a user may mute users. public bool MuteMembers => Permissions.GetValue(RawValue, ChannelPermission.MuteMembers); - /// If True, a user may deafen users. + /// If , a user may deafen users. public bool DeafenMembers => Permissions.GetValue(RawValue, ChannelPermission.DeafenMembers); - /// If True, a user may move other users between voice channels. + /// If , a user may move other users between voice channels. public bool MoveMembers => Permissions.GetValue(RawValue, ChannelPermission.MoveMembers); - /// If True, a user may use voice-activity-detection rather than push-to-talk. + /// If , a user may use voice-activity-detection rather than push-to-talk. public bool UseVAD => Permissions.GetValue(RawValue, ChannelPermission.UseVAD); - /// If True, a user may adjust role permissions. This also implictly grants all other permissions. + /// If , a user may adjust role permissions. This also implictly grants all other permissions. public bool ManageRoles => Permissions.GetValue(RawValue, ChannelPermission.ManageRoles); - /// If True, a user may edit the webhooks for this channel. + /// If , a user may edit the webhooks for this channel. public bool ManageWebhooks => Permissions.GetValue(RawValue, ChannelPermission.ManageWebhooks); - /// Creates a new ChannelPermissions with the provided packed value. + /// Creates a new with the provided packed value. public ChannelPermissions(ulong rawValue) { RawValue = rawValue; } private ChannelPermissions(ulong initialValue, bool? createInstantInvite = null, bool? manageChannel = null, @@ -117,7 +120,7 @@ namespace Discord RawValue = value; } - /// Creates a new ChannelPermissions with the provided permissions. + /// Creates a new with the provided permissions. public ChannelPermissions(bool createInstantInvite = false, bool manageChannel = false, bool addReactions = false, bool viewChannel = false, bool sendMessages = false, bool sendTTSMessages = false, bool manageMessages = false, @@ -129,7 +132,7 @@ namespace Discord speak, muteMembers, deafenMembers, moveMembers, useVoiceActivation, manageRoles, manageWebhooks) { } - /// Creates a new ChannelPermissions from this one, changing the provided non-null permissions. + /// Creates a new from this one, changing the provided non-null permissions. public ChannelPermissions Modify(bool? createInstantInvite = null, bool? manageChannel = null, bool? addReactions = null, bool? viewChannel = null, bool? sendMessages = null, bool? sendTTSMessages = null, bool? manageMessages = null, @@ -157,4 +160,4 @@ namespace Discord public override string ToString() => RawValue.ToString(); private string DebuggerDisplay => $"{string.Join(", ", ToList())}"; } -} \ No newline at end of file +} diff --git a/src/Discord.Net.Core/Entities/Permissions/GuildPermission.cs b/src/Discord.Net.Core/Entities/Permissions/GuildPermission.cs index 8469fd304..663c7fc6c 100644 --- a/src/Discord.Net.Core/Entities/Permissions/GuildPermission.cs +++ b/src/Discord.Net.Core/Entities/Permissions/GuildPermission.cs @@ -1,44 +1,131 @@ -using System; +using System; namespace Discord { - [FlagsAttribute] + /// Defines the available permissions for a channel. + [Flags] public enum GuildPermission : ulong { // General + /// + /// Allows creation of instant invites. + /// CreateInstantInvite = 0x00_00_00_01, - KickMembers = 0x00_00_00_02, - BanMembers = 0x00_00_00_04, - Administrator = 0x00_00_00_08, - ManageChannels = 0x00_00_00_10, - ManageGuild = 0x00_00_00_20, + /// + /// Allows kicking members. + /// + KickMembers = 0x00_00_00_02, + /// + /// Allows banning members. + /// + BanMembers = 0x00_00_00_04, + /// + /// Allows all permissions and bypasses channel permission overwrites. + /// + Administrator = 0x00_00_00_08, + /// + /// Allows management and editing of channels. + /// + ManageChannels = 0x00_00_00_10, + /// + /// Allows management and editing of the guild. + /// + ManageGuild = 0x00_00_00_20, // Text + /// + /// Allows for the addition of reactions to messages. + /// AddReactions = 0x00_00_00_40, + /// + /// Allows for viewing of audit logs. + /// ViewAuditLog = 0x00_00_00_80, - ReadMessages = 0x00_00_04_00, + /// + /// Allows for reading of message. + /// + ReadMessages = 0x00_00_04_00, + /// + /// Allows for sending messages in a channel. + /// SendMessages = 0x00_00_08_00, + /// + /// Allows for sending of text-to-speech messages. + /// SendTTSMessages = 0x00_00_10_00, - ManageMessages = 0x00_00_20_00, - EmbedLinks = 0x00_00_40_00, - AttachFiles = 0x00_00_80_00, - ReadMessageHistory = 0x00_01_00_00, - MentionEveryone = 0x00_02_00_00, - UseExternalEmojis = 0x00_04_00_00, + /// + /// Allows for deletion of other users messages. + /// + ManageMessages = 0x00_00_20_00, + /// + /// Allows links sent by users with this permission will be auto-embedded. + /// + EmbedLinks = 0x00_00_40_00, + /// + /// Allows for uploading images and files. + /// + AttachFiles = 0x00_00_80_00, + /// + /// Allows for reading of message history. + /// + ReadMessageHistory = 0x00_01_00_00, + /// + /// Allows for using the @everyone tag to notify all users in a channel, and the @here tag to notify all + /// online users in a channel. + /// + MentionEveryone = 0x00_02_00_00, + /// + /// Allows the usage of custom emojis from other servers. + /// + UseExternalEmojis = 0x00_04_00_00, + // Voice - Connect = 0x00_10_00_00, - Speak = 0x00_20_00_00, - MuteMembers = 0x00_40_00_00, - DeafenMembers = 0x00_80_00_00, - MoveMembers = 0x01_00_00_00, - UseVAD = 0x02_00_00_00, + /// + /// Allows for joining of a voice channel. + /// + Connect = 0x00_10_00_00, + /// + /// Allows for speaking in a voice channel. + /// + Speak = 0x00_20_00_00, + /// + /// Allows for muting members in a voice channel. + /// + MuteMembers = 0x00_40_00_00, + /// + /// Allows for deafening of members in a voice channel. + /// + DeafenMembers = 0x00_80_00_00, + /// + /// Allows for moving of members between voice channels. + /// + MoveMembers = 0x01_00_00_00, + /// + /// Allows for using voice-activity-detection in a voice channel. + /// + UseVAD = 0x02_00_00_00, // General 2 - ChangeNickname = 0x04_00_00_00, + /// + /// Allows for modification of own nickname. + /// + ChangeNickname = 0x04_00_00_00, + /// + /// Allows for modification of other users nicknames. + /// ManageNicknames = 0x08_00_00_00, + /// + /// Allows management and editing of roles. + /// ManageRoles = 0x10_00_00_00, + /// + /// Allows management and editing of webhooks. + /// ManageWebhooks = 0x20_00_00_00, + /// + /// Allows management and editing of emojis. + /// ManageEmojis = 0x40_00_00_00 } } diff --git a/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs b/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs index a880e62ca..e1dbb08fd 100644 --- a/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs +++ b/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Diagnostics; namespace Discord @@ -6,78 +6,78 @@ namespace Discord [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public struct GuildPermissions { - /// Gets a blank GuildPermissions that grants no permissions. + /// Gets a blank that grants no permissions. public static readonly GuildPermissions None = new GuildPermissions(); - /// Gets a GuildPermissions that grants all guild permissions for webhook users. + /// Gets a that grants all guild permissions for webhook users. public static readonly GuildPermissions Webhook = new GuildPermissions(0b00000_0000000_0001101100000_000000); - /// Gets a GuildPermissions that grants all guild permissions. + /// Gets a that grants all guild permissions. public static readonly GuildPermissions All = new GuildPermissions(0b11111_1111110_1111111110011_111111); - /// Gets a packed value representing all the permissions in this GuildPermissions. + /// Gets a packed value representing all the permissions in this . public ulong RawValue { get; } - /// If True, a user may create invites. + /// If , a user may create invites. public bool CreateInstantInvite => Permissions.GetValue(RawValue, GuildPermission.CreateInstantInvite); - /// If True, a user may ban users from the guild. + /// If , a user may ban users from the guild. public bool BanMembers => Permissions.GetValue(RawValue, GuildPermission.BanMembers); - /// If True, a user may kick users from the guild. + /// If , a user may kick users from the guild. public bool KickMembers => Permissions.GetValue(RawValue, GuildPermission.KickMembers); - /// If True, a user is granted all permissions, and cannot have them revoked via channel permissions. + /// If , a user is granted all permissions, and cannot have them revoked via channel permissions. public bool Administrator => Permissions.GetValue(RawValue, GuildPermission.Administrator); - /// If True, a user may create, delete and modify channels. + /// If , a user may create, delete and modify channels. public bool ManageChannels => Permissions.GetValue(RawValue, GuildPermission.ManageChannels); - /// If True, a user may adjust guild properties. + /// If , a user may adjust guild properties. public bool ManageGuild => Permissions.GetValue(RawValue, GuildPermission.ManageGuild); - /// If true, a user may add reactions. + /// If , a user may add reactions. public bool AddReactions => Permissions.GetValue(RawValue, GuildPermission.AddReactions); - /// If true, a user may view the audit log. + /// If , a user may view the audit log. public bool ViewAuditLog => Permissions.GetValue(RawValue, GuildPermission.ViewAuditLog); - /// If True, a user may join channels. + /// If , a user may join channels. public bool ReadMessages => Permissions.GetValue(RawValue, GuildPermission.ReadMessages); - /// If True, a user may send messages. + /// If , a user may send messages. public bool SendMessages => Permissions.GetValue(RawValue, GuildPermission.SendMessages); - /// If True, a user may send text-to-speech messages. + /// If , a user may send text-to-speech messages. public bool SendTTSMessages => Permissions.GetValue(RawValue, GuildPermission.SendTTSMessages); - /// If True, a user may delete messages. + /// If , a user may delete messages. public bool ManageMessages => Permissions.GetValue(RawValue, GuildPermission.ManageMessages); - /// If True, Discord will auto-embed links sent by this user. + /// If , Discord will auto-embed links sent by this user. public bool EmbedLinks => Permissions.GetValue(RawValue, GuildPermission.EmbedLinks); - /// If True, a user may send files. + /// If , a user may send files. public bool AttachFiles => Permissions.GetValue(RawValue, GuildPermission.AttachFiles); - /// If True, a user may read previous messages. + /// If , a user may read previous messages. public bool ReadMessageHistory => Permissions.GetValue(RawValue, GuildPermission.ReadMessageHistory); - /// If True, a user may mention @everyone. + /// If , a user may mention @everyone. public bool MentionEveryone => Permissions.GetValue(RawValue, GuildPermission.MentionEveryone); - /// If True, a user may use custom emoji from other guilds. + /// If , a user may use custom emoji from other guilds. public bool UseExternalEmojis => Permissions.GetValue(RawValue, GuildPermission.UseExternalEmojis); - /// If True, a user may connect to a voice channel. + /// If , a user may connect to a voice channel. public bool Connect => Permissions.GetValue(RawValue, GuildPermission.Connect); - /// If True, a user may speak in a voice channel. + /// If , a user may speak in a voice channel. public bool Speak => Permissions.GetValue(RawValue, GuildPermission.Speak); - /// If True, a user may mute users. + /// If , a user may mute users. public bool MuteMembers => Permissions.GetValue(RawValue, GuildPermission.MuteMembers); - /// If True, a user may deafen users. + /// If , a user may deafen users. public bool DeafenMembers => Permissions.GetValue(RawValue, GuildPermission.DeafenMembers); - /// If True, a user may move other users between voice channels. + /// If , a user may move other users between voice channels. public bool MoveMembers => Permissions.GetValue(RawValue, GuildPermission.MoveMembers); - /// If True, a user may use voice-activity-detection rather than push-to-talk. + /// If , a user may use voice-activity-detection rather than push-to-talk. public bool UseVAD => Permissions.GetValue(RawValue, GuildPermission.UseVAD); - /// If True, a user may change their own nickname. + /// If , a user may change their own nickname. public bool ChangeNickname => Permissions.GetValue(RawValue, GuildPermission.ChangeNickname); - /// If True, a user may change the nickname of other users. + /// If , a user may change the nickname of other users. public bool ManageNicknames => Permissions.GetValue(RawValue, GuildPermission.ManageNicknames); - /// If True, a user may adjust roles. + /// If , a user may adjust roles. public bool ManageRoles => Permissions.GetValue(RawValue, GuildPermission.ManageRoles); - /// If True, a user may edit the webhooks for this guild. + /// If , a user may edit the webhooks for this guild. public bool ManageWebhooks => Permissions.GetValue(RawValue, GuildPermission.ManageWebhooks); - /// If True, a user may edit the emojis for this guild. + /// If , a user may edit the emojis for this guild. public bool ManageEmojis => Permissions.GetValue(RawValue, GuildPermission.ManageEmojis); - /// Creates a new GuildPermissions with the provided packed value. + /// Creates a new with the provided packed value. public GuildPermissions(ulong rawValue) { RawValue = rawValue; } private GuildPermissions(ulong initialValue, bool? createInstantInvite = null, bool? kickMembers = null, @@ -123,7 +123,7 @@ namespace Discord RawValue = value; } - /// Creates a new GuildPermissions with the provided permissions. + /// Creates a new with the provided permissions. public GuildPermissions(bool createInstantInvite = false, bool kickMembers = false, bool banMembers = false, bool administrator = false, bool manageChannels = false, bool manageGuild = false, bool addReactions = false, bool viewAuditLog = false, @@ -141,7 +141,7 @@ namespace Discord manageNicknames: manageNicknames, manageWebhooks: manageWebhooks, manageEmojis: manageEmojis) { } - /// Creates a new GuildPermissions from this one, changing the provided non-null permissions. + /// Creates a new from this one, changing the provided non-null permissions. public GuildPermissions Modify(bool? createInstantInvite = null, bool? kickMembers = null, bool? banMembers = null, bool? administrator = null, bool? manageChannels = null, bool? manageGuild = null, bool? addReactions = null, bool? viewAuditLog = null, diff --git a/src/Discord.Net.Core/Entities/Permissions/Overwrite.cs b/src/Discord.Net.Core/Entities/Permissions/Overwrite.cs index bda67a870..790e7ea72 100644 --- a/src/Discord.Net.Core/Entities/Permissions/Overwrite.cs +++ b/src/Discord.Net.Core/Entities/Permissions/Overwrite.cs @@ -1,15 +1,23 @@ -namespace Discord +namespace Discord { public struct Overwrite { - /// Gets the unique identifier for the object this overwrite is targeting. + /// + /// Gets the unique identifier for the object this overwrite is targeting. + /// public ulong TargetId { get; } - /// Gets the type of object this overwrite is targeting. + /// + /// Gets the type of object this overwrite is targeting. + /// public PermissionTarget TargetType { get; } - /// Gets the permissions associated with this overwrite entry. + /// + /// Gets the permissions associated with this overwrite entry. + /// public OverwritePermissions Permissions { get; } - /// Creates a new Overwrite with provided target information and modified permissions. + /// + /// Creates a new with provided target information and modified permissions. + /// public Overwrite(ulong targetId, PermissionTarget targetType, OverwritePermissions permissions) { TargetId = targetId; diff --git a/src/Discord.Net.Core/Entities/Permissions/OverwritePermissions.cs b/src/Discord.Net.Core/Entities/Permissions/OverwritePermissions.cs index 108b67273..bcc34b98a 100644 --- a/src/Discord.Net.Core/Entities/Permissions/OverwritePermissions.cs +++ b/src/Discord.Net.Core/Entities/Permissions/OverwritePermissions.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; @@ -7,18 +7,28 @@ namespace Discord [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public struct OverwritePermissions { - /// Gets a blank OverwritePermissions that inherits all permissions. + /// + /// Gets a blank that inherits all permissions. + /// public static OverwritePermissions InheritAll { get; } = new OverwritePermissions(); - /// Gets a OverwritePermissions that grants all permissions for a given channelType. + /// + /// Gets a that grants all permissions for the given channel. + /// public static OverwritePermissions AllowAll(IChannel channel) => new OverwritePermissions(ChannelPermissions.All(channel).RawValue, 0); - /// Gets a OverwritePermissions that denies all permissions for a given channelType. + /// + /// Gets a that denies all permissions for the given channel. + /// public static OverwritePermissions DenyAll(IChannel channel) => new OverwritePermissions(0, ChannelPermissions.All(channel).RawValue); - /// Gets a packed value representing all the allowed permissions in this OverwritePermissions. + /// + /// Gets a packed value representing all the allowed permissions in this . + /// public ulong AllowValue { get; } - /// Gets a packed value representing all the denied permissions in this OverwritePermissions. + /// + /// Gets a packed value representing all the denied permissions in this . + /// public ulong DenyValue { get; } /// If Allowed, a user may create invites. @@ -62,7 +72,7 @@ namespace Discord /// If Allowed, a user may use voice-activity-detection rather than push-to-talk. public PermValue UseVAD => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.UseVAD); - /// If Allowed, a user may adjust role permissions. This also implictly grants all other permissions. + /// If Allowed, a user may adjust role permissions. This also implicitly grants all other permissions. public PermValue ManageRoles => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.ManageRoles); /// If True, a user may edit the webhooks for this channel. public PermValue ManageWebhooks => Permissions.GetValue(AllowValue, DenyValue, ChannelPermission.ManageWebhooks); @@ -107,7 +117,9 @@ namespace Discord DenyValue = denyValue; } - /// Creates a new ChannelPermissions with the provided permissions. + /// + /// Creates a new with the provided permissions. + /// public OverwritePermissions(PermValue createInstantInvite = PermValue.Inherit, PermValue manageChannel = PermValue.Inherit, PermValue addReactions = PermValue.Inherit, PermValue readMessages = PermValue.Inherit, PermValue sendMessages = PermValue.Inherit, PermValue sendTTSMessages = PermValue.Inherit, PermValue manageMessages = PermValue.Inherit, @@ -118,7 +130,10 @@ namespace Discord embedLinks, attachFiles, readMessageHistory, mentionEveryone, useExternalEmojis, connect, speak, muteMembers, deafenMembers, moveMembers, useVoiceActivation, manageRoles, manageWebhooks) { } - /// Creates a new OverwritePermissions from this one, changing the provided non-null permissions. + /// + /// Creates a new from this one, changing the provided non-null + /// permissions. + /// public OverwritePermissions Modify(PermValue? createInstantInvite = null, PermValue? manageChannel = null, PermValue? addReactions = null, PermValue? readMessages = null, PermValue? sendMessages = null, PermValue? sendTTSMessages = null, PermValue? manageMessages = null, diff --git a/src/Discord.Net.Core/Entities/Permissions/PermValue.cs b/src/Discord.Net.Core/Entities/Permissions/PermValue.cs index fe048b016..6cea8270d 100644 --- a/src/Discord.Net.Core/Entities/Permissions/PermValue.cs +++ b/src/Discord.Net.Core/Entities/Permissions/PermValue.cs @@ -1,9 +1,13 @@ -namespace Discord +namespace Discord { + /// Specifies the permission value. public enum PermValue { + /// Allows this permission. Allow, + /// Denies this permission. Deny, + /// Inherits the permission settings. Inherit } } diff --git a/src/Discord.Net.Core/Entities/Roles/Color.cs b/src/Discord.Net.Core/Entities/Roles/Color.cs index 89e76df6d..b84bbb313 100644 --- a/src/Discord.Net.Core/Entities/Roles/Color.cs +++ b/src/Discord.Net.Core/Entities/Roles/Color.cs @@ -3,6 +3,9 @@ using System.Diagnostics; namespace Discord { + /// + /// Represents a Discord color. + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public struct Color { @@ -59,10 +62,20 @@ namespace Discord /// Gets the blue component for this color. public byte B => (byte)(RawValue); + /// + /// Initializes a struct with the given raw value. + /// + /// A raw value for the color (e.g. 0x607D8B). public Color(uint rawValue) { RawValue = rawValue; } + /// + /// Initializes a struct with the given RGB bytes. + /// + /// The that represents the red color. + /// The that represents the green color. + /// The that represents the blue color. public Color(byte r, byte g, byte b) { RawValue = @@ -70,33 +83,48 @@ namespace Discord ((uint)g << 8) | (uint)b; } + /// + /// Initializes a struct with the given RGB value. + /// + /// The value that represents the red color. Must be within 0~255. + /// The value that represents the green color. Must be within 0~255. + /// The value that represents the blue color. Must be within 0~255. public Color(int r, int g, int b) { if (r < 0 || r > 255) - throw new ArgumentOutOfRangeException(nameof(r), "Value must be within [0,255]"); + throw new ArgumentOutOfRangeException(nameof(r), "Value must be within [0,255]."); if (g < 0 || g > 255) - throw new ArgumentOutOfRangeException(nameof(g), "Value must be within [0,255]"); + throw new ArgumentOutOfRangeException(nameof(g), "Value must be within [0,255]."); if (b < 0 || b > 255) - throw new ArgumentOutOfRangeException(nameof(b), "Value must be within [0,255]"); + throw new ArgumentOutOfRangeException(nameof(b), "Value must be within [0,255]."); RawValue = ((uint)r << 16) | ((uint)g << 8) | (uint)b; } + /// + /// Initializes a struct with the given RGB float value. + /// + /// The value that represents the red color. Must be within 0~1. + /// The value that represents the green color. Must be within 0~1. + /// The value that represents the blue color. Must be within 0~1. public Color(float r, float g, float b) { if (r < 0.0f || r > 1.0f) - throw new ArgumentOutOfRangeException(nameof(r), "Value must be within [0,1]"); + throw new ArgumentOutOfRangeException(nameof(r), "Value must be within [0,1]."); if (g < 0.0f || g > 1.0f) - throw new ArgumentOutOfRangeException(nameof(g), "Value must be within [0,1]"); + throw new ArgumentOutOfRangeException(nameof(g), "Value must be within [0,1]."); if (b < 0.0f || b > 1.0f) - throw new ArgumentOutOfRangeException(nameof(b), "Value must be within [0,1]"); + throw new ArgumentOutOfRangeException(nameof(b), "Value must be within [0,1]."); RawValue = ((uint)(r * 255.0f) << 16) | ((uint)(g * 255.0f) << 8) | (uint)(b * 255.0f); } - + + /// + /// Gets the hexadecimal representation of the color (e.g. #000ccc). + /// public override string ToString() => $"#{Convert.ToString(RawValue, 16)}"; private string DebuggerDisplay => diff --git a/src/Discord.Net.Core/Entities/Roles/IRole.cs b/src/Discord.Net.Core/Entities/Roles/IRole.cs index c40e0d716..9aa509cd4 100644 --- a/src/Discord.Net.Core/Entities/Roles/IRole.cs +++ b/src/Discord.Net.Core/Entities/Roles/IRole.cs @@ -1,29 +1,50 @@ -using System; +using System; using System.Threading.Tasks; namespace Discord { + /// + /// Represents a generic role object. + /// public interface IRole : ISnowflakeEntity, IDeletable, IMentionable, IComparable { - /// Gets the guild owning this role. + /// + /// Gets the guild owning this role. + /// IGuild Guild { get; } - /// Gets the color given to users of this role. + /// + /// Gets the color given to users of this role. + /// Color Color { get; } - /// Returns true if users of this role are separated in the user list. + /// + /// Returns if users of this role are separated in the user list. + /// bool IsHoisted { get; } - /// Returns true if this role is automatically managed by Discord. + /// + /// Returns if this role is automatically managed by Discord. + /// bool IsManaged { get; } - /// Returns true if this role may be mentioned in messages. + /// + /// Returns if this role may be mentioned in messages. + /// bool IsMentionable { get; } - /// Gets the name of this role. + /// + /// Gets the name of this role. + /// string Name { get; } - /// Gets the permissions granted to members of this role. + /// + /// Gets the permissions granted to members of this role. + /// GuildPermissions Permissions { get; } - /// Gets this role's position relative to other roles in the same guild. + /// + /// Gets this role's position relative to other roles in the same guild. + /// int Position { get; } - ///// Modifies this role. + /// + /// Modifies this role. + /// Task ModifyAsync(Action func, RequestOptions options = null); } } diff --git a/src/Discord.Net.Core/Entities/Roles/ReorderRoleProperties.cs b/src/Discord.Net.Core/Entities/Roles/ReorderRoleProperties.cs index 0c8afa24c..fcfa08da5 100644 --- a/src/Discord.Net.Core/Entities/Roles/ReorderRoleProperties.cs +++ b/src/Discord.Net.Core/Entities/Roles/ReorderRoleProperties.cs @@ -1,12 +1,24 @@ -namespace Discord +namespace Discord { + /// + /// Properties that are used to reorder an . + /// public class ReorderRoleProperties { - /// The id of the role to be edited + /// + /// Gets the ID of the role to be edited. + /// public ulong Id { get; } - /// The new zero-based position of the role. + /// + /// Gets the new zero-based position of the role. + /// public int Position { get; } + /// + /// Initializes a with the given role ID and position. + /// + /// The ID of the role to be edited. + /// The new zero-based position of the role. public ReorderRoleProperties(ulong id, int pos) { Id = id; diff --git a/src/Discord.Net.Core/Entities/Roles/RoleProperties.cs b/src/Discord.Net.Core/Entities/Roles/RoleProperties.cs index 8950a2634..7769a2a04 100644 --- a/src/Discord.Net.Core/Entities/Roles/RoleProperties.cs +++ b/src/Discord.Net.Core/Entities/Roles/RoleProperties.cs @@ -1,57 +1,57 @@ -namespace Discord +namespace Discord { /// - /// Modify an IRole with the specified parameters + /// Properties that are used to modify an with the specified changes. /// /// - /// - /// await role.ModifyAsync(x => - /// { - /// x.Color = new Color(180, 15, 40); - /// x.Hoist = true; - /// }); + /// + /// await role.ModifyAsync(x => + /// { + /// x.Color = new Color(180, 15, 40); + /// x.Hoist = true; + /// }); /// /// - /// + /// public class RoleProperties { /// - /// The name of the role + /// Gets or sets the name of the role. /// /// - /// If this role is the EveryoneRole, this value may not be set. + /// If this role is the EveryoneRole, this value may not be set. /// public Optional Name { get; set; } /// - /// The role's GuildPermissions + /// Gets or sets the role's . /// public Optional Permissions { get; set; } /// - /// The position of the role. This is 0-based! + /// Gets or sets the position of the role. This is 0-based! /// /// - /// If this role is the EveryoneRole, this value may not be set. + /// If this role is the EveryoneRole, this value may not be set. /// public Optional Position { get; set; } /// - /// The color of the Role. + /// Gets or sets the color of the role. /// /// - /// If this role is the EveryoneRole, this value may not be set. + /// If this role is the EveryoneRole, this value may not be set. /// public Optional Color { get; set; } /// - /// Whether or not this role should be displayed independently in the userlist. + /// Gets or sets whether or not this role should be displayed independently in the user list. /// /// - /// If this role is the EveryoneRole, this value may not be set. + /// If this role is the EveryoneRole, this value may not be set. /// public Optional Hoist { get; set; } /// - /// Whether or not this role can be mentioned. + /// Gets or sets whether or not this role can be mentioned. /// /// - /// If this role is the EveryoneRole, this value may not be set. + /// If this role is the EveryoneRole, this value may not be set. /// public Optional Mentionable { get; set; } } diff --git a/src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs b/src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs index 1c5e5482c..c731f1459 100644 --- a/src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs +++ b/src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs @@ -1,70 +1,83 @@ -using System.Collections.Generic; +using System.Collections.Generic; namespace Discord { /// - /// Modify an IGuildUser with the following parameters. + /// Properties that are used to modify an with the following parameters. /// /// - /// - /// await (Context.User as IGuildUser)?.ModifyAsync(x => + /// + /// await (Context.User as IGuildUser)?.ModifyAsync(x => /// { /// x.Nickname = $"festive {Context.User.Username}"; /// }); /// /// - /// + /// public class GuildUserProperties { /// - /// Should the user be guild-muted in a voice channel? + /// Gets or sets whether the user should be muted in a voice channel. /// /// - /// If this value is set to true, no user will be able to hear this user speak in the guild. + /// If this value is set to , no user will be able to hear this user speak in the guild. /// public Optional Mute { get; set; } /// - /// Should the user be guild-deafened in a voice channel? + /// Gets or sets whether the user should be deafened in a voice channel. /// /// - /// If this value is set to true, this user will not be able to hear anyone speak in the guild. + /// If this value is set to , this user will not be able to hear anyone speak in the guild. /// public Optional Deaf { get; set; } /// - /// Should the user have a nickname set? + /// Gets or sets the user's nickname. /// /// - /// To clear the user's nickname, this value can be set to or . + /// To clear the user's nickname, this value can be set to or + /// . /// public Optional Nickname { get; set; } /// - /// What roles should the user have? + /// Gets or sets the roles the user should have. /// /// - /// To add a role to a user: - /// To remove a role from a user: + /// + /// To add a role to a user: + /// + /// + /// + /// To remove a role from a user: + /// + /// /// public Optional> Roles { get; set; } /// - /// What roles should the user have? + /// Gets or sets the roles the user should have. /// /// - /// To add a role to a user: - /// To remove a role from a user: + /// + /// To add a role to a user: + /// + /// + /// + /// To remove a role from a user: + /// + /// /// public Optional> RoleIds { get; set; } /// - /// Move a user to a voice channel. + /// Moves a user to a voice channel. /// /// - /// This user MUST already be in a Voice Channel for this to work. + /// This user MUST already be in a for this to work. /// public Optional Channel { get; set; } /// - /// Move a user to a voice channel. + /// Moves a user to a voice channel. /// /// - /// This user MUST already be in a Voice Channel for this to work. + /// This user MUST already be in a for this to work. /// public Optional ChannelId { get; set; } } diff --git a/src/Discord.Net.Core/Entities/Users/IGroupUser.cs b/src/Discord.Net.Core/Entities/Users/IGroupUser.cs index dd046a5a8..ecf01f721 100644 --- a/src/Discord.Net.Core/Entities/Users/IGroupUser.cs +++ b/src/Discord.Net.Core/Entities/Users/IGroupUser.cs @@ -1,5 +1,8 @@ -namespace Discord +namespace Discord { + /// + /// Represents a Discord user that is in a group. + /// public interface IGroupUser : IUser, IVoiceState { ///// Kicks this user from this group. diff --git a/src/Discord.Net.Core/Entities/Users/IGuildUser.cs b/src/Discord.Net.Core/Entities/Users/IGuildUser.cs index 57cad1333..57093b3fd 100644 --- a/src/Discord.Net.Core/Entities/Users/IGuildUser.cs +++ b/src/Discord.Net.Core/Entities/Users/IGuildUser.cs @@ -1,41 +1,76 @@ -using System; +using System; using System.Collections.Generic; using System.Threading.Tasks; namespace Discord { - /// A Guild-User pairing. + /// + /// Represents a Discord user that is in a guild. + /// public interface IGuildUser : IUser, IVoiceState { - /// Gets when this user joined this guild. + /// + /// Gets when this user joined this guild. + /// DateTimeOffset? JoinedAt { get; } - /// Gets the nickname for this user. + /// + /// Gets the nickname for this user. + /// string Nickname { get; } - /// Gets the guild-level permissions for this user. + /// + /// Gets the guild-level permissions for this user. + /// GuildPermissions GuildPermissions { get; } - /// Gets the guild for this user. + /// + /// Gets the guild for this user. + /// IGuild Guild { get; } - /// Gets the id of the guild for this user. + /// + /// Gets the ID of the guild for this user. + /// ulong GuildId { get; } - /// Returns a collection of the ids of the roles this user is a member of in this guild, including the guild's @everyone role. + /// + /// Returns a collection of the ids of the roles this user is a member of in this guild, including the + /// guild's @everyone role. + /// IReadOnlyCollection RoleIds { get; } - /// Gets the level permissions granted to this user to a given channel. + /// + /// Gets the level permissions granted to this user to a given channel. + /// + /// The channel to get the permission from. ChannelPermissions GetPermissions(IGuildChannel channel); - /// Kicks this user from this guild. + /// + /// Kicks this user from this guild. + /// + /// The reason for the kick which will be recorded in the audit log. Task KickAsync(string reason = null, RequestOptions options = null); - /// Modifies this user's properties in this guild. + /// + /// Modifies this user's properties in this guild. + /// Task ModifyAsync(Action func, RequestOptions options = null); - /// Adds a role to this user in this guild. + /// + /// Adds a to this user in this guild. + /// + /// The role to be added to the user. Task AddRoleAsync(IRole role, RequestOptions options = null); - /// Adds roles to this user in this guild. + /// + /// Adds to this user in this guild. + /// + /// The roles to be added to the user. Task AddRolesAsync(IEnumerable roles, RequestOptions options = null); - /// Removes a role from this user in this guild. + /// + /// Removes a from this user in this guild. + /// + /// The role to be removed from the user. Task RemoveRoleAsync(IRole role, RequestOptions options = null); - /// Removes roles from this user in this guild. + /// + /// Removes from this user in this guild. + /// + /// The roles to be removed from the user. Task RemoveRolesAsync(IEnumerable roles, RequestOptions options = null); } } diff --git a/src/Discord.Net.Core/Entities/Users/IPresence.cs b/src/Discord.Net.Core/Entities/Users/IPresence.cs index 25adcc9c4..d40d47251 100644 --- a/src/Discord.Net.Core/Entities/Users/IPresence.cs +++ b/src/Discord.Net.Core/Entities/Users/IPresence.cs @@ -1,10 +1,17 @@ -namespace Discord +namespace Discord { + /// + /// Represents a Discord user's presence status. + /// public interface IPresence { - /// Gets the activity this user is currently doing. + /// + /// Gets the activity this user is currently doing. + /// IActivity Activity { get; } - /// Gets the current status of this user. + /// + /// Gets the current status of this user. + /// UserStatus Status { get; } } -} \ No newline at end of file +} diff --git a/src/Discord.Net.Core/Entities/Users/ISelfUser.cs b/src/Discord.Net.Core/Entities/Users/ISelfUser.cs index 7b91d4e3a..4a97c86ef 100644 --- a/src/Discord.Net.Core/Entities/Users/ISelfUser.cs +++ b/src/Discord.Net.Core/Entities/Users/ISelfUser.cs @@ -3,15 +3,27 @@ using System.Threading.Tasks; namespace Discord { + /// + /// Represents the logged-in Discord user. + /// public interface ISelfUser : IUser { - /// Gets the email associated with this user. + /// + /// Gets the email associated with this user. + /// string Email { get; } - /// Returns true if this user's email has been verified. + /// + /// Returns if this user's email has been verified. + /// bool IsVerified { get; } - /// Returns true if this user has enabled MFA on their account. + /// + /// Returns if this user has enabled MFA on their account. + /// bool IsMfaEnabled { get; } + /// + /// Modifies the user's properties. + /// Task ModifyAsync(Action func, RequestOptions options = null); } -} \ No newline at end of file +} diff --git a/src/Discord.Net.Core/Entities/Users/IUser.cs b/src/Discord.Net.Core/Entities/Users/IUser.cs index e3f270f6f..9b62be362 100644 --- a/src/Discord.Net.Core/Entities/Users/IUser.cs +++ b/src/Discord.Net.Core/Entities/Users/IUser.cs @@ -2,24 +2,48 @@ using System.Threading.Tasks; namespace Discord { + /// + /// Represents a Discord user. + /// public interface IUser : ISnowflakeEntity, IMentionable, IPresence { - /// Gets the id of this user's avatar. + /// + /// Gets the ID of this user's avatar. + /// string AvatarId { get; } - /// Gets the url to this user's avatar. + /// + /// Gets the URL to this user's avatar. + /// string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128); - /// Gets the per-username unique id for this user. + /// + /// Gets the URL to this user's default avatar. + /// + string GetDefaultAvatarUrl(); + /// + /// Gets the per-username unique ID for this user. + /// string Discriminator { get; } - /// Gets the per-username unique id for this user. + /// + /// Gets the per-username unique ID for this user. + /// ushort DiscriminatorValue { get; } - /// Returns true if this user is a bot user. + /// + /// Returns if this user is a bot user. + /// bool IsBot { get; } - /// Returns true if this user is a webhook user. + /// + /// Returns if this user is a webhook user. + /// bool IsWebhook { get; } - /// Gets the username for this user. + /// + /// Gets the username for this user. + /// string Username { get; } - /// Returns a private message channel to this user, creating one if it does not already exist. + /// + /// Returns a private message channel to this user, creating one if it does not already + /// exist. + /// Task GetOrCreateDMChannelAsync(RequestOptions options = null); } } diff --git a/src/Discord.Net.Core/Entities/Users/IVoiceState.cs b/src/Discord.Net.Core/Entities/Users/IVoiceState.cs index 428601f2a..ee1f74bae 100644 --- a/src/Discord.Net.Core/Entities/Users/IVoiceState.cs +++ b/src/Discord.Net.Core/Entities/Users/IVoiceState.cs @@ -1,20 +1,37 @@ -namespace Discord +namespace Discord { + /// + /// Represents a user's voice connection status. + /// public interface IVoiceState { - /// Returns true if the guild has deafened this user. + /// + /// Returns if the guild has deafened this user. + /// bool IsDeafened { get; } - /// Returns true if the guild has muted this user. + /// + /// Returns if the guild has muted this user. + /// bool IsMuted { get; } - /// Returns true if this user has marked themselves as deafened. + /// + /// Returns if this user has marked themselves as deafened. + /// bool IsSelfDeafened { get; } - /// Returns true if this user has marked themselves as muted. + /// + /// Returns if this user has marked themselves as muted. + /// bool IsSelfMuted { get; } - /// Returns true if the guild is temporarily blocking audio to/from this user. + /// + /// Returns if the guild is temporarily blocking audio to/from this user. + /// bool IsSuppressed { get; } - /// Gets the voice channel this user is currently in, if any. + /// + /// Gets the voice channel this user is currently in, if any. + /// IVoiceChannel VoiceChannel { get; } - /// Gets the unique identifier for this user's voice session. + /// + /// Gets the unique identifier for this user's voice session. + /// string VoiceSessionId { get; } } } diff --git a/src/Discord.Net.Core/Entities/Users/IWebhookUser.cs b/src/Discord.Net.Core/Entities/Users/IWebhookUser.cs index be769b944..7a10c6b6b 100644 --- a/src/Discord.Net.Core/Entities/Users/IWebhookUser.cs +++ b/src/Discord.Net.Core/Entities/Users/IWebhookUser.cs @@ -1,7 +1,9 @@ -namespace Discord +namespace Discord { + /// Represents a Webhook Discord user. public interface IWebhookUser : IGuildUser { + /// Gets the ID of a webhook. ulong WebhookId { get; } } } diff --git a/src/Discord.Net.Core/Entities/Users/SelfUserProperties.cs b/src/Discord.Net.Core/Entities/Users/SelfUserProperties.cs index 9c4162780..d79da0265 100644 --- a/src/Discord.Net.Core/Entities/Users/SelfUserProperties.cs +++ b/src/Discord.Net.Core/Entities/Users/SelfUserProperties.cs @@ -1,25 +1,25 @@ -namespace Discord +namespace Discord { /// - /// Modify the current user with the specified arguments + /// Properties that are used to modify the with the specified changes. /// /// - /// - /// await Context.Client.CurrentUser.ModifyAsync(x => - /// { - /// x.Avatar = new Image(File.OpenRead("avatar.jpg")); - /// }); - /// + /// + /// await Context.Client.CurrentUser.ModifyAsync(x => + /// { + /// x.Avatar = new Image(File.OpenRead("avatar.jpg")); + /// }); + /// /// - /// + /// public class SelfUserProperties { /// - /// Your username + /// Gets or sets the username. /// public Optional Username { get; set; } /// - /// Your avatar + /// Gets or sets the avatar. /// public Optional Avatar { get; set; } } diff --git a/src/Discord.Net.Core/Entities/Users/UserStatus.cs b/src/Discord.Net.Core/Entities/Users/UserStatus.cs index 74a52a0fa..09033261e 100644 --- a/src/Discord.Net.Core/Entities/Users/UserStatus.cs +++ b/src/Discord.Net.Core/Entities/Users/UserStatus.cs @@ -1,12 +1,33 @@ -namespace Discord +namespace Discord { + /// + /// Defines the available Discord user status. + /// public enum UserStatus { + /// + /// The user is offline. + /// Offline, + /// + /// The user is online. + /// Online, + /// + /// The user is idle. + /// Idle, + /// + /// The user is AFK. + /// AFK, + /// + /// The user is busy. + /// DoNotDisturb, + /// + /// The user is invisible. + /// Invisible, } } diff --git a/src/Discord.Net.Core/Entities/Webhooks/IWebhook.cs b/src/Discord.Net.Core/Entities/Webhooks/IWebhook.cs index ef56f72b9..b2d017316 100644 --- a/src/Discord.Net.Core/Entities/Webhooks/IWebhook.cs +++ b/src/Discord.Net.Core/Entities/Webhooks/IWebhook.cs @@ -3,32 +3,55 @@ using System.Threading.Tasks; namespace Discord { + /// + /// Represents a webhook object on Discord. + /// public interface IWebhook : IDeletable, ISnowflakeEntity { - /// Gets the token of this webhook. + /// + /// Gets the token of this webhook. + /// string Token { get; } - /// Gets the default name of this webhook. + /// + /// Gets the default name of this webhook. + /// string Name { get; } - /// Gets the id of this webhook's default avatar. + /// + /// Gets the ID of this webhook's default avatar. + /// string AvatarId { get; } - /// Gets the url to this webhook's default avatar. + /// + /// Gets the URL to this webhook's default avatar. + /// string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128); - /// Gets the channel for this webhook. + /// + /// Gets the channel for this webhook. + /// ITextChannel Channel { get; } - /// Gets the id of the channel for this webhook. + /// + /// Gets the ID of the channel for this webhook. + /// ulong ChannelId { get; } - /// Gets the guild owning this webhook. + /// + /// Gets the guild owning this webhook. + /// IGuild Guild { get; } - /// Gets the id of the guild owning this webhook. + /// + /// Gets the ID of the guild owning this webhook. + /// ulong? GuildId { get; } - /// Gets the user that created this webhook. + /// + /// Gets the user that created this webhook. + /// IUser Creator { get; } - /// Modifies this webhook. + /// + /// Modifies this webhook. + /// Task ModifyAsync(Action func, RequestOptions options = null); } } diff --git a/src/Discord.Net.Core/Entities/Webhooks/WebhookProperties.cs b/src/Discord.Net.Core/Entities/Webhooks/WebhookProperties.cs index 8759a1729..52adbe99a 100644 --- a/src/Discord.Net.Core/Entities/Webhooks/WebhookProperties.cs +++ b/src/Discord.Net.Core/Entities/Webhooks/WebhookProperties.cs @@ -1,40 +1,40 @@ -namespace Discord +namespace Discord { /// - /// Modify an with the specified parameters. + /// Properties used to modify an with the specified changes. /// /// - /// - /// await webhook.ModifyAsync(x => + /// + /// await webhook.ModifyAsync(x => /// { /// x.Name = "Bob"; /// x.Avatar = new Image("avatar.jpg"); /// }); /// /// - /// + /// public class WebhookProperties { /// - /// The default name of the webhook. + /// Gets or sets the default name of the webhook. /// public Optional Name { get; set; } /// - /// The default avatar of the webhook. + /// Gets or sets the default avatar of the webhook. /// public Optional Image { get; set; } /// - /// The channel for this webhook. + /// Gets or sets the channel for this webhook. /// /// - /// This field is not used when authenticated with . + /// This field is not used when authenticated with . /// public Optional Channel { get; set; } /// - /// The channel id for this webhook. + /// Gets or sets the channel ID for this webhook. /// /// - /// This field is not used when authenticated with . + /// This field is not used when authenticated with . /// public Optional ChannelId { get; set; } } diff --git a/src/Discord.Net.Core/Extensions/AsyncEnumerableExtensions.cs b/src/Discord.Net.Core/Extensions/AsyncEnumerableExtensions.cs index dd16d2943..56ed68cfe 100644 --- a/src/Discord.Net.Core/Extensions/AsyncEnumerableExtensions.cs +++ b/src/Discord.Net.Core/Extensions/AsyncEnumerableExtensions.cs @@ -4,19 +4,15 @@ using System.Threading.Tasks; namespace Discord { + /// An extension class for squashing . public static class AsyncEnumerableExtensions { - /// - /// Flattens the specified pages into one asynchronously - /// - /// - /// - /// + /// Flattens the specified pages into one asynchronously. public static async Task> FlattenAsync(this IAsyncEnumerable> source) { return await source.Flatten().ToArray().ConfigureAwait(false); } - + /// Flattens the specified pages into one . public static IAsyncEnumerable Flatten(this IAsyncEnumerable> source) { return source.SelectMany(enumerable => enumerable.ToAsyncEnumerable()); diff --git a/src/Discord.Net.Core/Extensions/DiscordClientExtensions.cs b/src/Discord.Net.Core/Extensions/DiscordClientExtensions.cs index ff3c7caf7..c38fa8e00 100644 --- a/src/Discord.Net.Core/Extensions/DiscordClientExtensions.cs +++ b/src/Discord.Net.Core/Extensions/DiscordClientExtensions.cs @@ -1,24 +1,31 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace Discord { + /// An extension class for the Discord client. public static class DiscordClientExtensions { + /// Gets the private channel with the provided ID. public static async Task GetPrivateChannelAsync(this IDiscordClient client, ulong id) => await client.GetChannelAsync(id).ConfigureAwait(false) as IPrivateChannel; + /// Gets the DM channel with the provided ID. public static async Task GetDMChannelAsync(this IDiscordClient client, ulong id) => await client.GetPrivateChannelAsync(id).ConfigureAwait(false) as IDMChannel; + /// Gets all available DM channels for the client. public static async Task> GetDMChannelsAsync(this IDiscordClient client) => (await client.GetPrivateChannelsAsync().ConfigureAwait(false)).Select(x => x as IDMChannel).Where(x => x != null); + /// Gets the group channel with the provided ID. public static async Task GetGroupChannelAsync(this IDiscordClient client, ulong id) => await client.GetPrivateChannelAsync(id).ConfigureAwait(false) as IGroupChannel; + /// Gets all available group channels for the client. public static async Task> GetGroupChannelsAsync(this IDiscordClient client) => (await client.GetPrivateChannelsAsync().ConfigureAwait(false)).Select(x => x as IGroupChannel).Where(x => x != null); + /// Gets the most optimal voice region for the client. public static async Task GetOptimalVoiceRegionAsync(this IDiscordClient discord) { var regions = await discord.GetVoiceRegionsAsync().ConfigureAwait(false); diff --git a/src/Discord.Net.Rest/Extensions/EmbedBuilderExtensions.cs b/src/Discord.Net.Core/Extensions/EmbedBuilderExtensions.cs similarity index 72% rename from src/Discord.Net.Rest/Extensions/EmbedBuilderExtensions.cs rename to src/Discord.Net.Core/Extensions/EmbedBuilderExtensions.cs index 2eb4ed473..b650aa401 100644 --- a/src/Discord.Net.Rest/Extensions/EmbedBuilderExtensions.cs +++ b/src/Discord.Net.Core/Extensions/EmbedBuilderExtensions.cs @@ -2,26 +2,34 @@ using System; namespace Discord { + /// An extension class for building an embed. public static class EmbedBuilderExtensions { + /// Adds embed color based on the provided raw value. public static EmbedBuilder WithColor(this EmbedBuilder builder, uint rawValue) => builder.WithColor(new Color(rawValue)); + /// Adds embed color based on the provided RGB value. public static EmbedBuilder WithColor(this EmbedBuilder builder, byte r, byte g, byte b) => builder.WithColor(new Color(r, g, b)); + /// Adds embed color based on the provided RGB value. public static EmbedBuilder WithColor(this EmbedBuilder builder, int r, int g, int b) => builder.WithColor(new Color(r, g, b)); + /// Adds embed color based on the provided RGB value. public static EmbedBuilder WithColor(this EmbedBuilder builder, float r, float g, float b) => builder.WithColor(new Color(r, g, b)); + /// Fills the embed author field with the provided user's full username and avatar URL. public static EmbedBuilder WithAuthor(this EmbedBuilder builder, IUser user) => builder.WithAuthor($"{user.Username}#{user.Discriminator}", user.GetAvatarUrl()); + /// Fills the embed author field with the provided user's nickname and avatar URL; username is used if nickname is not set. public static EmbedBuilder WithAuthor(this EmbedBuilder builder, IGuildUser user) => builder.WithAuthor($"{user.Nickname ?? user.Username}#{user.Discriminator}", user.GetAvatarUrl()); + /// Converts a object to a . public static EmbedBuilder ToEmbedBuilder(this IEmbed embed) { if (embed.Type != EmbedType.Rich) diff --git a/src/Discord.Net.Core/Extensions/UserExtensions.cs b/src/Discord.Net.Core/Extensions/UserExtensions.cs index 863201cfe..ad00296f7 100644 --- a/src/Discord.Net.Core/Extensions/UserExtensions.cs +++ b/src/Discord.Net.Core/Extensions/UserExtensions.cs @@ -3,11 +3,10 @@ using System.IO; namespace Discord { + /// An extension class for various Discord user objects. public static class UserExtensions { - /// - /// Sends a message to the user via DM. - /// + /// Sends a message to the user via DM. public static async Task SendMessageAsync(this IUser user, string text, bool isTTS = false, @@ -17,9 +16,7 @@ namespace Discord return await (await user.GetOrCreateDMChannelAsync().ConfigureAwait(false)).SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); } - /// - /// Sends a file to the user via DM. - /// + /// Sends a file to the user via DM. public static async Task SendFileAsync(this IUser user, Stream stream, string filename, @@ -33,9 +30,7 @@ namespace Discord } #if FILESYSTEM - /// - /// Sends a file to the user via DM. - /// + /// Sends a file to the user via DM. public static async Task SendFileAsync(this IUser user, string filePath, string text = null, @@ -46,5 +41,11 @@ namespace Discord return await (await user.GetOrCreateDMChannelAsync().ConfigureAwait(false)).SendFileAsync(filePath, text, isTTS, embed, options).ConfigureAwait(false); } #endif + /// Bans the provided user from the guild and optionally prunes their recent messages. + /// The user to ban. + /// The number of days to remove messages from this user for - must be between [0, 7] + /// The reason of the ban to be written in the audit log. + public static Task BanAsync(this IGuildUser user, int pruneDays = 0, string reason = null, RequestOptions options = null) + => user.Guild.AddBanAsync(user, pruneDays, reason, options); } } diff --git a/src/Discord.Net.Core/Format.cs b/src/Discord.Net.Core/Format.cs index aa822f99e..0b16daeb1 100644 --- a/src/Discord.Net.Core/Format.cs +++ b/src/Discord.Net.Core/Format.cs @@ -1,9 +1,10 @@ -namespace Discord +namespace Discord { + /// A helper class for formatting characters. public static class Format { // Characters which need escaping - private static string[] SensitiveCharacters = { "\\", "*", "_", "~", "`" }; + private static readonly string[] SensitiveCharacters = { "\\", "*", "_", "~", "`" }; /// Returns a markdown-formatted string with bold formatting. public static string Bold(string text) => $"**{text}**"; diff --git a/src/Discord.Net.Core/IDiscordClient.cs b/src/Discord.Net.Core/IDiscordClient.cs index 9abb959b5..a383c37da 100644 --- a/src/Discord.Net.Core/IDiscordClient.cs +++ b/src/Discord.Net.Core/IDiscordClient.cs @@ -36,5 +36,7 @@ namespace Discord Task GetVoiceRegionAsync(string id, RequestOptions options = null); Task GetWebhookAsync(ulong id, RequestOptions options = null); + + Task GetRecommendedShardCountAsync(RequestOptions options = null); } } diff --git a/src/Discord.Net.Core/Logging/LogMessage.cs b/src/Discord.Net.Core/Logging/LogMessage.cs index d1b3782be..7d6f8c13c 100644 --- a/src/Discord.Net.Core/Logging/LogMessage.cs +++ b/src/Discord.Net.Core/Logging/LogMessage.cs @@ -1,15 +1,38 @@ -using System; +using System; using System.Text; namespace Discord { + /// + /// Represents a message used for logging purposes. + /// public struct LogMessage { + /// + /// Gets the severity of the log message. + /// public LogSeverity Severity { get; } + /// + /// Gets the source of the log message. + /// public string Source { get; } + /// + /// Gets the message of the log message. + /// public string Message { get; } + /// + /// Gets the exception of the log message. + /// public Exception Exception { get; } + /// + /// Initializes a new with the severity, source, + /// of the event, and optionally, an exception. + /// + /// The severity of the event. + /// The source of the event. + /// The message of the event. + /// The exception of the event. public LogMessage(LogSeverity severity, string source, string message, Exception exception = null) { Severity = severity; @@ -17,8 +40,8 @@ namespace Discord Message = message; Exception = exception; } - - public override string ToString() => ToString(null); + + public override string ToString() => ToString(); public string ToString(StringBuilder builder = null, bool fullException = true, bool prependTimestamp = true, DateTimeKind timestampKind = DateTimeKind.Local, int? padSource = 11) { string sourceName = Source; diff --git a/src/Discord.Net.Core/Logging/LogSeverity.cs b/src/Discord.Net.Core/Logging/LogSeverity.cs index 785b0ef46..f9b518c17 100644 --- a/src/Discord.Net.Core/Logging/LogSeverity.cs +++ b/src/Discord.Net.Core/Logging/LogSeverity.cs @@ -1,12 +1,34 @@ -namespace Discord +namespace Discord { + /// + /// Specifies the severity of the log message. + /// public enum LogSeverity { + /// + /// Logs that contain the most severe level of error. This type of error indicate that immediate attention + /// may be required. + /// Critical = 0, + /// + /// Logs that highlight when the flow of execution is stopped due to a failure. + /// Error = 1, + /// + /// Logs that highlight an abnormal activity in the flow of execution. + /// Warning = 2, + /// + /// Logs that track the general flow of the application. + /// Info = 3, + /// + /// Logs that are used for interactive investigation during development. + /// Verbose = 4, + /// + /// Logs that contain the most detailed messages. + /// Debug = 5 } } diff --git a/src/Discord.Net.Core/LoginState.cs b/src/Discord.Net.Core/LoginState.cs index 42b6ecac9..49f86c9fa 100644 --- a/src/Discord.Net.Core/LoginState.cs +++ b/src/Discord.Net.Core/LoginState.cs @@ -1,10 +1,15 @@ -namespace Discord +namespace Discord { + /// Specifies the state of the client's login status. public enum LoginState : byte { + /// The client is currently logged out. LoggedOut, + /// The client is currently logging in. LoggingIn, + /// The client is currently logged in. LoggedIn, + /// The client is currently logging out. LoggingOut } } diff --git a/src/Discord.Net.Core/Net/HttpException.cs b/src/Discord.Net.Core/Net/HttpException.cs index d0ee65b23..3fabe0fa8 100644 --- a/src/Discord.Net.Core/Net/HttpException.cs +++ b/src/Discord.Net.Core/Net/HttpException.cs @@ -3,13 +3,31 @@ using System.Net; namespace Discord.Net { + /// + /// Describes an exception that occurred during the processing of Discord HTTP requests. + /// public class HttpException : Exception { + /// + /// Gets the HTTP status code returned by Discord. + /// public HttpStatusCode HttpCode { get; } + /// + /// Gets the JSON error code returned by Discord, or if none. + /// public int? DiscordCode { get; } + /// + /// Gets the reason of the exception. + /// public string Reason { get; } + /// + /// Gets the request object used to send the request. + /// public IRequest Request { get; } + /// + /// Initializes a new instance of the class. + /// public HttpException(HttpStatusCode httpCode, IRequest request, int? discordCode = null, string reason = null) : base(CreateMessage(httpCode, discordCode, reason)) { diff --git a/src/Discord.Net.Core/Net/RateLimitedException.cs b/src/Discord.Net.Core/Net/RateLimitedException.cs index 2d34d7bc2..969c0eafc 100644 --- a/src/Discord.Net.Core/Net/RateLimitedException.cs +++ b/src/Discord.Net.Core/Net/RateLimitedException.cs @@ -2,10 +2,20 @@ using System; namespace Discord.Net { + /// + /// An exception that indicates the user is being rate limited by Discord. + /// public class RateLimitedException : TimeoutException { + /// + /// Gets the request object used to send the request. + /// public IRequest Request { get; } + /// + /// Initializes a new instance of the class using the + /// sent. + /// public RateLimitedException(IRequest request) : base("You are being rate limited.") { diff --git a/src/Discord.Net.Core/Net/WebSocketClosedException.cs b/src/Discord.Net.Core/Net/WebSocketClosedException.cs index d647b6c8c..2936c01a9 100644 --- a/src/Discord.Net.Core/Net/WebSocketClosedException.cs +++ b/src/Discord.Net.Core/Net/WebSocketClosedException.cs @@ -1,11 +1,24 @@ -using System; +using System; namespace Discord.Net { + /// + /// Describes an exception that causes the WebSocket to close during a session. + /// public class WebSocketClosedException : Exception { + /// + /// Gets the close code sent by Discord. + /// public int CloseCode { get; } + /// + /// Gets the reason of the interruption. + /// public string Reason { get; } + /// + /// Initializes a new instance of the using the Discord close code + /// and the optional reason. + /// public WebSocketClosedException(int closeCode, string reason = null) : base($"The server sent close {closeCode}{(reason != null ? $": \"{reason}\"" : "")}") { diff --git a/src/Discord.Net.Core/RequestOptions.cs b/src/Discord.Net.Core/RequestOptions.cs index 5f3a8814b..2a03819cf 100644 --- a/src/Discord.Net.Core/RequestOptions.cs +++ b/src/Discord.Net.Core/RequestOptions.cs @@ -1,21 +1,32 @@ -using System.Threading; +using System.Threading; namespace Discord { + /// + /// Represents options that should be used when sending a request. + /// public class RequestOptions { + /// + /// Creates a new class with its default settings. + /// public static RequestOptions Default => new RequestOptions(); - /// - /// The max time, in milliseconds, to wait for this request to complete. If null, a request will not time out. - /// If a rate limit has been triggered for this request's bucket and will not be unpaused in time, this request will fail immediately. + /// + /// Gets or set the max time, in milliseconds, to wait for this request to complete. If + /// , a request will not time out. If a rate limit has been triggered for this + /// request's bucket and will not be unpaused in time, this request will fail immediately. /// public int? Timeout { get; set; } public CancellationToken CancelToken { get; set; } = CancellationToken.None; + /// + /// Gets or sets the retry behavior when the request fails. + /// public RetryMode? RetryMode { get; set; } public bool HeaderOnly { get; internal set; } /// - /// The reason for this action in the guild's audit log + /// Gets or sets the reason for this action in the guild's audit log. Note that this property may not apply + /// to every action. /// public string AuditLogReason { get; set; } @@ -31,11 +42,15 @@ namespace Discord return options.Clone(); } + /// + /// Initializes a new class with the default request timeout set in + /// . + /// public RequestOptions() { Timeout = DiscordConfig.DefaultRequestTimeout; } - + public RequestOptions Clone() => MemberwiseClone() as RequestOptions; } } diff --git a/src/Discord.Net.Core/TokenType.cs b/src/Discord.Net.Core/TokenType.cs index 62181420a..8ca3f031c 100644 --- a/src/Discord.Net.Core/TokenType.cs +++ b/src/Discord.Net.Core/TokenType.cs @@ -2,12 +2,22 @@ using System; namespace Discord { + /// Specifies the type of token to use with the client. public enum TokenType { [Obsolete("User logins are deprecated and may result in a ToS strike against your account - please see https://github.com/RogueException/Discord.Net/issues/827", error: true)] User, + /// + /// An OAuth2 token type. + /// Bearer, + /// + /// A bot token type. + /// Bot, + /// + /// A webhook token type. + /// Webhook } } diff --git a/src/Discord.Net.Core/Utils/Cacheable.cs b/src/Discord.Net.Core/Utils/Cacheable.cs index f17aa8699..2eb5a8542 100644 --- a/src/Discord.Net.Core/Utils/Cacheable.cs +++ b/src/Discord.Net.Core/Utils/Cacheable.cs @@ -1,30 +1,31 @@ -using System; +using System; using System.Threading.Tasks; namespace Discord { /// - /// Contains an entity that may be cached. + /// Represents a that contains an entity that may be cached. /// - /// The type of entity that is cached - /// The type of this entity's ID + /// The type of entity that is cached. + /// The type of this entity's ID. public struct Cacheable where TEntity : IEntity where TId : IEquatable { /// - /// Is this entity cached? + /// Gets whether this entity is cached. /// public bool HasValue { get; } /// - /// The ID of this entity. + /// Gets the ID of this entity. /// public TId Id { get; } /// - /// The entity, if it could be pulled from cache. + /// Gets the entity if it could be pulled from cache. /// /// - /// This value is not guaranteed to be set; in cases where the entity cannot be pulled from cache, it is null. + /// This value is not guaranteed to be set; in cases where the entity cannot be pulled from cache, it is + /// null. /// public TEntity Value { get; } private Func> DownloadFunc { get; } @@ -38,22 +39,26 @@ namespace Discord } /// - /// Downloads this entity to cache. + /// Downloads this entity to cache. /// - /// An awaitable Task containing the downloaded entity. /// Thrown when used from a user account. /// Thrown when the message is deleted. + /// + /// An awaitable containing the downloaded entity. + /// public async Task DownloadAsync() { - return await DownloadFunc(); + return await DownloadFunc().ConfigureAwait(false); } /// - /// Returns the cached entity if it exists; otherwise downloads it. + /// Returns the cached entity if it exists; otherwise downloads it. /// - /// An awaitable Task containing a cached or downloaded entity. /// Thrown when used from a user account. /// Thrown when the message is deleted and is not in cache. - public async Task GetOrDownloadAsync() => HasValue ? Value : await DownloadAsync(); + /// + /// An awaitable containing a cached or downloaded entity. + /// + public async Task GetOrDownloadAsync() => HasValue ? Value : await DownloadAsync().ConfigureAwait(false); } -} \ No newline at end of file +} diff --git a/src/Discord.Net.Core/Utils/Comparers.cs b/src/Discord.Net.Core/Utils/Comparers.cs index d7641e897..40500ffe3 100644 --- a/src/Discord.Net.Core/Utils/Comparers.cs +++ b/src/Discord.Net.Core/Utils/Comparers.cs @@ -3,13 +3,31 @@ using System.Collections.Generic; namespace Discord { + /// + /// Represents a collection of for various Discord objects. + /// public static class DiscordComparers { // TODO: simplify with '??=' slated for C# 8.0 + /// + /// Gets an to be used to compare users. + /// public static IEqualityComparer UserComparer => _userComparer ?? (_userComparer = new EntityEqualityComparer()); + /// + /// Gets an to be used to compare guilds. + /// public static IEqualityComparer GuildComparer => _guildComparer ?? (_guildComparer = new EntityEqualityComparer()); + /// + /// Gets an to be used to compare channels. + /// public static IEqualityComparer ChannelComparer => _channelComparer ?? (_channelComparer = new EntityEqualityComparer()); + /// + /// Gets an to be used to compare roles. + /// public static IEqualityComparer RoleComparer => _roleComparer ?? (_roleComparer = new EntityEqualityComparer()); + /// + /// Gets an to be used to compare messages. + /// public static IEqualityComparer MessageComparer => _messageComparer ?? (_messageComparer = new EntityEqualityComparer()); private static IEqualityComparer _userComparer; diff --git a/src/Discord.Net.Core/Utils/MentionUtils.cs b/src/Discord.Net.Core/Utils/MentionUtils.cs index 6c69827b4..36779de6e 100644 --- a/src/Discord.Net.Core/Utils/MentionUtils.cs +++ b/src/Discord.Net.Core/Utils/MentionUtils.cs @@ -1,29 +1,45 @@ -using System; +using System; using System.Globalization; using System.Text; namespace Discord { + /// + /// Represents a helper class for mention-related parsing. + /// public static class MentionUtils { private const char SanitizeChar = '\x200b'; //If the system can't be positive a user doesn't have a nickname, assume useNickname = true (source: Jake) internal static string MentionUser(string id, bool useNickname = true) => useNickname ? $"<@!{id}>" : $"<@{id}>"; + /// + /// Returns a mention string based on the user ID. + /// public static string MentionUser(ulong id) => MentionUser(id.ToString(), true); internal static string MentionChannel(string id) => $"<#{id}>"; + /// + /// Returns a mention string based on the channel ID. + /// public static string MentionChannel(ulong id) => MentionChannel(id.ToString()); - internal static string MentionRole(string id) => $"<@&{id}>"; + internal static string MentionRole(string id) => $"<@&{id}>"; + /// + /// Returns a mention string based on the role ID. + /// public static string MentionRole(ulong id) => MentionRole(id.ToString()); - /// Parses a provided user mention string. + /// + /// Parses a provided user mention string. + /// public static ulong ParseUser(string text) { if (TryParseUser(text, out ulong id)) return id; throw new ArgumentException("Invalid mention format", nameof(text)); } - /// Tries to parse a provided user mention string. + /// + /// Tries to parse a provided user mention string. + /// public static bool TryParseUser(string text, out ulong userId) { if (text.Length >= 3 && text[0] == '<' && text[1] == '@' && text[text.Length - 1] == '>') @@ -40,14 +56,18 @@ namespace Discord return false; } - /// Parses a provided channel mention string. + /// + /// Parses a provided channel mention string. + /// public static ulong ParseChannel(string text) { if (TryParseChannel(text, out ulong id)) return id; throw new ArgumentException("Invalid mention format", nameof(text)); } - /// Tries to parse a provided channel mention string. + /// + /// Tries to parse a provided channel mention string. + /// public static bool TryParseChannel(string text, out ulong channelId) { if (text.Length >= 3 && text[0] == '<' && text[1] == '#' && text[text.Length - 1] == '>') @@ -61,14 +81,18 @@ namespace Discord return false; } - /// Parses a provided role mention string. + /// + /// Parses a provided role mention string. + /// public static ulong ParseRole(string text) { if (TryParseRole(text, out ulong id)) return id; throw new ArgumentException("Invalid mention format", nameof(text)); } - /// Tries to parse a provided role mention string. + /// + /// Tries to parse a provided role mention string. + /// public static bool TryParseRole(string text, out ulong roleId) { if (text.Length >= 4 && text[0] == '<' && text[1] == '@' && text[2] == '&' && text[text.Length - 1] == '>') diff --git a/src/Discord.Net.Core/Utils/Optional.cs b/src/Discord.Net.Core/Utils/Optional.cs index eb3cbdca2..9284645f5 100644 --- a/src/Discord.Net.Core/Utils/Optional.cs +++ b/src/Discord.Net.Core/Utils/Optional.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Diagnostics; namespace Discord diff --git a/src/Discord.Net.Rest/API/Common/Game.cs b/src/Discord.Net.Rest/API/Common/Game.cs index 29e0d987d..4cde8444a 100644 --- a/src/Discord.Net.Rest/API/Common/Game.cs +++ b/src/Discord.Net.Rest/API/Common/Game.cs @@ -1,4 +1,4 @@ -#pragma warning disable CS1591 +#pragma warning disable CS1591 using Newtonsoft.Json; using Newtonsoft.Json.Serialization; using System.Runtime.Serialization; @@ -29,6 +29,10 @@ namespace Discord.API public Optional Timestamps { get; set; } [JsonProperty("instance")] public Optional Instance { get; set; } + [JsonProperty("sync_id")] + public Optional SyncId { get; set; } + [JsonProperty("session_id")] + public Optional SessionId { get; set; } [OnError] internal void OnError(StreamingContext context, ErrorContext errorContext) diff --git a/src/Discord.Net.Rest/API/Rest/UploadFileParams.cs b/src/Discord.Net.Rest/API/Rest/UploadFileParams.cs index 5c06a033e..9e909b50c 100644 --- a/src/Discord.Net.Rest/API/Rest/UploadFileParams.cs +++ b/src/Discord.Net.Rest/API/Rest/UploadFileParams.cs @@ -30,28 +30,24 @@ namespace Discord.API.Rest { var d = new Dictionary(); d["file"] = new MultipartFile(File, Filename.GetValueOrDefault("unknown.dat")); + + var payload = new Dictionary(); if (Content.IsSpecified) - d["content"] = Content.Value; + payload["content"] = Content.Value; if (IsTTS.IsSpecified) - d["tts"] = IsTTS.Value.ToString(); + payload["tts"] = IsTTS.Value.ToString(); if (Nonce.IsSpecified) - d["nonce"] = Nonce.Value; + payload["nonce"] = Nonce.Value; if (Embed.IsSpecified) - { - var payload = new StringBuilder(); - using (var text = new StringWriter(payload)) - using (var writer = new JsonTextWriter(text)) - { - var map = new Dictionary() - { - ["embed"] = Embed.Value, - }; - - _serializer.Serialize(writer, map); - } - d["payload_json"] = payload.ToString(); - - } + payload["embed"] = Embed.Value; + + var json = new StringBuilder(); + using (var text = new StringWriter(json)) + using (var writer = new JsonTextWriter(text)) + _serializer.Serialize(writer, payload); + + d["payload_json"] = json.ToString(); + return d; } } diff --git a/src/Discord.Net.Rest/AssemblyInfo.cs b/src/Discord.Net.Rest/AssemblyInfo.cs index a4f045ab5..126365e47 100644 --- a/src/Discord.Net.Rest/AssemblyInfo.cs +++ b/src/Discord.Net.Rest/AssemblyInfo.cs @@ -1,7 +1,10 @@ -using System.Runtime.CompilerServices; +using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Discord.Net.Rpc")] [assembly: InternalsVisibleTo("Discord.Net.WebSocket")] [assembly: InternalsVisibleTo("Discord.Net.Webhook")] [assembly: InternalsVisibleTo("Discord.Net.Commands")] -[assembly: InternalsVisibleTo("Discord.Net.Tests")] \ No newline at end of file +[assembly: InternalsVisibleTo("Discord.Net.Tests")] + +[assembly: TypeForwardedTo(typeof(Discord.EmbedBuilder))] +[assembly: TypeForwardedTo(typeof(Discord.EmbedBuilderExtensions))] diff --git a/src/Discord.Net.Rest/BaseDiscordClient.cs b/src/Discord.Net.Rest/BaseDiscordClient.cs index 269dedd71..f8642b96c 100644 --- a/src/Discord.Net.Rest/BaseDiscordClient.cs +++ b/src/Discord.Net.Rest/BaseDiscordClient.cs @@ -1,4 +1,4 @@ -using Discord.Logging; +using Discord.Logging; using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -125,6 +125,10 @@ namespace Discord.Rest /// public void Dispose() => Dispose(true); + /// + public Task GetRecommendedShardCountAsync(RequestOptions options = null) + => ClientHelper.GetRecommendShardCountAsync(this, options); + //IDiscordClient ConnectionState IDiscordClient.ConnectionState => ConnectionState.Disconnected; ISelfUser IDiscordClient.CurrentUser => CurrentUser; diff --git a/src/Discord.Net.Rest/ClientHelper.cs b/src/Discord.Net.Rest/ClientHelper.cs index 5c9e26433..73e1a08f0 100644 --- a/src/Discord.Net.Rest/ClientHelper.cs +++ b/src/Discord.Net.Rest/ClientHelper.cs @@ -1,4 +1,4 @@ -using Discord.API.Rest; +using Discord.API.Rest; using System.Collections.Generic; using System.Collections.Immutable; using System.IO; @@ -146,7 +146,7 @@ namespace Discord.Rest public static async Task GetWebhookAsync(BaseDiscordClient client, ulong id, RequestOptions options) { - var model = await client.ApiClient.GetWebhookAsync(id); + var model = await client.ApiClient.GetWebhookAsync(id).ConfigureAwait(false); if (model != null) return RestWebhook.Create(client, (IGuild)null, model); return null; @@ -163,5 +163,11 @@ namespace Discord.Rest var models = await client.ApiClient.GetVoiceRegionsAsync(options).ConfigureAwait(false); return models.Select(x => RestVoiceRegion.Create(client, x)).FirstOrDefault(x => x.Id == id); } + + public static async Task GetRecommendShardCountAsync(BaseDiscordClient client, RequestOptions options) + { + var response = await client.ApiClient.GetBotGatewayAsync(options).ConfigureAwait(false); + return response.Shards; + } } } diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index c2176ca60..c8ff616e2 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -269,6 +269,18 @@ namespace Discord.API await SendAsync("GET", () => "auth/login", new BucketIds(), options: options).ConfigureAwait(false); } + //Gateway + public async Task GetGatewayAsync(RequestOptions options = null) + { + options = RequestOptions.CreateOrClone(options); + return await SendAsync("GET", () => "gateway", new BucketIds(), options: options).ConfigureAwait(false); + } + public async Task GetBotGatewayAsync(RequestOptions options = null) + { + options = RequestOptions.CreateOrClone(options); + return await SendAsync("GET", () => "gateway/bot", new BucketIds(), options: options).ConfigureAwait(false); + } + //Channels public async Task GetChannelAsync(ulong channelId, RequestOptions options = null) { @@ -389,7 +401,7 @@ namespace Discord.API options = RequestOptions.CreateOrClone(options); var ids = new BucketIds(guildId: guildId); - await SendAsync("PUT", () => $"guilds/{guildId}/members/{userId}/roles/{roleId}", ids, options: options); + await SendAsync("PUT", () => $"guilds/{guildId}/members/{userId}/roles/{roleId}", ids, options: options).ConfigureAwait(false); } public async Task RemoveRoleAsync(ulong guildId, ulong userId, ulong roleId, RequestOptions options = null) { @@ -400,7 +412,7 @@ namespace Discord.API options = RequestOptions.CreateOrClone(options); var ids = new BucketIds(guildId: guildId); - await SendAsync("DELETE", () => $"guilds/{guildId}/members/{userId}/roles/{roleId}", ids, options: options); + await SendAsync("DELETE", () => $"guilds/{guildId}/members/{userId}/roles/{roleId}", ids, options: options).ConfigureAwait(false); } //Channel Messages @@ -458,7 +470,7 @@ namespace Discord.API if (!args.Embed.IsSpecified || args.Embed.Value == null) Preconditions.NotNullOrEmpty(args.Content, nameof(args.Content)); - if (args.Content.Length > DiscordConfig.MaxMessageSize) + if (args.Content?.Length > DiscordConfig.MaxMessageSize) throw new ArgumentException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content)); options = RequestOptions.CreateOrClone(options); @@ -475,7 +487,7 @@ namespace Discord.API if (!args.Embeds.IsSpecified || args.Embeds.Value == null || args.Embeds.Value.Length == 0) Preconditions.NotNullOrEmpty(args.Content, nameof(args.Content)); - if (args.Content.Length > DiscordConfig.MaxMessageSize) + if (args.Content?.Length > DiscordConfig.MaxMessageSize) throw new ArgumentException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content)); options = RequestOptions.CreateOrClone(options); @@ -556,7 +568,7 @@ namespace Discord.API { if (!args.Embed.IsSpecified) Preconditions.NotNullOrEmpty(args.Content, nameof(args.Content)); - if (args.Content.Value.Length > DiscordConfig.MaxMessageSize) + if (args.Content.Value?.Length > DiscordConfig.MaxMessageSize) throw new ArgumentOutOfRangeException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content)); } options = RequestOptions.CreateOrClone(options); @@ -1066,7 +1078,7 @@ namespace Discord.API options = RequestOptions.CreateOrClone(options); var ids = new BucketIds(guildId: guildId); - return await SendAsync("GET", () => $"guilds/{guildId}/emojis/{emoteId}", ids, options: options); + return await SendAsync("GET", () => $"guilds/{guildId}/emojis/{emoteId}", ids, options: options).ConfigureAwait(false); } public async Task CreateGuildEmoteAsync(ulong guildId, Rest.CreateGuildEmoteParams args, RequestOptions options = null) @@ -1078,7 +1090,7 @@ namespace Discord.API options = RequestOptions.CreateOrClone(options); var ids = new BucketIds(guildId: guildId); - return await SendJsonAsync("POST", () => $"guilds/{guildId}/emojis", args, ids, options: options); + return await SendJsonAsync("POST", () => $"guilds/{guildId}/emojis", args, ids, options: options).ConfigureAwait(false); } public async Task ModifyGuildEmoteAsync(ulong guildId, ulong emoteId, ModifyGuildEmoteParams args, RequestOptions options = null) @@ -1089,7 +1101,7 @@ namespace Discord.API options = RequestOptions.CreateOrClone(options); var ids = new BucketIds(guildId: guildId); - return await SendJsonAsync("PATCH", () => $"guilds/{guildId}/emojis/{emoteId}", args, ids, options: options); + return await SendJsonAsync("PATCH", () => $"guilds/{guildId}/emojis/{emoteId}", args, ids, options: options).ConfigureAwait(false); } public async Task DeleteGuildEmoteAsync(ulong guildId, ulong emoteId, RequestOptions options = null) @@ -1099,7 +1111,7 @@ namespace Discord.API options = RequestOptions.CreateOrClone(options); var ids = new BucketIds(guildId: guildId); - await SendAsync("DELETE", () => $"guilds/{guildId}/emojis/{emoteId}", ids, options: options); + await SendAsync("DELETE", () => $"guilds/{guildId}/emojis/{emoteId}", ids, options: options).ConfigureAwait(false); } //Users @@ -1199,7 +1211,7 @@ namespace Discord.API options = RequestOptions.CreateOrClone(options); var ids = new BucketIds(channelId: channelId); - return await SendJsonAsync("POST", () => $"channels/{channelId}/webhooks", args, ids, options: options); + return await SendJsonAsync("POST", () => $"channels/{channelId}/webhooks", args, ids, options: options).ConfigureAwait(false); } public async Task GetWebhookAsync(ulong webhookId, RequestOptions options = null) { diff --git a/src/Discord.Net.Rest/DiscordRestClient.cs b/src/Discord.Net.Rest/DiscordRestClient.cs index 3d90b6c00..2dc3ebe05 100644 --- a/src/Discord.Net.Rest/DiscordRestClient.cs +++ b/src/Discord.Net.Rest/DiscordRestClient.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Collections.Immutable; using System.IO; using System.Threading.Tasks; @@ -37,7 +37,7 @@ namespace Discord.Rest /// public async Task GetApplicationInfoAsync(RequestOptions options = null) { - return _applicationInfo ?? (_applicationInfo = await ClientHelper.GetApplicationInfoAsync(this, options)); + return _applicationInfo ?? (_applicationInfo = await ClientHelper.GetApplicationInfoAsync(this, options).ConfigureAwait(false)); } /// @@ -165,6 +165,6 @@ namespace Discord.Rest => await GetVoiceRegionAsync(id, options).ConfigureAwait(false); async Task IDiscordClient.GetWebhookAsync(ulong id, RequestOptions options) - => await GetWebhookAsync(id, options); + => await GetWebhookAsync(id, options).ConfigureAwait(false); } } diff --git a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs index 710746896..6784f7f6a 100644 --- a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs +++ b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs @@ -180,7 +180,7 @@ namespace Discord.Rest public static async Task SendFileAsync(IMessageChannel channel, BaseDiscordClient client, Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options) { - var args = new UploadFileParams(stream) { Filename = filename, Content = text, IsTTS = isTTS, Embed = embed?.ToModel() }; + var args = new UploadFileParams(stream) { Filename = filename, Content = text, IsTTS = isTTS, Embed = embed != null ? embed.ToModel() : Optional.Unspecified }; var model = await client.ApiClient.UploadFileAsync(channel.Id, args, options).ConfigureAwait(false); return RestUserMessage.Create(client, channel, client.CurrentUser, model); } diff --git a/src/Discord.Net.Rest/Entities/Channels/ChannelType.cs b/src/Discord.Net.Rest/Entities/Channels/ChannelType.cs index e9f069a50..7759622c2 100644 --- a/src/Discord.Net.Rest/Entities/Channels/ChannelType.cs +++ b/src/Discord.Net.Rest/Entities/Channels/ChannelType.cs @@ -1,11 +1,17 @@ -namespace Discord +namespace Discord { + /// Defines the types of channels. public enum ChannelType { + /// The channel is a text channel. Text = 0, + /// The channel is a Direct Message channel. DM = 1, + /// The channel is a voice channel. Voice = 2, + /// The channel is a group channel. Group = 3, + /// The channel is a category channel. Category = 4 } } diff --git a/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs b/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs index 8ab6e9819..8668d890a 100644 --- a/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs @@ -4,26 +4,45 @@ using System.Threading.Tasks; namespace Discord.Rest { + /// + /// Represents a REST channel that can send and receive messages. + /// public interface IRestMessageChannel : IMessageChannel { - /// Sends a message to this message channel. + /// + /// Sends a message to this message channel. + /// new Task SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null); #if FILESYSTEM - /// Sends a file to this text channel, with an optional caption. + /// + /// Sends a file to this message channel, with an optional caption. + /// new Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null); #endif - /// Sends a file to this text channel, with an optional caption. + /// + /// Sends a file to this message channel, with an optional caption. + /// new Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null); - /// Gets a message from this message channel with the given id, or null if not found. + /// + /// Gets a message from this message channel with the given ID, or if not found. + /// Task GetMessageAsync(ulong id, RequestOptions options = null); - /// Gets the last N messages from this message channel. + /// + /// Gets the last N messages from this message channel. + /// IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null); - /// Gets a collection of messages in this channel. + /// + /// Gets a collection of messages in this channel. + /// IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null); - /// Gets a collection of messages in this channel. + /// + /// Gets a collection of messages in this channel. + /// IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null); - /// Gets a collection of pinned messages in this channel. + /// + /// Gets a collection of pinned messages in this channel. + /// new Task> GetPinnedMessagesAsync(RequestOptions options = null); } } diff --git a/src/Discord.Net.Rest/Entities/Channels/IRestPrivateChannel.cs b/src/Discord.Net.Rest/Entities/Channels/IRestPrivateChannel.cs index a6939f81e..628dcb776 100644 --- a/src/Discord.Net.Rest/Entities/Channels/IRestPrivateChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/IRestPrivateChannel.cs @@ -1,9 +1,15 @@ -using System.Collections.Generic; +using System.Collections.Generic; namespace Discord.Rest { + /// + /// Represents a REST channel that is private to select recipients. + /// public interface IRestPrivateChannel : IPrivateChannel { + /// + /// Users that can access this channel. + /// new IReadOnlyCollection Recipients { get; } } } diff --git a/src/Discord.Net.Rest/Entities/Channels/RestCategoryChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestCategoryChannel.cs index 397e14e76..1700999d9 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestCategoryChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestCategoryChannel.cs @@ -1,13 +1,14 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; -using System.IO; -using System.Linq; using System.Threading.Tasks; using Model = Discord.API.Channel; namespace Discord.Rest { + /// + /// Represents a REST category channel. + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class RestCategoryChannel : RestGuildChannel, ICategoryChannel { diff --git a/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs index 08acdf32b..ca71e719b 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs @@ -9,11 +9,14 @@ using Model = Discord.API.Channel; namespace Discord.Rest { + /// + /// Represents a REST DM channel. + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public class RestDMChannel : RestChannel, IDMChannel, IRestPrivateChannel, IRestMessageChannel, IUpdateable + public class RestDMChannel : RestChannel, IDMChannel, IRestPrivateChannel, IRestMessageChannel { - public RestUser CurrentUser { get; private set; } - public RestUser Recipient { get; private set; } + public RestUser CurrentUser { get; } + public RestUser Recipient { get; } public IReadOnlyCollection Users => ImmutableArray.Create(CurrentUser, Recipient); @@ -39,6 +42,7 @@ namespace Discord.Rest var model = await Discord.ApiClient.GetChannelAsync(Id, options).ConfigureAwait(false); Update(model); } + /// public Task CloseAsync(RequestOptions options = null) => ChannelHelper.DeleteAsync(this, Discord, options); @@ -52,26 +56,35 @@ namespace Discord.Rest return null; } + /// public Task GetMessageAsync(ulong id, RequestOptions options = null) => ChannelHelper.GetMessageAsync(this, Discord, id, options); + /// public IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) => ChannelHelper.GetMessagesAsync(this, Discord, null, Direction.Before, limit, options); + /// public IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) => ChannelHelper.GetMessagesAsync(this, Discord, fromMessageId, dir, limit, options); + /// public IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) => ChannelHelper.GetMessagesAsync(this, Discord, fromMessage.Id, dir, limit, options); + /// public Task> GetPinnedMessagesAsync(RequestOptions options = null) => ChannelHelper.GetPinnedMessagesAsync(this, Discord, options); + /// public Task SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); #if FILESYSTEM + /// public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options); #endif + /// public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options); + /// public Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); public IDisposable EnterTypingState(RequestOptions options = null) diff --git a/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs index a1868573e..81c1b85eb 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs @@ -11,11 +11,12 @@ using Model = Discord.API.Channel; namespace Discord.Rest { [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public class RestGroupChannel : RestChannel, IGroupChannel, IRestPrivateChannel, IRestMessageChannel, IRestAudioChannel, IUpdateable + public class RestGroupChannel : RestChannel, IGroupChannel, IRestPrivateChannel, IRestMessageChannel, IRestAudioChannel { private string _iconId; private ImmutableDictionary _users; + /// public string Name { get; private set; } public IReadOnlyCollection Users => _users.ToReadOnlyCollection(); @@ -49,12 +50,13 @@ namespace Discord.Rest users[models[i].Id] = RestGroupUser.Create(Discord, models[i]); _users = users.ToImmutable(); } - + /// public override async Task UpdateAsync(RequestOptions options = null) { var model = await Discord.ApiClient.GetChannelAsync(Id, options).ConfigureAwait(false); Update(model); } + /// public Task LeaveAsync(RequestOptions options = null) => ChannelHelper.DeleteAsync(this, Discord, options); @@ -65,26 +67,35 @@ namespace Discord.Rest return null; } + /// public Task GetMessageAsync(ulong id, RequestOptions options = null) => ChannelHelper.GetMessageAsync(this, Discord, id, options); + /// public IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) => ChannelHelper.GetMessagesAsync(this, Discord, null, Direction.Before, limit, options); + /// public IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) => ChannelHelper.GetMessagesAsync(this, Discord, fromMessageId, dir, limit, options); + /// public IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) => ChannelHelper.GetMessagesAsync(this, Discord, fromMessage.Id, dir, limit, options); + /// public Task> GetPinnedMessagesAsync(RequestOptions options = null) => ChannelHelper.GetPinnedMessagesAsync(this, Discord, options); + /// public Task SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); #if FILESYSTEM + /// public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options); #endif + /// public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options); + /// public Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); public IDisposable EnterTypingState(RequestOptions options = null) diff --git a/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs index 026d03cc8..c8eeabf00 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; @@ -7,16 +7,24 @@ using Model = Discord.API.Channel; namespace Discord.Rest { - public class RestGuildChannel : RestChannel, IGuildChannel, IUpdateable + /// + /// Represents a private REST group channel. + /// + public class RestGuildChannel : RestChannel, IGuildChannel { private ImmutableArray _overwrites; + /// public IReadOnlyCollection PermissionOverwrites => _overwrites; internal IGuild Guild { get; } + /// public string Name { get; private set; } + /// public int Position { get; private set; } + /// public ulong? CategoryId { get; private set; } + /// public ulong GuildId => Guild.Id; internal RestGuildChannel(BaseDiscordClient discord, IGuild guild, ulong id) @@ -51,19 +59,23 @@ namespace Discord.Rest _overwrites = newOverwrites.ToImmutable(); } + /// public override async Task UpdateAsync(RequestOptions options = null) { var model = await Discord.ApiClient.GetChannelAsync(GuildId, Id, options).ConfigureAwait(false); Update(model); } + /// public async Task ModifyAsync(Action func, RequestOptions options = null) { var model = await ChannelHelper.ModifyAsync(this, Discord, func, options).ConfigureAwait(false); Update(model); } + /// public Task DeleteAsync(RequestOptions options = null) => ChannelHelper.DeleteAsync(this, Discord, options); + /// public async Task GetCategoryAsync() { if (CategoryId.HasValue) diff --git a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs index 600b197d6..9752697d6 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs @@ -88,11 +88,11 @@ namespace Discord.Rest //ITextChannel async Task ITextChannel.CreateWebhookAsync(string name, Stream avatar, RequestOptions options) - => await CreateWebhookAsync(name, avatar, options); + => await CreateWebhookAsync(name, avatar, options).ConfigureAwait(false); async Task ITextChannel.GetWebhookAsync(ulong id, RequestOptions options) - => await GetWebhookAsync(id, options); + => await GetWebhookAsync(id, options).ConfigureAwait(false); async Task> ITextChannel.GetWebhooksAsync(RequestOptions options) - => await GetWebhooksAsync(options); + => await GetWebhooksAsync(options).ConfigureAwait(false); //IMessageChannel async Task IMessageChannel.GetMessageAsync(ulong id, CacheMode mode, RequestOptions options) diff --git a/src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs index 3b6a68193..049f86c08 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs @@ -53,7 +53,7 @@ namespace Discord.Rest async Task IMessageChannel.GetMessageAsync(ulong id, CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) - return await GetMessageAsync(id, options); + return await GetMessageAsync(id, options).ConfigureAwait(false); else return null; } @@ -79,28 +79,27 @@ namespace Discord.Rest return AsyncEnumerable.Empty>(); } async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) - => await GetPinnedMessagesAsync(options); + => await GetPinnedMessagesAsync(options).ConfigureAwait(false); #if FILESYSTEM async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options) - => await SendFileAsync(filePath, text, isTTS, embed, options); + => await SendFileAsync(filePath, text, isTTS, embed, options).ConfigureAwait(false); #endif async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options) - => await SendFileAsync(stream, filename, text, isTTS, embed, options); + => await SendFileAsync(stream, filename, text, isTTS, embed, options).ConfigureAwait(false); async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) - => await SendMessageAsync(text, isTTS, embed, options); + => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); IDisposable IMessageChannel.EnterTypingState(RequestOptions options) => EnterTypingState(options); //IChannel - string IChannel.Name { get { throw new NotSupportedException(); } } - IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) - { + string IChannel.Name => throw new NotSupportedException(); - } - Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) - { + + IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) => + throw new NotSupportedException(); + + Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => throw new NotSupportedException(); - } } } diff --git a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs index 12fdb075d..38c5fe9e2 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs @@ -237,7 +237,7 @@ namespace Discord.Rest }; if (info.Position != null) args.AfterUserId = info.Position.Value; - var models = await client.ApiClient.GetGuildMembersAsync(guild.Id, args, options); + var models = await client.ApiClient.GetGuildMembersAsync(guild.Id, args, options).ConfigureAwait(false); return models.Select(x => RestGuildUser.Create(client, guild, x)).ToImmutableArray(); }, nextPage: (info, lastPage) => @@ -280,7 +280,7 @@ namespace Discord.Rest //Emotes public static async Task GetEmoteAsync(IGuild guild, BaseDiscordClient client, ulong id, RequestOptions options) { - var emote = await client.ApiClient.GetGuildEmoteAsync(guild.Id, id, options); + var emote = await client.ApiClient.GetGuildEmoteAsync(guild.Id, id, options).ConfigureAwait(false); return emote.ToEntity(); } public static async Task CreateEmoteAsync(IGuild guild, BaseDiscordClient client, string name, Image image, Optional> roles, @@ -294,7 +294,7 @@ namespace Discord.Rest if (roles.IsSpecified) apiargs.RoleIds = roles.Value?.Select(xr => xr.Id)?.ToArray(); - var emote = await client.ApiClient.CreateGuildEmoteAsync(guild.Id, apiargs, options); + var emote = await client.ApiClient.CreateGuildEmoteAsync(guild.Id, apiargs, options).ConfigureAwait(false); return emote.ToEntity(); } public static async Task ModifyEmoteAsync(IGuild guild, BaseDiscordClient client, ulong id, Action func, @@ -312,7 +312,7 @@ namespace Discord.Rest if (props.Roles.IsSpecified) apiargs.RoleIds = props.Roles.Value?.Select(xr => xr.Id)?.ToArray(); - var emote = await client.ApiClient.ModifyGuildEmoteAsync(guild.Id, id, apiargs, options); + var emote = await client.ApiClient.ModifyGuildEmoteAsync(guild.Id, id, apiargs, options).ConfigureAwait(false); return emote.ToEntity(); } public static Task DeleteEmoteAsync(IGuild guild, BaseDiscordClient client, ulong id, RequestOptions options) diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs index 5d12731a6..15d106804 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -121,7 +121,7 @@ namespace Discord.Rest public async Task ReorderChannelsAsync(IEnumerable args, RequestOptions options = null) { var arr = args.ToArray(); - await GuildHelper.ReorderChannelsAsync(this, Discord, arr, options); + await GuildHelper.ReorderChannelsAsync(this, Discord, arr, options).ConfigureAwait(false); } public async Task ReorderRolesAsync(IEnumerable args, RequestOptions options = null) { @@ -420,8 +420,8 @@ namespace Discord.Rest Task IGuild.DownloadUsersAsync() { throw new NotSupportedException(); } async Task IGuild.GetWebhookAsync(ulong id, RequestOptions options) - => await GetWebhookAsync(id, options); + => await GetWebhookAsync(id, options).ConfigureAwait(false); async Task> IGuild.GetWebhooksAsync(RequestOptions options) - => await GetWebhooksAsync(options); + => await GetWebhooksAsync(options).ConfigureAwait(false); } } diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestUserGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestUserGuild.cs index 6bc9cea7a..de5a5f7d9 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestUserGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestUserGuild.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Diagnostics; using System.Threading.Tasks; using Model = Discord.API.UserGuild; @@ -6,7 +6,7 @@ using Model = Discord.API.UserGuild; namespace Discord.Rest { [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public class RestUserGuild : RestEntity, ISnowflakeEntity, IUserGuild + public class RestUserGuild : RestEntity, IUserGuild { private string _iconId; diff --git a/src/Discord.Net.Rest/Entities/Invites/RestInvite.cs b/src/Discord.Net.Rest/Entities/Invites/RestInvite.cs index 900d1f0ac..6df97f6f7 100644 --- a/src/Discord.Net.Rest/Entities/Invites/RestInvite.cs +++ b/src/Discord.Net.Rest/Entities/Invites/RestInvite.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Diagnostics; using System.Threading.Tasks; using Model = Discord.API.Invite; @@ -8,14 +8,20 @@ namespace Discord.Rest [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class RestInvite : RestEntity, IInvite, IUpdateable { + /// public string ChannelName { get; private set; } + /// public string GuildName { get; private set; } + /// public ulong ChannelId { get; private set; } + /// public ulong GuildId { get; private set; } - internal IChannel Channel { get; private set; } - internal IGuild Guild { get; private set; } + internal IChannel Channel { get; } + internal IGuild Guild { get; } + /// public string Code => Id; + /// public string Url => $"{DiscordConfig.InviteUrl}{Code}"; internal RestInvite(BaseDiscordClient discord, IGuild guild, IChannel channel, string id) @@ -37,15 +43,17 @@ namespace Discord.Rest GuildName = model.Guild.Name; ChannelName = model.Channel.Name; } - + + /// public async Task UpdateAsync(RequestOptions options = null) { var model = await Discord.ApiClient.GetInviteAsync(Code, options).ConfigureAwait(false); Update(model); } + /// public Task DeleteAsync(RequestOptions options = null) => InviteHelper.DeleteAsync(this, Discord, options); - + /// public Task AcceptAsync(RequestOptions options = null) => InviteHelper.AcceptAsync(this, Discord, options); diff --git a/src/Discord.Net.Rest/Entities/Invites/RestInviteMetadata.cs b/src/Discord.Net.Rest/Entities/Invites/RestInviteMetadata.cs index 42aeb40aa..2bb5ed209 100644 --- a/src/Discord.Net.Rest/Entities/Invites/RestInviteMetadata.cs +++ b/src/Discord.Net.Rest/Entities/Invites/RestInviteMetadata.cs @@ -1,19 +1,29 @@ -using System; +using System; using Model = Discord.API.InviteMetadata; namespace Discord.Rest { + /// Represents additional information regarding the REST invite object. public class RestInviteMetadata : RestInvite, IInviteMetadata { private long _createdAtTicks; + /// public bool IsRevoked { get; private set; } + /// public bool IsTemporary { get; private set; } + /// public int? MaxAge { get; private set; } + /// public int? MaxUses { get; private set; } + /// public int Uses { get; private set; } + /// + /// Gets the user that created this invite. + /// public RestUser Inviter { get; private set; } + /// public DateTimeOffset CreatedAt => DateTimeUtils.FromTicks(_createdAtTicks); internal RestInviteMetadata(BaseDiscordClient discord, IGuild guild, IChannel channel, string id) diff --git a/src/Discord.Net.Rest/Entities/Messages/Attachment.cs b/src/Discord.Net.Rest/Entities/Messages/Attachment.cs index e185234ac..a51ac8e09 100644 --- a/src/Discord.Net.Rest/Entities/Messages/Attachment.cs +++ b/src/Discord.Net.Rest/Entities/Messages/Attachment.cs @@ -1,17 +1,25 @@ -using System.Diagnostics; +using System.Diagnostics; using Model = Discord.API.Attachment; namespace Discord { + /// A Discord attachment. [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class Attachment : IAttachment { + /// public ulong Id { get; } + /// public string Filename { get; } + /// public string Url { get; } + /// public string ProxyUrl { get; } + /// public int Size { get; } + /// public int? Height { get; } + /// public int? Width { get; } internal Attachment(ulong id, string filename, string url, string proxyUrl, int size, int? height, int? width) @@ -31,6 +39,7 @@ namespace Discord model.Width.IsSpecified ? model.Width.Value : (int?)null); } + /// Returns the filename of the attachment. public override string ToString() => Filename; private string DebuggerDisplay => $"{Filename} ({Size} bytes)"; } diff --git a/src/Discord.Net.Rest/Entities/Messages/EmbedBuilder.cs b/src/Discord.Net.Rest/Entities/Messages/EmbedBuilder.cs deleted file mode 100644 index f5663cea3..000000000 --- a/src/Discord.Net.Rest/Entities/Messages/EmbedBuilder.cs +++ /dev/null @@ -1,379 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Immutable; - -namespace Discord -{ - public class EmbedBuilder - { - private readonly Embed _embed; - - public const int MaxFieldCount = 25; - public const int MaxTitleLength = 256; - public const int MaxDescriptionLength = 2048; - public const int MaxEmbedLength = 6000; // user bot limit is 2000, but we don't validate that here. - - public EmbedBuilder() - { - _embed = new Embed(EmbedType.Rich); - Fields = new List(); - } - - public string Title - { - get => _embed.Title; - set - { - if (value?.Length > MaxTitleLength) throw new ArgumentException($"Title length must be less than or equal to {MaxTitleLength}.", nameof(Title)); - _embed.Title = value; - } - } - - public string Description - { - get => _embed.Description; - set - { - if (value?.Length > MaxDescriptionLength) throw new ArgumentException($"Description length must be less than or equal to {MaxDescriptionLength}.", nameof(Description)); - _embed.Description = value; - } - } - - public string Url - { - get => _embed.Url; - set - { - if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(Url)); - _embed.Url = value; - } - } - public string ThumbnailUrl - { - get => _embed.Thumbnail?.Url; - set - { - if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(ThumbnailUrl)); - _embed.Thumbnail = new EmbedThumbnail(value, null, null, null); - } - } - public string ImageUrl - { - get => _embed.Image?.Url; - set - { - if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(ImageUrl)); - _embed.Image = new EmbedImage(value, null, null, null); - } - } - public DateTimeOffset? Timestamp { get => _embed.Timestamp; set { _embed.Timestamp = value; } } - public Color? Color { get => _embed.Color; set { _embed.Color = value; } } - - public EmbedAuthorBuilder Author { get; set; } - public EmbedFooterBuilder Footer { get; set; } - private List _fields; - public List Fields - { - get => _fields; - set - { - - if (value == null) throw new ArgumentNullException("Cannot set an embed builder's fields collection to null", nameof(Fields)); - if (value.Count > MaxFieldCount) throw new ArgumentException($"Field count must be less than or equal to {MaxFieldCount}.", nameof(Fields)); - _fields = value; - } - } - - public EmbedBuilder WithTitle(string title) - { - Title = title; - return this; - } - public EmbedBuilder WithDescription(string description) - { - Description = description; - return this; - } - public EmbedBuilder WithUrl(string url) - { - Url = url; - return this; - } - public EmbedBuilder WithThumbnailUrl(string thumbnailUrl) - { - ThumbnailUrl = thumbnailUrl; - return this; - } - public EmbedBuilder WithImageUrl(string imageUrl) - { - ImageUrl = imageUrl; - return this; - } - public EmbedBuilder WithCurrentTimestamp() - { - Timestamp = DateTimeOffset.UtcNow; - return this; - } - public EmbedBuilder WithTimestamp(DateTimeOffset dateTimeOffset) - { - Timestamp = dateTimeOffset; - return this; - } - public EmbedBuilder WithColor(Color color) - { - Color = color; - return this; - } - - public EmbedBuilder WithAuthor(EmbedAuthorBuilder author) - { - Author = author; - return this; - } - public EmbedBuilder WithAuthor(Action action) - { - var author = new EmbedAuthorBuilder(); - action(author); - Author = author; - return this; - } - public EmbedBuilder WithAuthor(string name, string iconUrl = null, string url = null) - { - var author = new EmbedAuthorBuilder - { - Name = name, - IconUrl = iconUrl, - Url = url - }; - Author = author; - return this; - } - public EmbedBuilder WithFooter(EmbedFooterBuilder footer) - { - Footer = footer; - return this; - } - public EmbedBuilder WithFooter(Action action) - { - var footer = new EmbedFooterBuilder(); - action(footer); - Footer = footer; - return this; - } - public EmbedBuilder WithFooter(string text, string iconUrl = null) - { - var footer = new EmbedFooterBuilder - { - Text = text, - IconUrl = iconUrl - }; - Footer = footer; - return this; - } - - public EmbedBuilder AddField(string name, object value, bool inline = false) - { - var field = new EmbedFieldBuilder() - .WithIsInline(inline) - .WithName(name) - .WithValue(value); - AddField(field); - return this; - } - - public EmbedBuilder AddField(EmbedFieldBuilder field) - { - if (Fields.Count >= MaxFieldCount) - { - throw new ArgumentException($"Field count must be less than or equal to {MaxFieldCount}.", nameof(field)); - } - - Fields.Add(field); - return this; - } - public EmbedBuilder AddField(Action action) - { - var field = new EmbedFieldBuilder(); - action(field); - this.AddField(field); - return this; - } - - public Embed Build() - { - _embed.Footer = Footer?.Build(); - _embed.Author = Author?.Build(); - var fields = ImmutableArray.CreateBuilder(Fields.Count); - for (int i = 0; i < Fields.Count; i++) - fields.Add(Fields[i].Build()); - _embed.Fields = fields.ToImmutable(); - - if (_embed.Length > MaxEmbedLength) - { - throw new InvalidOperationException($"Total embed length must be less than or equal to {MaxEmbedLength}"); - } - - return _embed; - } - } - - public class EmbedFieldBuilder - { - private EmbedField _field; - - public const int MaxFieldNameLength = 256; - public const int MaxFieldValueLength = 1024; - - public string Name - { - get => _field.Name; - set - { - if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException($"Field name must not be null, empty or entirely whitespace.", nameof(Name)); - if (value.Length > MaxFieldNameLength) throw new ArgumentException($"Field name length must be less than or equal to {MaxFieldNameLength}.", nameof(Name)); - _field.Name = value; - } - } - - public object Value - { - get => _field.Value; - set - { - var stringValue = value?.ToString(); - if (string.IsNullOrEmpty(stringValue)) throw new ArgumentException($"Field value must not be null or empty.", nameof(Value)); - if (stringValue.Length > MaxFieldValueLength) throw new ArgumentException($"Field value length must be less than or equal to {MaxFieldValueLength}.", nameof(Value)); - _field.Value = stringValue; - } - } - public bool IsInline { get => _field.Inline; set { _field.Inline = value; } } - - public EmbedFieldBuilder() - { - _field = new EmbedField(); - } - - public EmbedFieldBuilder WithName(string name) - { - Name = name; - return this; - } - public EmbedFieldBuilder WithValue(object value) - { - Value = value; - return this; - } - public EmbedFieldBuilder WithIsInline(bool isInline) - { - IsInline = isInline; - return this; - } - - public EmbedField Build() - => _field; - } - - public class EmbedAuthorBuilder - { - private EmbedAuthor _author; - - public const int MaxAuthorNameLength = 256; - - public string Name - { - get => _author.Name; - set - { - if (value?.Length > MaxAuthorNameLength) throw new ArgumentException($"Author name length must be less than or equal to {MaxAuthorNameLength}.", nameof(Name)); - _author.Name = value; - } - } - public string Url - { - get => _author.Url; - set - { - if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(Url)); - _author.Url = value; - } - } - public string IconUrl - { - get => _author.IconUrl; - set - { - if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(IconUrl)); - _author.IconUrl = value; - } - } - - public EmbedAuthorBuilder() - { - _author = new EmbedAuthor(); - } - - public EmbedAuthorBuilder WithName(string name) - { - Name = name; - return this; - } - public EmbedAuthorBuilder WithUrl(string url) - { - Url = url; - return this; - } - public EmbedAuthorBuilder WithIconUrl(string iconUrl) - { - IconUrl = iconUrl; - return this; - } - - public EmbedAuthor Build() - => _author; - } - - public class EmbedFooterBuilder - { - private EmbedFooter _footer; - - public const int MaxFooterTextLength = 2048; - - public string Text - { - get => _footer.Text; - set - { - if (value?.Length > MaxFooterTextLength) throw new ArgumentException($"Footer text length must be less than or equal to {MaxFooterTextLength}.", nameof(Text)); - _footer.Text = value; - } - } - public string IconUrl - { - get => _footer.IconUrl; - set - { - if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(IconUrl)); - _footer.IconUrl = value; - } - } - - public EmbedFooterBuilder() - { - _footer = new EmbedFooter(); - } - - public EmbedFooterBuilder WithText(string text) - { - Text = text; - return this; - } - public EmbedFooterBuilder WithIconUrl(string iconUrl) - { - IconUrl = iconUrl; - return this; - } - - public EmbedFooter Build() - => _footer; - } -} diff --git a/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs b/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs index 47bb6f926..071628da0 100644 --- a/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs +++ b/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs @@ -1,4 +1,4 @@ -using Discord.API.Rest; +using Discord.API.Rest; using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -13,6 +13,9 @@ namespace Discord.Rest public static async Task ModifyAsync(IMessage msg, BaseDiscordClient client, Action func, RequestOptions options) { + if (msg.Author.Id != client.CurrentUser.Id) + throw new InvalidOperationException("Only the author of a message may change it."); + var args = new MessageProperties(); func(args); var apiArgs = new API.Rest.ModifyMessageParams @@ -40,7 +43,7 @@ namespace Discord.Rest public static async Task RemoveAllReactionsAsync(IMessage msg, BaseDiscordClient client, RequestOptions options) { - await client.ApiClient.RemoveAllReactionsAsync(msg.Channel.Id, msg.Id, options); + await client.ApiClient.RemoveAllReactionsAsync(msg.Channel.Id, msg.Id, options).ConfigureAwait(false); } public static async Task> GetReactionUsersAsync(IMessage msg, IEmote emote, diff --git a/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs b/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs index 590886886..84fef4c18 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; @@ -11,23 +11,33 @@ namespace Discord.Rest { private long _timestampTicks; + /// public IMessageChannel Channel { get; } public IUser Author { get; } + /// public MessageSource Source { get; } + /// public string Content { get; private set; } + /// public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); + /// public virtual bool IsTTS => false; + /// public virtual bool IsPinned => false; + /// public virtual DateTimeOffset? EditedTimestamp => null; public virtual IReadOnlyCollection Attachments => ImmutableArray.Create(); public virtual IReadOnlyCollection Embeds => ImmutableArray.Create(); + /// public virtual IReadOnlyCollection MentionedChannelIds => ImmutableArray.Create(); + /// public virtual IReadOnlyCollection MentionedRoleIds => ImmutableArray.Create(); public virtual IReadOnlyCollection MentionedUsers => ImmutableArray.Create(); public virtual IReadOnlyCollection Tags => ImmutableArray.Create(); + /// public DateTimeOffset Timestamp => DateTimeUtils.FromTicks(_timestampTicks); internal RestMessage(BaseDiscordClient discord, ulong id, IMessageChannel channel, IUser author, MessageSource source) @@ -53,11 +63,13 @@ namespace Discord.Rest Content = model.Content.Value; } + /// public async Task UpdateAsync(RequestOptions options = null) { var model = await Discord.ApiClient.GetChannelMessageAsync(Channel.Id, Id, options).ConfigureAwait(false); Update(model); } + /// public Task DeleteAsync(RequestOptions options = null) => MessageHelper.DeleteAsync(this, Discord, options); diff --git a/src/Discord.Net.Rest/Entities/Messages/RestReaction.cs b/src/Discord.Net.Rest/Entities/Messages/RestReaction.cs index 6d3f72419..c38efe32d 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestReaction.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestReaction.cs @@ -1,11 +1,21 @@ -using Model = Discord.API.Reaction; +using Model = Discord.API.Reaction; namespace Discord.Rest { + /// + /// Represents a REST reaction object. + /// public class RestReaction : IReaction { + /// public IEmote Emote { get; } + /// + /// Gets the number of reactions added. + /// public int Count { get; } + /// + /// Gets whether the reactions is added by the user. + /// public bool Me { get; } internal RestReaction(IEmote emote, int count, bool me) diff --git a/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs b/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs index e5eed874e..0d1f3be2b 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; diff --git a/src/Discord.Net.Rest/Entities/RestApplication.cs b/src/Discord.Net.Rest/Entities/RestApplication.cs index 827c33cf7..d8986a470 100644 --- a/src/Discord.Net.Rest/Entities/RestApplication.cs +++ b/src/Discord.Net.Rest/Entities/RestApplication.cs @@ -1,23 +1,32 @@ -using System; +using System; using System.Diagnostics; using System.Threading.Tasks; using Model = Discord.API.Application; namespace Discord.Rest { + /// + /// Represents a REST entity that contains information about a Discord application created via the developer portal. + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class RestApplication : RestEntity, IApplication { protected string _iconId; - + + /// public string Name { get; private set; } + /// public string Description { get; private set; } + /// public string[] RPCOrigins { get; private set; } public ulong Flags { get; private set; } + /// public IUser Owner { get; private set; } + /// public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); + /// public string IconUrl => CDN.GetApplicationIconUrl(Id, _iconId); internal RestApplication(BaseDiscordClient discord, ulong id) diff --git a/src/Discord.Net.Rest/Entities/Users/RestGroupUser.cs b/src/Discord.Net.Rest/Entities/Users/RestGroupUser.cs index 951bd2e7c..0ad0d8175 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestGroupUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestGroupUser.cs @@ -1,4 +1,4 @@ -using System.Diagnostics; +using System.Diagnostics; using Model = Discord.API.User; namespace Discord.Rest @@ -16,14 +16,21 @@ namespace Discord.Rest entity.Update(model); return entity; } - + //IVoiceState + /// bool IVoiceState.IsDeafened => false; + /// bool IVoiceState.IsMuted => false; + /// bool IVoiceState.IsSelfDeafened => false; + /// bool IVoiceState.IsSelfMuted => false; + /// bool IVoiceState.IsSuppressed => false; + /// IVoiceChannel IVoiceState.VoiceChannel => null; + /// string IVoiceState.VoiceSessionId => null; } } diff --git a/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs b/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs index e571f8f73..051ed73d5 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; @@ -14,12 +14,17 @@ namespace Discord.Rest private long? _joinedAtTicks; private ImmutableArray _roleIds; + /// public string Nickname { get; private set; } internal IGuild Guild { get; private set; } + /// public bool IsDeafened { get; private set; } + /// public bool IsMuted { get; private set; } + /// public ulong GuildId => Guild.Id; + /// public GuildPermissions GuildPermissions { get @@ -29,8 +34,10 @@ namespace Discord.Rest return new GuildPermissions(Permissions.ResolveGuild(Guild, this)); } } + /// public IReadOnlyCollection RoleIds => _roleIds; + /// public DateTimeOffset? JoinedAt => DateTimeUtils.FromTicks(_joinedAtTicks); internal RestGuildUser(BaseDiscordClient discord, IGuild guild, ulong id) @@ -67,11 +74,13 @@ namespace Discord.Rest _roleIds = roles.ToImmutable(); } + /// public override async Task UpdateAsync(RequestOptions options = null) { var model = await Discord.ApiClient.GetGuildMemberAsync(GuildId, Id, options).ConfigureAwait(false); Update(model); } + /// public async Task ModifyAsync(Action func, RequestOptions options = null) { var args = await UserHelper.ModifyAsync(this, Discord, func, options).ConfigureAwait(false); @@ -86,6 +95,7 @@ namespace Discord.Rest else if (args.RoleIds.IsSpecified) UpdateRoles(args.RoleIds.Value.ToArray()); } + /// public Task KickAsync(string reason = null, RequestOptions options = null) => UserHelper.KickAsync(this, Discord, reason, options); /// @@ -101,6 +111,7 @@ namespace Discord.Rest public Task RemoveRolesAsync(IEnumerable roles, RequestOptions options = null) => UserHelper.RemoveRolesAsync(this, Discord, roles, options); + /// public ChannelPermissions GetPermissions(IGuildChannel channel) { var guildPerms = GuildPermissions; diff --git a/src/Discord.Net.Rest/Entities/Users/RestUser.cs b/src/Discord.Net.Rest/Entities/Users/RestUser.cs index c6cf6103a..f0c771ae0 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestUser.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Diagnostics; using System.Threading.Tasks; using Model = Discord.API.User; @@ -60,11 +60,14 @@ namespace Discord.Rest public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) => CDN.GetUserAvatarUrl(Id, AvatarId, size, format); + public string GetDefaultAvatarUrl() + => CDN.GetDefaultUserAvatarUrl(DiscriminatorValue); + public override string ToString() => $"{Username}#{Discriminator}"; private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")})"; //IUser async Task IUser.GetOrCreateDMChannelAsync(RequestOptions options) - => await GetOrCreateDMChannelAsync(options); + => await GetOrCreateDMChannelAsync(options).ConfigureAwait(false); } } diff --git a/src/Discord.Net.Rest/Entities/Users/RestWebhookUser.cs b/src/Discord.Net.Rest/Entities/Users/RestWebhookUser.cs index bb44f2777..bee4892fe 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestWebhookUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestWebhookUser.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; @@ -10,10 +10,13 @@ namespace Discord.Rest [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class RestWebhookUser : RestUser, IWebhookUser { + /// public ulong WebhookId { get; } internal IGuild Guild { get; } + /// public override bool IsWebhook => true; + /// public ulong GuildId => Guild.Id; internal RestWebhookUser(BaseDiscordClient discord, IGuild guild, ulong id, ulong webhookId) @@ -28,8 +31,9 @@ namespace Discord.Rest entity.Update(model); return entity; } - + //IGuildUser + /// IGuild IGuildUser.Guild { get @@ -39,45 +43,55 @@ namespace Discord.Rest throw new InvalidOperationException("Unable to return this entity's parent unless it was fetched through that object."); } } + /// IReadOnlyCollection IGuildUser.RoleIds => ImmutableArray.Create(); + /// DateTimeOffset? IGuildUser.JoinedAt => null; + /// string IGuildUser.Nickname => null; + /// GuildPermissions IGuildUser.GuildPermissions => GuildPermissions.Webhook; + /// ChannelPermissions IGuildUser.GetPermissions(IGuildChannel channel) => Permissions.ToChannelPerms(channel, GuildPermissions.Webhook.RawValue); - Task IGuildUser.KickAsync(string reason, RequestOptions options) - { + /// + Task IGuildUser.KickAsync(string reason, RequestOptions options) => throw new NotSupportedException("Webhook users cannot be kicked."); - } - Task IGuildUser.ModifyAsync(Action func, RequestOptions options) - { + + /// + Task IGuildUser.ModifyAsync(Action func, RequestOptions options) => throw new NotSupportedException("Webhook users cannot be modified."); - } - Task IGuildUser.AddRoleAsync(IRole role, RequestOptions options) - { + /// + Task IGuildUser.AddRoleAsync(IRole role, RequestOptions options) => throw new NotSupportedException("Roles are not supported on webhook users."); - } - Task IGuildUser.AddRolesAsync(IEnumerable roles, RequestOptions options) - { + + /// + Task IGuildUser.AddRolesAsync(IEnumerable roles, RequestOptions options) => throw new NotSupportedException("Roles are not supported on webhook users."); - } - Task IGuildUser.RemoveRoleAsync(IRole role, RequestOptions options) - { + + /// + Task IGuildUser.RemoveRoleAsync(IRole role, RequestOptions options) => throw new NotSupportedException("Roles are not supported on webhook users."); - } - Task IGuildUser.RemoveRolesAsync(IEnumerable roles, RequestOptions options) - { + + /// + Task IGuildUser.RemoveRolesAsync(IEnumerable roles, RequestOptions options) => throw new NotSupportedException("Roles are not supported on webhook users."); - } //IVoiceState + /// bool IVoiceState.IsDeafened => false; + /// bool IVoiceState.IsMuted => false; + /// bool IVoiceState.IsSelfDeafened => false; + /// bool IVoiceState.IsSelfMuted => false; + /// bool IVoiceState.IsSuppressed => false; + /// IVoiceChannel IVoiceState.VoiceChannel => null; + /// string IVoiceState.VoiceSessionId => null; } } diff --git a/src/Discord.Net.Rest/Entities/Users/UserHelper.cs b/src/Discord.Net.Rest/Entities/Users/UserHelper.cs index dfb81ff2c..3e7493ab5 100644 --- a/src/Discord.Net.Rest/Entities/Users/UserHelper.cs +++ b/src/Discord.Net.Rest/Entities/Users/UserHelper.cs @@ -76,13 +76,13 @@ namespace Discord.Rest public static async Task AddRolesAsync(IGuildUser user, BaseDiscordClient client, IEnumerable roles, RequestOptions options) { foreach (var role in roles) - await client.ApiClient.AddRoleAsync(user.Guild.Id, user.Id, role.Id, options); + await client.ApiClient.AddRoleAsync(user.Guild.Id, user.Id, role.Id, options).ConfigureAwait(false); } public static async Task RemoveRolesAsync(IGuildUser user, BaseDiscordClient client, IEnumerable roles, RequestOptions options) { foreach (var role in roles) - await client.ApiClient.RemoveRoleAsync(user.Guild.Id, user.Id, role.Id, options); + await client.ApiClient.RemoveRoleAsync(user.Guild.Id, user.Id, role.Id, options).ConfigureAwait(false); } } } diff --git a/src/Discord.Net.Rest/Net/Converters/OptionalConverter.cs b/src/Discord.Net.Rest/Net/Converters/OptionalConverter.cs index 18b2a9e1c..d3d6191c0 100644 --- a/src/Discord.Net.Rest/Net/Converters/OptionalConverter.cs +++ b/src/Discord.Net.Rest/Net/Converters/OptionalConverter.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; using System; namespace Discord.Net.Converters @@ -19,10 +19,18 @@ namespace Discord.Net.Converters public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { T obj; + // custom converters need to be able to safely fail; move this check in here to prevent wasteful casting when parsing primitives if (_innerConverter != null) - obj = (T)_innerConverter.ReadJson(reader, typeof(T), null, serializer); + { + object o = _innerConverter.ReadJson(reader, typeof(T), null, serializer); + if (o is Optional) + return o; + + obj = (T)o; + } else obj = serializer.Deserialize(reader); + return new Optional(obj); } diff --git a/src/Discord.Net.Rest/Net/Converters/UnixTimestampConverter.cs b/src/Discord.Net.Rest/Net/Converters/UnixTimestampConverter.cs index d4660dc44..0b50cb166 100644 --- a/src/Discord.Net.Rest/Net/Converters/UnixTimestampConverter.cs +++ b/src/Discord.Net.Rest/Net/Converters/UnixTimestampConverter.cs @@ -1,4 +1,4 @@ -using System; +using System; using Newtonsoft.Json; namespace Discord.Net.Converters @@ -11,13 +11,18 @@ namespace Discord.Net.Converters public override bool CanRead => true; public override bool CanWrite => true; + // 1e13 unix ms = year 2286 + // necessary to prevent discord.js from sending values in the e15 and overflowing a DTO + private const long MaxSaneMs = 1_000_000_000_000_0; + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { - // Discord doesn't validate if timestamps contain decimals or not - if (reader.Value is double d) + // Discord doesn't validate if timestamps contain decimals or not, and they also don't validate if timestamps are reasonably sized + if (reader.Value is double d && d < MaxSaneMs) return new DateTimeOffset(1970, 1, 1, 0, 0, 0, 0, TimeSpan.Zero).AddMilliseconds(d); - long offset = (long)reader.Value; - return new DateTimeOffset(1970, 1, 1, 0, 0, 0, 0, TimeSpan.Zero).AddMilliseconds(offset); + else if (reader.Value is long l && l < MaxSaneMs) + return new DateTimeOffset(1970, 1, 1, 0, 0, 0, 0, TimeSpan.Zero).AddMilliseconds(l); + return Optional.Unspecified; } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) @@ -25,4 +30,4 @@ namespace Discord.Net.Converters throw new NotImplementedException(); } } -} \ No newline at end of file +} diff --git a/src/Discord.Net.WebSocket/BaseSocketClient.cs b/src/Discord.Net.WebSocket/BaseSocketClient.cs index 5fa3cbff8..923b2c23b 100644 --- a/src/Discord.Net.WebSocket/BaseSocketClient.cs +++ b/src/Discord.Net.WebSocket/BaseSocketClient.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Threading.Tasks; using Discord.API; diff --git a/src/Discord.Net.WebSocket/Commands/ShardedCommandContext.cs b/src/Discord.Net.WebSocket/Commands/ShardedCommandContext.cs index a29c9bb70..f970c32fc 100644 --- a/src/Discord.Net.WebSocket/Commands/ShardedCommandContext.cs +++ b/src/Discord.Net.WebSocket/Commands/ShardedCommandContext.cs @@ -1,9 +1,11 @@ -using Discord.WebSocket; +using Discord.WebSocket; namespace Discord.Commands { + /// The sharded variant of , which may contain the client, user, guild, channel, and message. public class ShardedCommandContext : SocketCommandContext, ICommandContext { + /// Gets the that the command is executed with. public new DiscordShardedClient Client { get; } public ShardedCommandContext(DiscordShardedClient client, SocketUserMessage msg) @@ -12,10 +14,12 @@ namespace Discord.Commands Client = client; } + /// Gets the shard ID of the command context. private static int GetShardId(DiscordShardedClient client, IGuild guild) => guild == null ? 0 : client.GetShardIdFor(guild); //ICommandContext + /// IDiscordClient ICommandContext.Client => Client; } } diff --git a/src/Discord.Net.WebSocket/Commands/SocketCommandContext.cs b/src/Discord.Net.WebSocket/Commands/SocketCommandContext.cs index c8b0747e7..29d78ab45 100644 --- a/src/Discord.Net.WebSocket/Commands/SocketCommandContext.cs +++ b/src/Discord.Net.WebSocket/Commands/SocketCommandContext.cs @@ -1,15 +1,22 @@ -using Discord.WebSocket; +using Discord.WebSocket; namespace Discord.Commands { + /// The WebSocket variant of , which may contain the client, user, guild, channel, and message. public class SocketCommandContext : ICommandContext { + /// Gets the that the command is executed with. public DiscordSocketClient Client { get; } + /// Gets the that the command is executed in. public SocketGuild Guild { get; } + /// Gets the that the command is executed in. public ISocketMessageChannel Channel { get; } + /// Gets the who executed the command. public SocketUser User { get; } + /// Gets the that the command is interpreted from. public SocketUserMessage Message { get; } + /// Indicates whether the channel that the command is executed in is a private channel. public bool IsPrivate => Channel is IPrivateChannel; public SocketCommandContext(DiscordSocketClient client, SocketUserMessage msg) @@ -22,10 +29,15 @@ namespace Discord.Commands } //ICommandContext + /// IDiscordClient ICommandContext.Client => Client; + /// IGuild ICommandContext.Guild => Guild; + /// IMessageChannel ICommandContext.Channel => Channel; + /// IUser ICommandContext.User => User; + /// IUserMessage ICommandContext.Message => Message; } } diff --git a/src/Discord.Net.WebSocket/DiscordShardedClient.cs b/src/Discord.Net.WebSocket/DiscordShardedClient.cs index fb78a201f..ad89067df 100644 --- a/src/Discord.Net.WebSocket/DiscordShardedClient.cs +++ b/src/Discord.Net.WebSocket/DiscordShardedClient.cs @@ -1,4 +1,4 @@ -using Discord.API; +using Discord.API; using Discord.Rest; using System; using System.Collections.Generic; @@ -75,8 +75,8 @@ namespace Discord.WebSocket { if (_automaticShards) { - var response = await ApiClient.GetBotGatewayAsync().ConfigureAwait(false); - _shardIds = Enumerable.Range(0, response.Shards).ToArray(); + var shardCount = await GetRecommendedShardCountAsync().ConfigureAwait(false); + _shardIds = Enumerable.Range(0, shardCount).ToArray(); _totalShards = _shardIds.Length; _shards = new DiscordSocketClient[_shardIds.Length]; for (int i = 0; i < _shardIds.Length; i++) diff --git a/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs b/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs index 72781204c..8ae41cc59 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs @@ -1,4 +1,4 @@ -#pragma warning disable CS1591 +#pragma warning disable CS1591 using Discord.API.Gateway; using Discord.API.Rest; using Discord.Net.Queue; @@ -207,18 +207,7 @@ namespace Discord.API await RequestQueue.SendAsync(new WebSocketRequest(WebSocketClient, null, bytes, true, options)).ConfigureAwait(false); await _sentGatewayMessageEvent.InvokeAsync(opCode).ConfigureAwait(false); } - - //Gateway - public async Task GetGatewayAsync(RequestOptions options = null) - { - options = RequestOptions.CreateOrClone(options); - return await SendAsync("GET", () => "gateway", new BucketIds(), options: options).ConfigureAwait(false); - } - public async Task GetBotGatewayAsync(RequestOptions options = null) - { - options = RequestOptions.CreateOrClone(options); - return await SendAsync("GET", () => "gateway/bot", new BucketIds(), options: options).ConfigureAwait(false); - } + public async Task SendIdentifyAsync(int largeThreshold = 100, int shardID = 0, int totalShards = 1, RequestOptions options = null) { options = RequestOptions.CreateOrClone(options); diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index 593f796c2..80c16ea79 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -68,9 +68,9 @@ namespace Discord.WebSocket => State.PrivateChannels.Select(x => x as SocketGroupChannel).Where(x => x != null).ToImmutableArray(); public override IReadOnlyCollection VoiceRegions => _voiceRegions.ToReadOnlyCollection(); - /// Creates a new REST/WebSocket discord client. + /// Creates a new REST/WebSocket Discord client. public DiscordSocketClient() : this(new DiscordSocketConfig()) { } - /// Creates a new REST/WebSocket discord client. + /// Creates a new REST/WebSocket Discord client. public DiscordSocketClient(DiscordSocketConfig config) : this(config, CreateApiClient(config), null, null) { } internal DiscordSocketClient(DiscordSocketConfig config, SemaphoreSlim groupLock, DiscordSocketClient parentClient) : this(config, CreateApiClient(config), groupLock, parentClient) { } private DiscordSocketClient(DiscordSocketConfig config, API.DiscordSocketApiClient client, SemaphoreSlim groupLock, DiscordSocketClient parentClient) @@ -1146,7 +1146,7 @@ namespace Discord.WebSocket after = SocketMessage.Create(this, State, author, channel, data); } - var cacheableBefore = new Cacheable(before, data.Id, isCached, async () => await channel.GetMessageAsync(data.Id)); + var cacheableBefore = new Cacheable(before, data.Id, isCached, async () => await channel.GetMessageAsync(data.Id).ConfigureAwait(false)); await TimedInvokeAsync(_messageUpdatedEvent, nameof(MessageUpdated), cacheableBefore, after, channel).ConfigureAwait(false); } @@ -1193,9 +1193,9 @@ namespace Discord.WebSocket { var cachedMsg = channel.GetCachedMessage(data.MessageId) as SocketUserMessage; bool isCached = cachedMsg != null; - var user = await channel.GetUserAsync(data.UserId, CacheMode.CacheOnly); + var user = await channel.GetUserAsync(data.UserId, CacheMode.CacheOnly).ConfigureAwait(false); var reaction = SocketReaction.Create(data, channel, cachedMsg, Optional.Create(user)); - var cacheable = new Cacheable(cachedMsg, data.MessageId, isCached, async () => await channel.GetMessageAsync(data.MessageId) as IUserMessage); + var cacheable = new Cacheable(cachedMsg, data.MessageId, isCached, async () => await channel.GetMessageAsync(data.MessageId).ConfigureAwait(false) as IUserMessage); cachedMsg?.AddReaction(reaction); @@ -1217,9 +1217,9 @@ namespace Discord.WebSocket { var cachedMsg = channel.GetCachedMessage(data.MessageId) as SocketUserMessage; bool isCached = cachedMsg != null; - var user = await channel.GetUserAsync(data.UserId, CacheMode.CacheOnly); + var user = await channel.GetUserAsync(data.UserId, CacheMode.CacheOnly).ConfigureAwait(false); var reaction = SocketReaction.Create(data, channel, cachedMsg, Optional.Create(user)); - var cacheable = new Cacheable(cachedMsg, data.MessageId, isCached, async () => await channel.GetMessageAsync(data.MessageId) as IUserMessage); + var cacheable = new Cacheable(cachedMsg, data.MessageId, isCached, async () => await channel.GetMessageAsync(data.MessageId).ConfigureAwait(false) as IUserMessage); cachedMsg?.RemoveReaction(reaction); @@ -1241,7 +1241,7 @@ namespace Discord.WebSocket { var cachedMsg = channel.GetCachedMessage(data.MessageId) as SocketUserMessage; bool isCached = cachedMsg != null; - var cacheable = new Cacheable(cachedMsg, data.MessageId, isCached, async () => await channel.GetMessageAsync(data.MessageId) as IUserMessage); + var cacheable = new Cacheable(cachedMsg, data.MessageId, isCached, async () => (await channel.GetMessageAsync(data.MessageId).ConfigureAwait(false)) as IUserMessage); cachedMsg?.ClearReactions(); @@ -1272,7 +1272,7 @@ namespace Discord.WebSocket { var msg = SocketChannelHelper.RemoveMessage(channel, this, id); bool isCached = msg != null; - var cacheable = new Cacheable(msg, id, isCached, async () => await channel.GetMessageAsync(id)); + var cacheable = new Cacheable(msg, id, isCached, async () => await channel.GetMessageAsync(id).ConfigureAwait(false)); await TimedInvokeAsync(_messageDeletedEvent, nameof(MessageDeleted), cacheable, channel).ConfigureAwait(false); } } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs index d5a183b1e..e7a165c2f 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; @@ -15,10 +15,12 @@ namespace Discord.WebSocket public class SocketCategoryChannel : SocketGuildChannel, ICategoryChannel { public override IReadOnlyCollection Users - => Guild.Users.Where(x => x.VoiceChannel?.Id == Id).ToImmutableArray(); + => Guild.Users.Where(x => Permissions.GetValue( + Permissions.ResolveChannel(Guild, x, this, Permissions.ResolveGuild(Guild, x)), + ChannelPermission.ViewChannel)).ToImmutableArray(); public IReadOnlyCollection Channels - => Guild.Channels.Where(x => x.CategoryId == CategoryId).ToImmutableArray(); + => Guild.Channels.Where(x => x.CategoryId == Id).ToImmutableArray(); internal SocketCategoryChannel(DiscordSocketClient discord, ulong id, SocketGuild guild) : base(discord, id, guild) @@ -31,14 +33,28 @@ namespace Discord.WebSocket return entity; } + //Users + public override SocketGuildUser GetUser(ulong id) + { + var user = Guild.GetUser(id); + if (user != null) + { + var guildPerms = Permissions.ResolveGuild(Guild, user); + var channelPerms = Permissions.ResolveChannel(Guild, user, this, guildPerms); + if (Permissions.GetValue(channelPerms, ChannelPermission.ViewChannel)) + return user; + } + return null; + } + private string DebuggerDisplay => $"{Name} ({Id}, Category)"; internal new SocketCategoryChannel Clone() => MemberwiseClone() as SocketCategoryChannel; // IGuildChannel IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options) - => throw new NotSupportedException(); + => ImmutableArray.Create>(Users).ToAsyncEnumerable(); Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) - => throw new NotSupportedException(); + => Task.FromResult(GetUser(id)); Task IGuildChannel.CreateInviteAsync(int? maxAge, int? maxUses, bool isTemporary, bool isUnique, RequestOptions options) => throw new NotSupportedException(); Task> IGuildChannel.GetInvitesAsync(RequestOptions options) @@ -46,8 +62,8 @@ namespace Discord.WebSocket //IChannel IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) - => throw new NotSupportedException(); + => ImmutableArray.Create>(Users).ToAsyncEnumerable(); Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) - => throw new NotSupportedException(); + => Task.FromResult(GetUser(id)); } } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs index d46c5fc59..27713609c 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs @@ -14,6 +14,9 @@ using VoiceStateModel = Discord.API.VoiceState; namespace Discord.WebSocket { + /// + /// Represents a private WebSocket group channel. + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class SocketGroupChannel : SocketChannel, IGroupChannel, ISocketPrivateChannel, ISocketMessageChannel, ISocketAudioChannel { diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs index 2163daf55..84072a2ab 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs @@ -1,4 +1,4 @@ -using Discord.Rest; +using Discord.Rest; using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -9,18 +9,23 @@ using Model = Discord.API.Channel; namespace Discord.WebSocket { + /// The WebSocket variant of . Represents a guild channel (text, voice, category). [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class SocketGuildChannel : SocketChannel, IGuildChannel { private ImmutableArray _overwrites; public SocketGuild Guild { get; } + /// public string Name { get; private set; } + /// public int Position { get; private set; } + /// public ulong? CategoryId { get; private set; } public ICategoryChannel Category => CategoryId.HasValue ? Guild.GetChannel(CategoryId.Value) as ICategoryChannel : null; + /// public IReadOnlyCollection PermissionOverwrites => _overwrites; public new virtual IReadOnlyCollection Users => ImmutableArray.Create(); @@ -57,8 +62,10 @@ namespace Discord.WebSocket _overwrites = newOverwrites.ToImmutable(); } + /// public Task ModifyAsync(Action func, RequestOptions options = null) => ChannelHelper.ModifyAsync(this, Discord, func, options); + /// public Task DeleteAsync(RequestOptions options = null) => ChannelHelper.DeleteAsync(this, Discord, options); @@ -132,38 +139,53 @@ namespace Discord.WebSocket internal override SocketUser GetUserInternal(ulong id) => GetUser(id); //IGuildChannel + /// IGuild IGuildChannel.Guild => Guild; + /// ulong IGuildChannel.GuildId => Guild.Id; + /// Task IGuildChannel.GetCategoryAsync() => Task.FromResult(Category); + /// async Task> IGuildChannel.GetInvitesAsync(RequestOptions options) => await GetInvitesAsync(options).ConfigureAwait(false); + /// async Task IGuildChannel.CreateInviteAsync(int? maxAge, int? maxUses, bool isTemporary, bool isUnique, RequestOptions options) => await CreateInviteAsync(maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false); + /// OverwritePermissions? IGuildChannel.GetPermissionOverwrite(IRole role) => GetPermissionOverwrite(role); + /// OverwritePermissions? IGuildChannel.GetPermissionOverwrite(IUser user) => GetPermissionOverwrite(user); + /// async Task IGuildChannel.AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options) => await AddPermissionOverwriteAsync(role, permissions, options).ConfigureAwait(false); + /// async Task IGuildChannel.AddPermissionOverwriteAsync(IUser user, OverwritePermissions permissions, RequestOptions options) => await AddPermissionOverwriteAsync(user, permissions, options).ConfigureAwait(false); + /// async Task IGuildChannel.RemovePermissionOverwriteAsync(IRole role, RequestOptions options) => await RemovePermissionOverwriteAsync(role, options).ConfigureAwait(false); + /// async Task IGuildChannel.RemovePermissionOverwriteAsync(IUser user, RequestOptions options) => await RemovePermissionOverwriteAsync(user, options).ConfigureAwait(false); + /// IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options) => ImmutableArray.Create>(Users).ToAsyncEnumerable(); + /// Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetUser(id)); //IChannel + /// IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) => ImmutableArray.Create>(Users).ToAsyncEnumerable(); //Overridden in Text/Voice + /// Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetUser(id)); //Overridden in Text/Voice } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs index ec7615b55..368a2a730 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs @@ -15,13 +15,18 @@ namespace Discord.WebSocket { private readonly MessageCache _messages; + /// public string Topic { get; private set; } private bool _nsfw; + /// public bool IsNsfw => _nsfw || ChannelHelper.IsNsfw(this); + /// public string Mention => MentionUtils.MentionChannel(Id); + /// public IReadOnlyCollection CachedMessages => _messages?.Messages ?? ImmutableArray.Create(); + /// public override IReadOnlyCollection Users => Guild.Users.Where(x => Permissions.GetValue( Permissions.ResolveChannel(Guild, x, this, Permissions.ResolveGuild(Guild, x)), @@ -47,10 +52,12 @@ namespace Discord.WebSocket _nsfw = model.Nsfw.GetValueOrDefault(); } + /// public Task ModifyAsync(Action func, RequestOptions options = null) => ChannelHelper.ModifyAsync(this, Discord, func, options); //Messages + /// public SocketMessage GetCachedMessage(ulong id) => _messages?.Get(id); public async Task GetMessageAsync(ulong id, RequestOptions options = null) @@ -75,20 +82,26 @@ namespace Discord.WebSocket public Task> GetPinnedMessagesAsync(RequestOptions options = null) => ChannelHelper.GetPinnedMessagesAsync(this, Discord, options); + /// public Task SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); #if FILESYSTEM + /// public Task SendFileAsync(string filePath, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options); #endif + /// public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, embed, options); + /// public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) => ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options); + /// public Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null) => ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options); + /// public Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); public IDisposable EnterTypingState(RequestOptions options = null) @@ -100,6 +113,7 @@ namespace Discord.WebSocket => _messages?.Remove(id); //Users + /// public override SocketGuildUser GetUser(ulong id) { var user = Guild.GetUser(id); @@ -125,20 +139,26 @@ namespace Discord.WebSocket internal new SocketTextChannel Clone() => MemberwiseClone() as SocketTextChannel; //ITextChannel + /// async Task ITextChannel.CreateWebhookAsync(string name, Stream avatar, RequestOptions options) - => await CreateWebhookAsync(name, avatar, options); + => await CreateWebhookAsync(name, avatar, options).ConfigureAwait(false); + /// async Task ITextChannel.GetWebhookAsync(ulong id, RequestOptions options) - => await GetWebhookAsync(id, options); + => await GetWebhookAsync(id, options).ConfigureAwait(false); + /// async Task> ITextChannel.GetWebhooksAsync(RequestOptions options) - => await GetWebhooksAsync(options); + => await GetWebhooksAsync(options).ConfigureAwait(false); //IGuildChannel + /// Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetUser(id)); + /// IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options) => ImmutableArray.Create>(Users).ToAsyncEnumerable(); //IMessageChannel + /// async Task IMessageChannel.GetMessageAsync(ulong id, CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -146,22 +166,30 @@ namespace Discord.WebSocket else return GetCachedMessage(id); } + /// IAsyncEnumerable> IMessageChannel.GetMessagesAsync(int limit, CacheMode mode, RequestOptions options) => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, null, Direction.Before, limit, mode, options); + /// IAsyncEnumerable> IMessageChannel.GetMessagesAsync(ulong fromMessageId, Direction dir, int limit, CacheMode mode, RequestOptions options) => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessageId, dir, limit, mode, options); + /// IAsyncEnumerable> IMessageChannel.GetMessagesAsync(IMessage fromMessage, Direction dir, int limit, CacheMode mode, RequestOptions options) => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessage.Id, dir, limit, mode, options); + /// async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) => await GetPinnedMessagesAsync(options).ConfigureAwait(false); #if FILESYSTEM + /// async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options) => await SendFileAsync(filePath, text, isTTS, embed, options).ConfigureAwait(false); #endif + /// async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options) => await SendFileAsync(stream, filename, text, isTTS, embed, options).ConfigureAwait(false); + /// async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, Embed embed, RequestOptions options) => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); + /// IDisposable IMessageChannel.EnterTypingState(RequestOptions options) => EnterTypingState(options); } diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index 259dae5a8..5acb359c3 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -34,30 +34,53 @@ namespace Discord.WebSocket private ImmutableArray _features; private AudioClient _audioClient; + /// public string Name { get; private set; } + /// public int AFKTimeout { get; private set; } + /// public bool IsEmbeddable { get; private set; } + /// public VerificationLevel VerificationLevel { get; private set; } + /// public MfaLevel MfaLevel { get; private set; } + /// public DefaultMessageNotifications DefaultMessageNotifications { get; private set; } + /// Gets the number of members. + /// + /// The number of members is returned by Discord and is the most accurate. + /// You may see discrepancy between the Users collection and this. + /// public int MemberCount { get; internal set; } + /// Gets the number of members downloaded to the local guild cache. public int DownloadedMemberCount { get; private set; } internal bool IsAvailable { get; private set; } + /// Indicates whether the client is connected to this guild. public bool IsConnected { get; internal set; } internal ulong? AFKChannelId { get; private set; } internal ulong? EmbedChannelId { get; private set; } internal ulong? SystemChannelId { get; private set; } + /// public ulong OwnerId { get; private set; } + /// Gets the user that owns this guild. public SocketGuildUser Owner => GetUser(OwnerId); + /// public string VoiceRegionId { get; private set; } + /// public string IconId { get; private set; } + /// public string SplashId { get; private set; } + /// public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); + /// public string IconUrl => CDN.GetGuildIconUrl(Id, IconId); + /// public string SplashUrl => CDN.GetGuildSplashUrl(Id, SplashId); + /// Indicates whether the client has all the members downloaded to the local guild cache. public bool HasAllMembers => MemberCount == DownloadedMemberCount;// _downloaderPromise.Task.IsCompleted; + /// Indicates whether the guild cache is synced to this guild. public bool IsSynced => _syncPromise.Task.IsCompleted; public Task SyncPromise => _syncPromise.Task; public Task DownloaderPromise => _downloaderPromise.Task; @@ -270,32 +293,43 @@ namespace Discord.WebSocket } //General + /// public Task DeleteAsync(RequestOptions options = null) => GuildHelper.DeleteAsync(this, Discord, options); + /// public Task ModifyAsync(Action func, RequestOptions options = null) => GuildHelper.ModifyAsync(this, Discord, func, options); + /// public Task ModifyEmbedAsync(Action func, RequestOptions options = null) => GuildHelper.ModifyEmbedAsync(this, Discord, func, options); + /// public Task ReorderChannelsAsync(IEnumerable args, RequestOptions options = null) => GuildHelper.ReorderChannelsAsync(this, Discord, args, options); + /// public Task ReorderRolesAsync(IEnumerable args, RequestOptions options = null) => GuildHelper.ReorderRolesAsync(this, Discord, args, options); + /// public Task LeaveAsync(RequestOptions options = null) => GuildHelper.LeaveAsync(this, Discord, options); //Bans + /// Gets a collection of the banned users in this guild. public Task> GetBansAsync(RequestOptions options = null) => GuildHelper.GetBansAsync(this, Discord, options); + /// public Task AddBanAsync(IUser user, int pruneDays = 0, string reason = null, RequestOptions options = null) => GuildHelper.AddBanAsync(this, Discord, user.Id, pruneDays, reason, options); + /// public Task AddBanAsync(ulong userId, int pruneDays = 0, string reason = null, RequestOptions options = null) => GuildHelper.AddBanAsync(this, Discord, userId, pruneDays, reason, options); + /// public Task RemoveBanAsync(IUser user, RequestOptions options = null) => GuildHelper.RemoveBanAsync(this, Discord, user.Id, options); + /// public Task RemoveBanAsync(ulong userId, RequestOptions options = null) => GuildHelper.RemoveBanAsync(this, Discord, userId, options); @@ -441,12 +475,16 @@ namespace Discord.WebSocket => GuildHelper.GetWebhooksAsync(this, Discord, options); //Emotes + /// public Task GetEmoteAsync(ulong id, RequestOptions options = null) => GuildHelper.GetEmoteAsync(this, Discord, id, options); + /// public Task CreateEmoteAsync(string name, Image image, Optional> roles = default(Optional>), RequestOptions options = null) => GuildHelper.CreateEmoteAsync(this, Discord, name, image, roles, options); + /// public Task ModifyEmoteAsync(GuildEmote emote, Action func, RequestOptions options = null) => GuildHelper.ModifyEmoteAsync(this, Discord, emote.Id, func, options); + /// public Task DeleteEmoteAsync(GuildEmote emote, RequestOptions options = null) => GuildHelper.DeleteEmoteAsync(this, Discord, emote.Id, options); @@ -630,71 +668,105 @@ namespace Discord.WebSocket internal SocketGuild Clone() => MemberwiseClone() as SocketGuild; //IGuild + /// ulong? IGuild.AFKChannelId => AFKChannelId; + /// IAudioClient IGuild.AudioClient => null; + /// bool IGuild.Available => true; + /// ulong IGuild.DefaultChannelId => DefaultChannel?.Id ?? 0; + /// ulong? IGuild.EmbedChannelId => EmbedChannelId; + /// ulong? IGuild.SystemChannelId => SystemChannelId; + /// IRole IGuild.EveryoneRole => EveryoneRole; + /// IReadOnlyCollection IGuild.Roles => Roles; + /// async Task> IGuild.GetBansAsync(RequestOptions options) => await GetBansAsync(options).ConfigureAwait(false); + /// Task> IGuild.GetChannelsAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(Channels); + /// Task IGuild.GetChannelAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetChannel(id)); + /// Task> IGuild.GetTextChannelsAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(TextChannels); + /// Task IGuild.GetTextChannelAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetTextChannel(id)); + /// Task> IGuild.GetVoiceChannelsAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(VoiceChannels); + /// Task> IGuild.GetCategoriesAsync(CacheMode mode , RequestOptions options) => Task.FromResult>(CategoryChannels); + /// Task IGuild.GetVoiceChannelAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetVoiceChannel(id)); + /// Task IGuild.GetAFKChannelAsync(CacheMode mode, RequestOptions options) => Task.FromResult(AFKChannel); + /// Task IGuild.GetDefaultChannelAsync(CacheMode mode, RequestOptions options) => Task.FromResult(DefaultChannel); + /// Task IGuild.GetEmbedChannelAsync(CacheMode mode, RequestOptions options) => Task.FromResult(EmbedChannel); + /// Task IGuild.GetSystemChannelAsync(CacheMode mode, RequestOptions options) => Task.FromResult(SystemChannel); + /// async Task IGuild.CreateTextChannelAsync(string name, RequestOptions options) => await CreateTextChannelAsync(name, options).ConfigureAwait(false); + /// async Task IGuild.CreateVoiceChannelAsync(string name, RequestOptions options) => await CreateVoiceChannelAsync(name, options).ConfigureAwait(false); + /// async Task IGuild.CreateCategoryAsync(string name, RequestOptions options) => await CreateCategoryChannelAsync(name, options).ConfigureAwait(false); + /// async Task> IGuild.GetIntegrationsAsync(RequestOptions options) => await GetIntegrationsAsync(options).ConfigureAwait(false); + /// async Task IGuild.CreateIntegrationAsync(ulong id, string type, RequestOptions options) => await CreateIntegrationAsync(id, type, options).ConfigureAwait(false); + /// async Task> IGuild.GetInvitesAsync(RequestOptions options) => await GetInvitesAsync(options).ConfigureAwait(false); + /// IRole IGuild.GetRole(ulong id) => GetRole(id); + /// async Task IGuild.CreateRoleAsync(string name, GuildPermissions? permissions, Color? color, bool isHoisted, RequestOptions options) => await CreateRoleAsync(name, permissions, color, isHoisted, options).ConfigureAwait(false); + /// Task> IGuild.GetUsersAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(Users); + /// Task IGuild.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetUser(id)); + /// Task IGuild.GetCurrentUserAsync(CacheMode mode, RequestOptions options) => Task.FromResult(CurrentUser); + /// Task IGuild.GetOwnerAsync(CacheMode mode, RequestOptions options) => Task.FromResult(Owner); + /// async Task IGuild.GetWebhookAsync(ulong id, RequestOptions options) => await GetWebhookAsync(id, options); + /// async Task> IGuild.GetWebhooksAsync(RequestOptions options) => await GetWebhooksAsync(options); } diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs index b240645e5..5489ad2bb 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs @@ -1,4 +1,4 @@ -using Discord.Rest; +using Discord.Rest; using System; using System.Collections.Generic; using System.Collections.Immutable; diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGroupUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGroupUser.cs index 8d1b360e3..10701b418 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGroupUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGroupUser.cs @@ -1,4 +1,4 @@ -using System.Diagnostics; +using System.Diagnostics; using Model = Discord.API.User; namespace Discord.WebSocket @@ -9,12 +9,17 @@ namespace Discord.WebSocket public SocketGroupChannel Channel { get; } internal override SocketGlobalUser GlobalUser { get; } + /// public override bool IsBot { get { return GlobalUser.IsBot; } internal set { GlobalUser.IsBot = value; } } + /// public override string Username { get { return GlobalUser.Username; } internal set { GlobalUser.Username = value; } } + /// public override ushort DiscriminatorValue { get { return GlobalUser.DiscriminatorValue; } internal set { GlobalUser.DiscriminatorValue = value; } } + /// public override string AvatarId { get { return GlobalUser.AvatarId; } internal set { GlobalUser.AvatarId = value; } } internal override SocketPresence Presence { get { return GlobalUser.Presence; } set { GlobalUser.Presence = value; } } + /// public override bool IsWebhook => false; internal SocketGroupUser(SocketGroupChannel channel, SocketGlobalUser globalUser) @@ -33,12 +38,19 @@ namespace Discord.WebSocket internal new SocketGroupUser Clone() => MemberwiseClone() as SocketGroupUser; //IVoiceState + /// bool IVoiceState.IsDeafened => false; + /// bool IVoiceState.IsMuted => false; + /// bool IVoiceState.IsSelfDeafened => false; + /// bool IVoiceState.IsSelfMuted => false; + /// bool IVoiceState.IsSuppressed => false; + /// IVoiceChannel IVoiceState.VoiceChannel => null; + /// string IVoiceState.VoiceSessionId => null; } } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs index 66af20bb6..cad354dae 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs @@ -1,4 +1,4 @@ -using Discord.Audio; +using Discord.Audio; using Discord.Rest; using System; using System.Collections.Generic; @@ -12,6 +12,7 @@ using PresenceModel = Discord.API.Presence; namespace Discord.WebSocket { + /// The WebSocket variant of . Represents a Discord user that is in a guild. [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class SocketGuildUser : SocketUser, IGuildUser { @@ -20,32 +21,46 @@ namespace Discord.WebSocket internal override SocketGlobalUser GlobalUser { get; } public SocketGuild Guild { get; } + /// public string Nickname { get; private set; } + /// public override bool IsBot { get { return GlobalUser.IsBot; } internal set { GlobalUser.IsBot = value; } } + /// public override string Username { get { return GlobalUser.Username; } internal set { GlobalUser.Username = value; } } + /// public override ushort DiscriminatorValue { get { return GlobalUser.DiscriminatorValue; } internal set { GlobalUser.DiscriminatorValue = value; } } + /// public override string AvatarId { get { return GlobalUser.AvatarId; } internal set { GlobalUser.AvatarId = value; } } + /// public GuildPermissions GuildPermissions => new GuildPermissions(Permissions.ResolveGuild(Guild, this)); internal override SocketPresence Presence { get; set; } + /// public override bool IsWebhook => false; + /// public bool IsSelfDeafened => VoiceState?.IsSelfDeafened ?? false; + /// public bool IsSelfMuted => VoiceState?.IsSelfMuted ?? false; + /// public bool IsSuppressed => VoiceState?.IsSuppressed ?? false; + /// public bool IsDeafened => VoiceState?.IsDeafened ?? false; + /// public bool IsMuted => VoiceState?.IsMuted ?? false; + /// public DateTimeOffset? JoinedAt => DateTimeUtils.FromTicks(_joinedAtTicks); public IReadOnlyCollection Roles => _roleIds.Select(id => Guild.GetRole(id)).Where(x => x != null).ToReadOnlyCollection(() => _roleIds.Length); public SocketVoiceChannel VoiceChannel => VoiceState?.VoiceChannel; + /// public string VoiceSessionId => VoiceState?.VoiceSessionId ?? ""; public SocketVoiceState? VoiceState => Guild.GetVoiceState(Id); public AudioInStream AudioStream => Guild.GetAudioStream(Id); /// The position of the user within the role hierarchy. /// The returned value equal to the position of the highest role the user has, - /// or int.MaxValue if user is the server owner. + /// or if user is the server owner. public int Hierarchy { get @@ -119,9 +134,11 @@ namespace Discord.WebSocket roles.Add(roleIds[i]); _roleIds = roles.ToImmutable(); } - + + /// public Task ModifyAsync(Action func, RequestOptions options = null) => UserHelper.ModifyAsync(this, Discord, func, options); + /// public Task KickAsync(string reason = null, RequestOptions options = null) => UserHelper.KickAsync(this, Discord, reason, options); /// @@ -137,17 +154,22 @@ namespace Discord.WebSocket public Task RemoveRolesAsync(IEnumerable roles, RequestOptions options = null) => UserHelper.RemoveRolesAsync(this, Discord, roles, options); + /// public ChannelPermissions GetPermissions(IGuildChannel channel) => new ChannelPermissions(Permissions.ResolveChannel(Guild, this, channel, GuildPermissions.RawValue)); internal new SocketGuildUser Clone() => MemberwiseClone() as SocketGuildUser; //IGuildUser + /// IGuild IGuildUser.Guild => Guild; + /// ulong IGuildUser.GuildId => Guild.Id; + /// IReadOnlyCollection IGuildUser.RoleIds => _roleIds; - + //IVoiceState + /// IVoiceChannel IVoiceState.VoiceChannel => VoiceChannel; } } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs index 58d5c62a1..45cd5deec 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs @@ -1,24 +1,35 @@ -using Discord.Rest; +using Discord.Rest; using System; using System.Threading.Tasks; using Model = Discord.API.User; namespace Discord.WebSocket { + /// The WebSocket variant of . Represents a Discord user. public abstract class SocketUser : SocketEntity, IUser { + /// public abstract bool IsBot { get; internal set; } + /// public abstract string Username { get; internal set; } + /// public abstract ushort DiscriminatorValue { get; internal set; } + /// public abstract string AvatarId { get; internal set; } + /// public abstract bool IsWebhook { get; } internal abstract SocketGlobalUser GlobalUser { get; } internal abstract SocketPresence Presence { get; set; } + /// public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); + /// public string Discriminator => DiscriminatorValue.ToString("D4"); + /// public string Mention => MentionUtils.MentionUser(Id); + /// public IActivity Activity => Presence.Activity; + /// public UserStatus Status => Presence.Status; internal SocketUser(DiscordSocketClient discord, ulong id) @@ -37,30 +48,36 @@ namespace Discord.WebSocket { var newVal = ushort.Parse(model.Discriminator.Value); if (newVal != DiscriminatorValue) - { + { DiscriminatorValue = ushort.Parse(model.Discriminator.Value); hasChanges = true; } } if (model.Bot.IsSpecified && model.Bot.Value != IsBot) - { + { IsBot = model.Bot.Value; hasChanges = true; } if (model.Username.IsSpecified && model.Username.Value != Username) - { + { Username = model.Username.Value; hasChanges = true; } return hasChanges; - } + } + /// public async Task GetOrCreateDMChannelAsync(RequestOptions options = null) => GlobalUser.DMChannel ?? await UserHelper.CreateDMChannelAsync(this, Discord, options) as IDMChannel; + /// public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) => CDN.GetUserAvatarUrl(Id, AvatarId, size, format); + /// + public string GetDefaultAvatarUrl() + => CDN.GetDefaultUserAvatarUrl(DiscriminatorValue); + public override string ToString() => $"{Username}#{Discriminator}"; private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")})"; internal SocketUser Clone() => MemberwiseClone() as SocketUser; diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs index 78a29639b..d4ddb4fa8 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs @@ -10,18 +10,26 @@ namespace Discord.WebSocket [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class SocketWebhookUser : SocketUser, IWebhookUser { + /// Gets the guild of this webhook. public SocketGuild Guild { get; } + /// public ulong WebhookId { get; } + /// public override string Username { get; internal set; } + /// public override ushort DiscriminatorValue { get; internal set; } + /// public override string AvatarId { get; internal set; } + /// public override bool IsBot { get; internal set; } + /// public override bool IsWebhook => true; internal override SocketPresence Presence { get { return new SocketPresence(UserStatus.Offline, null); } set { } } - internal override SocketGlobalUser GlobalUser { get { throw new NotSupportedException(); } } + internal override SocketGlobalUser GlobalUser => + throw new NotSupportedException(); internal SocketWebhookUser(SocketGuild guild, ulong id, ulong webhookId) : base(guild.Discord, id) @@ -39,47 +47,59 @@ namespace Discord.WebSocket //IGuildUser + /// IGuild IGuildUser.Guild => Guild; + /// ulong IGuildUser.GuildId => Guild.Id; + /// IReadOnlyCollection IGuildUser.RoleIds => ImmutableArray.Create(); + /// DateTimeOffset? IGuildUser.JoinedAt => null; + /// string IGuildUser.Nickname => null; + /// GuildPermissions IGuildUser.GuildPermissions => GuildPermissions.Webhook; + /// ChannelPermissions IGuildUser.GetPermissions(IGuildChannel channel) => Permissions.ToChannelPerms(channel, GuildPermissions.Webhook.RawValue); - Task IGuildUser.KickAsync(string reason, RequestOptions options) - { + /// + Task IGuildUser.KickAsync(string reason, RequestOptions options) => throw new NotSupportedException("Webhook users cannot be kicked."); - } - Task IGuildUser.ModifyAsync(Action func, RequestOptions options) - { + + /// + Task IGuildUser.ModifyAsync(Action func, RequestOptions options) => throw new NotSupportedException("Webhook users cannot be modified."); - } - Task IGuildUser.AddRoleAsync(IRole role, RequestOptions options) - { + /// + Task IGuildUser.AddRoleAsync(IRole role, RequestOptions options) => throw new NotSupportedException("Roles are not supported on webhook users."); - } - Task IGuildUser.AddRolesAsync(IEnumerable roles, RequestOptions options) - { + + /// + Task IGuildUser.AddRolesAsync(IEnumerable roles, RequestOptions options) => throw new NotSupportedException("Roles are not supported on webhook users."); - } - Task IGuildUser.RemoveRoleAsync(IRole role, RequestOptions options) - { + + /// + Task IGuildUser.RemoveRoleAsync(IRole role, RequestOptions options) => throw new NotSupportedException("Roles are not supported on webhook users."); - } - Task IGuildUser.RemoveRolesAsync(IEnumerable roles, RequestOptions options) - { + + /// + Task IGuildUser.RemoveRolesAsync(IEnumerable roles, RequestOptions options) => throw new NotSupportedException("Roles are not supported on webhook users."); - } //IVoiceState + /// bool IVoiceState.IsDeafened => false; + /// bool IVoiceState.IsMuted => false; + /// bool IVoiceState.IsSelfDeafened => false; + /// bool IVoiceState.IsSelfMuted => false; + /// bool IVoiceState.IsSuppressed => false; + /// IVoiceChannel IVoiceState.VoiceChannel => null; + /// string IVoiceState.VoiceSessionId => null; } } diff --git a/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs b/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs index 181a837e4..ff395a932 100644 --- a/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs +++ b/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs @@ -4,6 +4,27 @@ namespace Discord.WebSocket { public static IActivity ToEntity(this API.Game model) { + // Spotify Game + if (model.SyncId.IsSpecified) + { + var assets = model.Assets.GetValueOrDefault()?.ToEntity(); + string albumText = assets?[1]?.Text; + string albumArtId = assets?[1]?.ImageId?.Replace("spotify:",""); + var timestamps = model.Timestamps.IsSpecified ? model.Timestamps.Value.ToEntity() : null; + return new SpotifyGame + { + Name = model.Name, + SessionId = model.SessionId.GetValueOrDefault(), + SyncId = model.SyncId.Value, + AlbumTitle = albumText, + TrackTitle = model.Details.GetValueOrDefault(), + Artists = model.State.GetValueOrDefault()?.Split(';'), + Duration = timestamps?.End - timestamps?.Start, + AlbumArt = albumArtId != null ? CDN.GetSpotifyAlbumArtUrl(albumArtId) : null, + Type = ActivityType.Listening + }; + } + // Rich Game if (model.ApplicationId.IsSpecified) { @@ -34,7 +55,7 @@ namespace Discord.WebSocket } // (Small, Large) - public static GameAsset[] ToEntity(this API.GameAssets model, ulong appId) + public static GameAsset[] ToEntity(this API.GameAssets model, ulong? appId = null) { return new GameAsset[] { diff --git a/src/Discord.Net.Webhook/Discord.Net.Webhook.csproj b/src/Discord.Net.Webhook/Discord.Net.Webhook.csproj index 7c224e01e..a35e5d578 100644 --- a/src/Discord.Net.Webhook/Discord.Net.Webhook.csproj +++ b/src/Discord.Net.Webhook/Discord.Net.Webhook.csproj @@ -4,7 +4,7 @@ Discord.Net.Webhook Discord.Webhook A core Discord.Net library containing the Webhook client and models. - netstandard1.1 + netstandard1.1;netstandard1.3 diff --git a/src/Discord.Net.Webhook/DiscordWebhookClient.cs b/src/Discord.Net.Webhook/DiscordWebhookClient.cs index 59cc8f3e7..088988c9a 100644 --- a/src/Discord.Net.Webhook/DiscordWebhookClient.cs +++ b/src/Discord.Net.Webhook/DiscordWebhookClient.cs @@ -7,6 +7,7 @@ using Discord.Rest; namespace Discord.Webhook { + /// A client responsible for connecting as a Webhook. public class DiscordWebhookClient : IDisposable { public event Func Log { add { _logEvent.Add(value); } remove { _logEvent.Remove(value); } } @@ -19,14 +20,14 @@ namespace Discord.Webhook internal API.DiscordRestApiClient ApiClient { get; } internal LogManager LogManager { get; } - /// Creates a new Webhook discord client. + /// Creates a new Webhook Discord client. public DiscordWebhookClient(IWebhook webhook) : this(webhook.Id, webhook.Token, new DiscordRestConfig()) { } - /// Creates a new Webhook discord client. + /// Creates a new Webhook Discord client. public DiscordWebhookClient(ulong webhookId, string webhookToken) : this(webhookId, webhookToken, new DiscordRestConfig()) { } - /// Creates a new Webhook discord client. + /// Creates a new Webhook Discord client. public DiscordWebhookClient(ulong webhookId, string webhookToken, DiscordRestConfig config) : this(config) { @@ -34,7 +35,7 @@ namespace Discord.Webhook ApiClient.LoginAsync(TokenType.Webhook, webhookToken).GetAwaiter().GetResult(); Webhook = WebhookClientHelper.GetWebhookAsync(this, webhookId).GetAwaiter().GetResult(); } - /// Creates a new Webhook discord client. + /// Creates a new Webhook Discord client. public DiscordWebhookClient(IWebhook webhook, DiscordRestConfig config) : this(config) { @@ -61,19 +62,22 @@ namespace Discord.Webhook } private static API.DiscordRestApiClient CreateApiClient(DiscordRestConfig config) => new API.DiscordRestApiClient(config.RestClientProvider, DiscordRestConfig.UserAgent); - - /// Sends a message using to the channel for this webhook. Returns the ID of the created message. + + /// Sends a message using to the channel for this webhook. + /// Returns the ID of the created message. public Task SendMessageAsync(string text, bool isTTS = false, IEnumerable embeds = null, string username = null, string avatarUrl = null, RequestOptions options = null) => WebhookClientHelper.SendMessageAsync(this, text, isTTS, embeds, username, avatarUrl, options); #if FILESYSTEM - /// Send a message to the channel for this webhook with an attachment. Returns the ID of the created message. + /// Sends a message to the channel for this webhook with an attachment. + /// Returns the ID of the created message. public Task SendFileAsync(string filePath, string text, bool isTTS = false, IEnumerable embeds = null, string username = null, string avatarUrl = null, RequestOptions options = null) => WebhookClientHelper.SendFileAsync(this, filePath, text, isTTS, embeds, username, avatarUrl, options); #endif - /// Send a message to the channel for this webhook with an attachment. Returns the ID of the created message. + /// Sends a message to the channel for this webhook with an attachment. + /// Returns the ID of the created message. public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, IEnumerable embeds = null, string username = null, string avatarUrl = null, RequestOptions options = null) => WebhookClientHelper.SendFileAsync(this, stream, filename, text, isTTS, embeds, username, avatarUrl, options); diff --git a/src/Discord.Net.Webhook/Entities/Webhooks/RestInternalWebhook.cs b/src/Discord.Net.Webhook/Entities/Webhooks/RestInternalWebhook.cs index cd35d731c..60cb89ee2 100644 --- a/src/Discord.Net.Webhook/Entities/Webhooks/RestInternalWebhook.cs +++ b/src/Discord.Net.Webhook/Entities/Webhooks/RestInternalWebhook.cs @@ -49,7 +49,7 @@ namespace Discord.Webhook public async Task ModifyAsync(Action func, RequestOptions options = null) { - var model = await WebhookClientHelper.ModifyAsync(_client, func, options); + var model = await WebhookClientHelper.ModifyAsync(_client, func, options).ConfigureAwait(false); Update(model); } diff --git a/src/Discord.Net.Webhook/WebhookClientHelper.cs b/src/Discord.Net.Webhook/WebhookClientHelper.cs index 1116662a6..992ae03ab 100644 --- a/src/Discord.Net.Webhook/WebhookClientHelper.cs +++ b/src/Discord.Net.Webhook/WebhookClientHelper.cs @@ -14,7 +14,7 @@ namespace Discord.Webhook { public static async Task GetWebhookAsync(DiscordWebhookClient client, ulong webhookId) { - var model = await client.ApiClient.GetWebhookAsync(webhookId); + var model = await client.ApiClient.GetWebhookAsync(webhookId).ConfigureAwait(false); if (model == null) throw new InvalidOperationException("Could not find a webhook for the supplied credentials."); return RestInternalWebhook.Create(client, model);